fix(#411): QA-002 — invert verifySession error classification + health check escalation
verifySession now allowlists known auth errors (return null) and re-throws everything else as infrastructure errors. OIDC health check escalates to error level after 3 consecutive failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,8 @@ describe("AuthService", () => {
|
||||
(service as any).lastHealthCheck = 0;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthResult = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).consecutiveHealthFailures = 0;
|
||||
});
|
||||
|
||||
it("should return true when discovery URL returns 200", async () => {
|
||||
@@ -252,6 +254,90 @@ describe("AuthService", () => {
|
||||
expect(result2).toBe(false);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should escalate to error level after 3 consecutive failures", async () => {
|
||||
const mockFetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const loggerWarn = vi.spyOn(service["logger"], "warn");
|
||||
const loggerError = vi.spyOn(service["logger"], "error");
|
||||
|
||||
// Failures 1 and 2 should log at warn level
|
||||
await service.isOidcProviderReachable();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthCheck = 0; // Reset cache
|
||||
await service.isOidcProviderReachable();
|
||||
|
||||
expect(loggerWarn).toHaveBeenCalledTimes(2);
|
||||
expect(loggerError).not.toHaveBeenCalled();
|
||||
|
||||
// Failure 3 should escalate to error level
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthCheck = 0;
|
||||
await service.isOidcProviderReachable();
|
||||
|
||||
expect(loggerError).toHaveBeenCalledTimes(1);
|
||||
expect(loggerError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("OIDC provider unreachable"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should escalate to error level after 3 consecutive non-OK responses", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({ ok: false, status: 503 });
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const loggerWarn = vi.spyOn(service["logger"], "warn");
|
||||
const loggerError = vi.spyOn(service["logger"], "error");
|
||||
|
||||
// Failures 1 and 2 at warn level
|
||||
await service.isOidcProviderReachable();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthCheck = 0;
|
||||
await service.isOidcProviderReachable();
|
||||
|
||||
expect(loggerWarn).toHaveBeenCalledTimes(2);
|
||||
expect(loggerError).not.toHaveBeenCalled();
|
||||
|
||||
// Failure 3 at error level
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthCheck = 0;
|
||||
await service.isOidcProviderReachable();
|
||||
|
||||
expect(loggerError).toHaveBeenCalledTimes(1);
|
||||
expect(loggerError).toHaveBeenCalledWith(
|
||||
expect.stringContaining("OIDC provider returned non-OK status"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should reset failure counter and log recovery on success after failures", async () => {
|
||||
const mockFetch = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
||||
.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
||||
.mockResolvedValueOnce({ ok: true, status: 200 });
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
const loggerLog = vi.spyOn(service["logger"], "log");
|
||||
|
||||
// Two failures
|
||||
await service.isOidcProviderReachable();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthCheck = 0;
|
||||
await service.isOidcProviderReachable();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(service as any).lastHealthCheck = 0;
|
||||
|
||||
// Recovery
|
||||
const result = await service.isOidcProviderReachable();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(loggerLog).toHaveBeenCalledWith(
|
||||
expect.stringContaining("OIDC provider recovered after 2 consecutive failure(s)"),
|
||||
);
|
||||
// Verify counter reset
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect((service as any).consecutiveHealthFailures).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthConfig", () => {
|
||||
@@ -349,14 +435,72 @@ describe("AuthService", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null and log warning on auth verification failure", async () => {
|
||||
it("should return null for 'invalid token' auth error", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue(new Error("Invalid token provided"));
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("bad-token");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for 'expired' auth error", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue(new Error("Token expired"));
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("expired-token");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for 'session not found' auth error", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue(new Error("Session not found"));
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("missing-session");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for 'unauthorized' auth error", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue(new Error("Unauthorized"));
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("unauth-token");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for 'invalid session' auth error", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue(new Error("Invalid session"));
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("invalid-session");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when a non-Error value is thrown", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue("string-error");
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("any-token");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should re-throw unexpected errors that are not known auth errors", async () => {
|
||||
const auth = service.getAuth();
|
||||
const mockGetSession = vi.fn().mockRejectedValue(new Error("Verification failed"));
|
||||
auth.api = { getSession: mockGetSession } as any;
|
||||
|
||||
const result = await service.verifySession("error-token");
|
||||
|
||||
expect(result).toBeNull();
|
||||
await expect(service.verifySession("error-token")).rejects.toThrow("Verification failed");
|
||||
});
|
||||
|
||||
it("should re-throw Prisma infrastructure errors", async () => {
|
||||
|
||||
Reference in New Issue
Block a user