test(#411): QA-014 — add verifySession non-Error thrown value tests

Verify verifySession returns null when getSession throws non-Error
values (strings, objects) rather than crashing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 14:03:08 -06:00
parent 0a2eaaa5e4
commit e0d6d585b3
2 changed files with 121 additions and 3 deletions

View File

@@ -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"));

View File

@@ -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<void> => {
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(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
// 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<void> => {
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(
<AuthProvider>
<TestComponent />
</AuthProvider>
);
// 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(() => {