test(api): fill router auth and security coverage gaps

Four new test files — 27 tests total:
- role-router-auth.test.ts (8): UNAUTHORIZED/FORBIDDEN on all mutations for
  unauthenticated/USER callers; MANAGER and ADMIN happy paths
- webhook-router-auth.test.ts (6): adminProcedure guard verified for all
  six webhook procedures across USER/MANAGER/ADMIN roles
- comment-sanitization-router.test.ts (4): proves stripHtml runs before
  db.comment.create — script tags stripped, plain text and @mentions preserved
- auth-anomaly-check/route.test.ts (+5 unit tests): detectAuthAnomalies()
  unit coverage — empty window, global threshold, per-entity threshold, null
  entityId, and both anomaly types firing simultaneously

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-02 23:31:26 +02:00
parent 1d02afddfd
commit ed4d4e4640
4 changed files with 616 additions and 0 deletions
@@ -203,3 +203,107 @@ describe("GET /api/cron/auth-anomaly-check — error handling", () => {
expect(body.ok).toBe(false);
});
});
// ─── Unit tests for detectAuthAnomalies() ─────────────────────────────────────
// These call the exported pure function directly, bypassing the HTTP layer and
// the CRON_SECRET check, to verify threshold logic in isolation.
describe("detectAuthAnomalies — unit tests", () => {
beforeEach(() => { vi.clearAllMocks(); });
it("returns empty anomalies and zero totalFailures when no events are found", async () => {
auditLogFindManyMock.mockResolvedValue([]);
const { detectAuthAnomalies } = await importRoute();
const result = await detectAuthAnomalies();
expect(result.totalFailures).toBe(0);
expect(result.anomalies).toHaveLength(0);
});
it("returns HIGH_GLOBAL_FAILURE_RATE when total failures reach the global threshold", async () => {
const events = Array.from({ length: THRESHOLDS.globalFailures }, (_, i) => ({
entityId: `user_${i}`,
summary: "Login failed: bad password",
}));
auditLogFindManyMock.mockResolvedValue(events);
const { detectAuthAnomalies } = await importRoute();
const result = await detectAuthAnomalies();
const globalAnomaly = result.anomalies.find(
(a) => a.type === "HIGH_GLOBAL_FAILURE_RATE",
);
expect(globalAnomaly).toBeDefined();
expect(globalAnomaly!.count).toBe(THRESHOLDS.globalFailures);
expect(result.totalFailures).toBe(THRESHOLDS.globalFailures);
});
it("returns CONCENTRATED_FAILURES when one entityId accumulates enough failures", async () => {
const events = Array.from({ length: THRESHOLDS.perEntityFailures }, () => ({
entityId: "user_attacker",
summary: "Login failed: bad password",
}));
auditLogFindManyMock.mockResolvedValue(events);
const { detectAuthAnomalies } = await importRoute();
const result = await detectAuthAnomalies();
const concentrated = result.anomalies.find(
(a) => a.type === "CONCENTRATED_FAILURES",
);
expect(concentrated).toBeDefined();
expect(concentrated!.count).toBe(THRESHOLDS.perEntityFailures);
expect(concentrated!.entityId).toBe("user_attacker");
});
it("does not flag CONCENTRATED_FAILURES when entityId is null, even when global threshold is reached", async () => {
// 15 events all with null entityId — below global threshold (20) but we
// explicitly confirm no CONCENTRATED_FAILURES regardless of count.
const events = Array.from({ length: 15 }, () => ({
entityId: null,
summary: "Login failed: unknown user",
}));
auditLogFindManyMock.mockResolvedValue(events);
const { detectAuthAnomalies } = await importRoute();
const result = await detectAuthAnomalies();
const concentrated = result.anomalies.find(
(a) => a.type === "CONCENTRATED_FAILURES",
);
expect(concentrated).toBeUndefined();
});
it("fires both HIGH_GLOBAL_FAILURE_RATE and CONCENTRATED_FAILURES simultaneously", async () => {
// 20 events total; 10 attributed to user_bot → both thresholds are hit.
const botEvents = Array.from({ length: THRESHOLDS.perEntityFailures }, () => ({
entityId: "user_bot",
summary: "Login failed: bad password",
}));
const otherEvents = Array.from(
{ length: THRESHOLDS.globalFailures - THRESHOLDS.perEntityFailures },
(_, i) => ({
entityId: `user_other_${i}`,
summary: "Login failed: bad password",
}),
);
auditLogFindManyMock.mockResolvedValue([...botEvents, ...otherEvents]);
const { detectAuthAnomalies } = await importRoute();
const result = await detectAuthAnomalies();
expect(result.totalFailures).toBe(THRESHOLDS.globalFailures);
const globalAnomaly = result.anomalies.find(
(a) => a.type === "HIGH_GLOBAL_FAILURE_RATE",
);
expect(globalAnomaly).toBeDefined();
const concentrated = result.anomalies.find(
(a) => a.type === "CONCENTRATED_FAILURES" && a.entityId === "user_bot",
);
expect(concentrated).toBeDefined();
expect(concentrated!.count).toBe(THRESHOLDS.perEntityFailures);
});
});