Files
frontend/src/shared/api/client.test.ts
T
ksv741 edd54892cd fix(auth): isolate session races and financial caches
TD-001 and TD-002: serialize cookie writes, ignore stale responses and reset financial queries on identity changes. Backend cookie lifecycle regression: ec5780d.
2026-09-21 21:09:23 +03:00

97 lines
2.8 KiB
TypeScript

import { HttpResponse, http } from "msw";
import { describe, expect, it, vi } from "vitest";
import { server } from "@/test/server";
import { apiClient, configureAuthClient } from "./client";
describe("apiClient authentication", () => {
it("refreshes once and retries an unauthorized protected request", async () => {
let token = "expired";
const refresh = vi.fn(async () => {
token = "fresh";
return true;
});
const authorization: string[] = [];
server.use(
http.get("*/api/v1/brokerage-accounts", ({ request }) => {
authorization.push(request.headers.get("Authorization") ?? "");
return authorization.length === 1
? HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 })
: HttpResponse.json([]);
}),
);
configureAuthClient({ accessToken: () => token, refresh, clear: vi.fn() });
const { data } = await apiClient.GET("/api/v1/brokerage-accounts");
expect(data).toEqual([]);
expect(refresh).toHaveBeenCalledOnce();
expect(authorization).toEqual(["Bearer expired", "Bearer fresh"]);
});
it("clears the session when refresh cannot restore it", async () => {
const clear = vi.fn();
server.use(
http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 }),
),
);
configureAuthClient({
accessToken: () => "expired",
refresh: async () => false,
clear,
});
await apiClient.GET("/api/v1/brokerage-accounts");
expect(clear).toHaveBeenCalledOnce();
});
it("does not attach Bearer auth to authentication endpoints", async () => {
let authorization: string | null = null;
server.use(
http.post("*/api/v1/auth/refresh", ({ request }) => {
authorization = request.headers.get("Authorization");
return HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 });
}),
);
configureAuthClient({
accessToken: () => "token",
refresh: vi.fn(),
clear: vi.fn(),
});
await apiClient.POST("/api/v1/auth/refresh");
expect(authorization).toBeNull();
});
it("discards a retried request response after another session takes over", async () => {
let generation = 0;
let calls = 0;
let release!: () => void;
const pending = new Promise<void>((resolve) => {
release = resolve;
});
server.use(
http.get("*/api/v1/brokerage-accounts", async () => {
if (++calls === 1)
return HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 });
await pending;
return HttpResponse.json([{ id: "previous-user-account" }]);
}),
);
configureAuthClient({
accessToken: () => "synthetic",
generation: () => generation,
refresh: async () => true,
clear: vi.fn(),
});
const response = expect(
apiClient.GET("/api/v1/brokerage-accounts"),
).rejects.toMatchObject({ name: "AbortError" });
await vi.waitFor(() => expect(calls).toBe(2));
generation += 1;
release();
await response;
});
});