import { beforeEach, describe, expect, it, vi } from "vitest"; beforeEach(() => { window.localStorage.clear(); vi.unstubAllGlobals(); vi.resetModules(); }); // Fresh module import per test so the internal `refreshPromise` module-level // state (shared across concurrent 401s) never leaks between tests. async function loadApiClient() { return import("@/lib/api-client"); } describe("api client automatic token refresh", () => { it("transparently retries once after a 401 when the refresh succeeds", async () => { const { api, setTokens, getAccessToken } = await loadApiClient(); setTokens("expired-access", "valid-refresh"); const fetchMock = vi .fn() // The actual request, with the now-expired access token -> 401. .mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ detail: "Invalid or expired token" }), }) // POST /auth/refresh -> a fresh token pair. .mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ access_token: "new-access", refresh_token: "new-refresh", token_type: "bearer", expires_in_minutes: 15, }), }) // The retried original request, now with the new access token. .mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ id: "1", email: "a@example.com" }), }); vi.stubGlobal("fetch", fetchMock); const result = await api.me(); expect(result).toEqual({ id: "1", email: "a@example.com" }); expect(fetchMock).toHaveBeenCalledTimes(3); expect(getAccessToken()).toBe("new-access"); const retryCall = fetchMock.mock.calls[2]; const retryHeaders = retryCall?.[1]?.headers as Headers; expect(retryHeaders.get("Authorization")).toBe("Bearer new-access"); }); it("clears tokens and reports the original 401 when the refresh itself also fails", async () => { const { api, setTokens, getAccessToken, getRefreshToken, ApiError } = await loadApiClient(); setTokens("expired-access", "expired-refresh"); const fetchMock = vi .fn() .mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ detail: "Invalid or expired token" }), }) .mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ detail: "Invalid or expired refresh token" }), }); vi.stubGlobal("fetch", fetchMock); await expect(api.me()).rejects.toBeInstanceOf(ApiError); expect(fetchMock).toHaveBeenCalledTimes(2); // original request + failed refresh, no retry expect(getAccessToken()).toBeNull(); expect(getRefreshToken()).toBeNull(); }); it("does not attempt a refresh when there is no refresh token at all", async () => { const { api, clearTokens } = await loadApiClient(); clearTokens(); const fetchMock = vi.fn().mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ detail: "Not authenticated" }), }); vi.stubGlobal("fetch", fetchMock); await expect(api.me()).rejects.toThrow(); expect(fetchMock).toHaveBeenCalledTimes(1); // no refresh attempt made }); });