diff --git a/apps/api/src/auth/auth.service.spec.ts b/apps/api/src/auth/auth.service.spec.ts index 24e6d3d..56d0f56 100644 --- a/apps/api/src/auth/auth.service.spec.ts +++ b/apps/api/src/auth/auth.service.spec.ts @@ -278,7 +278,7 @@ describe("AuthService", () => { expect(loggerError).toHaveBeenCalledTimes(1); expect(loggerError).toHaveBeenCalledWith( - expect.stringContaining("OIDC provider unreachable"), + expect.stringContaining("OIDC provider unreachable") ); }); @@ -305,7 +305,7 @@ describe("AuthService", () => { expect(loggerError).toHaveBeenCalledTimes(1); expect(loggerError).toHaveBeenCalledWith( - expect.stringContaining("OIDC provider returned non-OK status"), + expect.stringContaining("OIDC provider returned non-OK status") ); }); @@ -332,7 +332,7 @@ describe("AuthService", () => { expect(result).toBe(true); expect(loggerLog).toHaveBeenCalledWith( - expect.stringContaining("OIDC provider recovered after 2 consecutive failure(s)"), + expect.stringContaining("OIDC provider recovered after 2 consecutive failure(s)") ); // Verify counter reset // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -495,6 +495,26 @@ describe("AuthService", () => { expect(result).toBeNull(); }); + it("should return null when getSession throws a non-Error value (string)", async () => { + const auth = service.getAuth(); + const mockGetSession = vi.fn().mockRejectedValue("some error"); + auth.api = { getSession: mockGetSession } as any; + + const result = await service.verifySession("any-token"); + + expect(result).toBeNull(); + }); + + it("should return null when getSession throws a non-Error value (object)", async () => { + const auth = service.getAuth(); + const mockGetSession = vi.fn().mockRejectedValue({ code: "ERR_UNKNOWN" }); + 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")); diff --git a/apps/web/src/lib/auth/auth-context.test.tsx b/apps/web/src/lib/auth/auth-context.test.tsx index ab92a15..d0c88dd 100644 --- a/apps/web/src/lib/auth/auth-context.test.tsx +++ b/apps/web/src/lib/auth/auth-context.test.tsx @@ -158,6 +158,104 @@ describe("AuthContext", (): void => { expect(apiPost).toHaveBeenCalledWith("/auth/sign-out"); }); + it("should clear user and set authError to 'network' when signOut fails with a network error", async (): Promise => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { + // Intentionally empty - suppressing log output in tests + }); + + const mockUser: AuthUser = { + id: "user-1", + email: "test@example.com", + name: "Test User", + }; + + // First: user is logged in + vi.mocked(apiGet).mockResolvedValueOnce({ + user: mockUser, + session: { id: "session-1", token: "token123", expiresAt: futureExpiry() }, + }); + + // signOut request fails with a network error (TypeError with "fetch") + vi.mocked(apiPost).mockRejectedValueOnce(new TypeError("Failed to fetch")); + + render( + + + + ); + + // Wait for authenticated state + await waitFor(() => { + expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated"); + }); + + // Click sign out — the apiPost will reject + const signOutButton = screen.getByRole("button", { name: "Sign Out" }); + signOutButton.click(); + + // User should be cleared (finally block runs even on error) + await waitFor(() => { + expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated"); + }); + + // authError should be set to "network" via classifyAuthError + expect(screen.getByTestId("auth-error")).toHaveTextContent("network"); + + // Verify the sign-out endpoint was still called + expect(apiPost).toHaveBeenCalledWith("/auth/sign-out"); + + consoleErrorSpy.mockRestore(); + }); + + it("should clear user and set authError to 'backend' when signOut fails with a server error", async (): Promise => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { + // Intentionally empty - suppressing log output in tests + }); + + const mockUser: AuthUser = { + id: "user-1", + email: "test@example.com", + name: "Test User", + }; + + // First: user is logged in + vi.mocked(apiGet).mockResolvedValueOnce({ + user: mockUser, + session: { id: "session-1", token: "token123", expiresAt: futureExpiry() }, + }); + + // signOut request fails with a 500 Internal Server Error + vi.mocked(apiPost).mockRejectedValueOnce(new Error("Internal Server Error")); + + render( + + + + ); + + // Wait for authenticated state + await waitFor(() => { + expect(screen.getByTestId("auth-status")).toHaveTextContent("Authenticated"); + }); + + // Click sign out — the apiPost will reject with server error + const signOutButton = screen.getByRole("button", { name: "Sign Out" }); + signOutButton.click(); + + // User should be cleared (finally block runs even on error) + await waitFor(() => { + expect(screen.getByTestId("auth-status")).toHaveTextContent("Not Authenticated"); + }); + + // authError should be set to "backend" via classifyAuthError + expect(screen.getByTestId("auth-error")).toHaveTextContent("backend"); + + // Verify the sign-out endpoint was still called + expect(apiPost).toHaveBeenCalledWith("/auth/sign-out"); + + consoleErrorSpy.mockRestore(); + }); + it("should throw error when useAuth is used outside AuthProvider", (): void => { // Suppress console.error for this test const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {