fix(#411): QA-005 — production logging, error classification, session-expired state

logAuthError now always logs (not dev-only). Replaced isBackendError with
parseAuthError-based classification. signOut uses proper error type.
Session expiry sets explicit session_expired state. Login page logs in prod.
Fixed pre-existing lint violations in auth package (campsite rule).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-16 13:37:49 -06:00
parent 8a572e8525
commit 752e839054
10 changed files with 201 additions and 139 deletions

View File

@@ -105,7 +105,7 @@ describe("fetchWithRetry", (): void => {
fetchWithRetry("https://api.example.com/auth/config", undefined, {
maxRetries: 3,
baseDelayMs: 1000,
}),
})
).rejects.toThrow("Failed to fetch");
// 1 initial + 3 retries = 4 total attempts
@@ -149,15 +149,13 @@ describe("fetchWithRetry", (): void => {
it("should respect custom maxRetries option", async (): Promise<void> => {
const networkError = new TypeError("Failed to fetch");
vi.mocked(global.fetch)
.mockRejectedValueOnce(networkError)
.mockRejectedValueOnce(networkError);
vi.mocked(global.fetch).mockRejectedValueOnce(networkError).mockRejectedValueOnce(networkError);
await expect(
fetchWithRetry("https://api.example.com/auth/config", undefined, {
maxRetries: 1,
baseDelayMs: 50,
}),
})
).rejects.toThrow("Failed to fetch");
// 1 initial + 1 retry = 2 total attempts
@@ -202,7 +200,7 @@ describe("fetchWithRetry", (): void => {
});
it("should log retry attempts in all environments", async (): Promise<void> => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation((): void => {});
const warnSpy = vi.spyOn(console, "warn").mockReturnValue(undefined);
const okResponse = mockResponse(200);
vi.mocked(global.fetch)
@@ -212,28 +210,24 @@ describe("fetchWithRetry", (): void => {
await fetchWithRetry("https://api.example.com/auth/config");
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[Auth] Retry 1/3"),
);
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("[Auth] Retry 1/3"));
warnSpy.mockRestore();
});
it("should log retry attempts for HTTP errors", async (): Promise<void> => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation((): void => {});
const warnSpy = vi.spyOn(console, "warn").mockReturnValue(undefined);
const serverError = mockResponse(500);
const okResponse = mockResponse(200);
vi.mocked(global.fetch)
.mockResolvedValueOnce(serverError)
.mockResolvedValueOnce(okResponse);
vi.mocked(global.fetch).mockResolvedValueOnce(serverError).mockResolvedValueOnce(okResponse);
await fetchWithRetry("https://api.example.com/auth/config");
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[Auth] Retry 1/3 after HTTP 500"),
expect.stringContaining("[Auth] Retry 1/3 after HTTP 500")
);
warnSpy.mockRestore();
@@ -253,7 +247,7 @@ describe("fetchWithRetry", (): void => {
expect(global.fetch).toHaveBeenCalledWith(
"https://api.example.com/auth/config",
requestOptions,
requestOptions
);
});
@@ -262,9 +256,9 @@ describe("fetchWithRetry", (): void => {
const nonRetryableError = new Error("Unauthorized");
vi.mocked(global.fetch).mockRejectedValueOnce(nonRetryableError);
await expect(
fetchWithRetry("https://api.example.com/auth/config"),
).rejects.toThrow("Unauthorized");
await expect(fetchWithRetry("https://api.example.com/auth/config")).rejects.toThrow(
"Unauthorized"
);
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(sleepMock).not.toHaveBeenCalled();