import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from "axios"; type Harness = { api: typeof import("./api").api; boundary: typeof import("./sessionBoundary") }; let harness: Harness; let listeners: Array<[string, EventListenerOrEventListenerObject]>; let addListener: jest.SpyInstance; function cookie(value: string) { document.cookie = `XSRF-TOKEN=${value}; path=/`; } function response(config: InternalAxiosRequestConfig, data: unknown): AxiosResponse { return { config, data, status: 200, statusText: "OK", headers: {} }; } async function bindA() { cookie("csrf-a"); harness.api.defaults.adapter = async config => response(config, { id: "user-a", sessionContext: "session-a" }); await harness.api.get("/auth/me"); } beforeEach(() => { jest.useFakeTimers(); window.localStorage.clear(); document.cookie = "XSRF-TOKEN=; max-age=0; path=/"; listeners = []; const originalAdd = window.addEventListener.bind(window); addListener = jest.spyOn(window, "addEventListener").mockImplementation((type, callback, options) => { listeners.push([type, callback]); originalAdd(type, callback, options); }); jest.isolateModules(() => { // setupTests keeps its API mock for UI tests; these tests explicitly exercise real Axios. harness = { api: jest.requireActual("./api").api, boundary: jest.requireActual("./sessionBoundary") }; }); }); afterEach(() => { for (const [type, callback] of listeners) window.removeEventListener(type, callback); addListener.mockRestore(); jest.clearAllTimers(); jest.useRealTimers(); }); test("writes carry the page's server-issued context and preserve the same account", async () => { await bindA(); const calls: InternalAxiosRequestConfig[] = []; harness.api.defaults.adapter = async config => { calls.push(config); return response(config, {}); }; await harness.api.put("/career/profile", { profile: { contact: { fullName: "A edit" } } }); expect(calls).toHaveLength(1); expect(calls[0].headers.get("X-Session-Context")).toBe("session-a"); expect(calls[0].headers.get("X-CSRF-TOKEN")).toBe("csrf-a"); expect(harness.boundary.getSessionContext()).toBe("session-a"); }); test("cookie switch rejects a stale callback before transport and never adopts B's context", async () => { await bindA(); const adapter = jest.fn(async config => response(config, {})); harness.api.defaults.adapter = adapter; const queuedSave = () => harness.api.put("/career/profile", { profile: { contact: { fullName: "A retained draft" } } }); cookie("csrf-b"); await expect(queuedSave()).rejects.toMatchObject({ code: "ERR_CANCELED" }); expect(adapter).not.toHaveBeenCalled(); expect(harness.boundary.isSessionInvalidated()).toBe(true); expect(harness.boundary.bindSession("user-b", "session-b")).toBe(false); expect(harness.boundary.getSessionContext()).toBeNull(); }); test.each([ ["authUserKey", "user-b"], ["authUserKey", null], ["authSessionContext", "new-session-for-same-user"], [null, null], ])("cross-tab storage event %s invalidates and aborts outstanding requests", async (key, newValue) => { await bindA(); window.dispatchEvent(new StorageEvent("storage", { key, newValue })); expect(harness.boundary.isSessionInvalidated()).toBe(true); expect(harness.boundary.sessionRequestSignal().aborted).toBe(true); }); test("another session cannot be adopted even if the readable cookie signal is unchanged", async () => { await bindA(); harness.api.defaults.adapter = async config => response(config, { id: "user-b", sessionContext: "session-b" }); await expect(harness.api.get("/auth/me")).rejects.toMatchObject({ code: "ERR_CANCELED" }); expect(harness.boundary.getSessionContext()).toBeNull(); }); test("same-account reauthentication also discards the old page", async () => { await bindA(); expect(harness.boundary.bindSession("user-a", "session-a-new")).toBe(false); expect(harness.boundary.isSessionInvalidated()).toBe(true); }); test("a delayed private response cannot repopulate caches after invalidation", async () => { await bindA(); let finish!: (value: AxiosResponse) => void; let request!: InternalAxiosRequestConfig; harness.api.defaults.adapter = config => { request = config; return new Promise(resolve => { finish = resolve; }); }; const read = harness.api.get("/companies"); cookie("csrf-b"); window.dispatchEvent(new Event("focus")); finish(response(request, [{ id: 1, name: "Private A company" }])); await expect(read).rejects.toMatchObject({ code: "ERR_CANCELED" }); }); test.each([false, true])("a late bootstrap read/error cannot overwrite or sign out a newly bound session (error=%s)", async failure => { cookie("csrf-a"); let finish!: (value: AxiosResponse) => void; let fail!: (reason: unknown) => void; let oldRequest!: InternalAxiosRequestConfig; harness.api.defaults.adapter = config => { if (config.url === "/companies") { oldRequest = config; return new Promise((resolve, reject) => { finish = resolve; fail = reject; }); } return Promise.resolve(response(config, { id: "user-b", sessionContext: "session-b" })); }; const oldRead = harness.api.get("/companies"); cookie("csrf-b"); await harness.api.get("/auth/me"); if (failure) fail(new AxiosError("Expired old session", undefined, oldRequest, undefined, { ...response(oldRequest, {}), status: 401 })); else finish(response(oldRequest, [{ name: "A private company" }])); await expect(oldRead).rejects.toMatchObject({ code: "ERR_CANCELED" }); expect(harness.boundary.getSessionContext()).toBe("session-b"); expect(window.localStorage.getItem("authUserKey")).toBe("user-b"); expect(harness.boundary.isSessionInvalidated()).toBe(false); }); test("server precondition failure clears the page without silently retrying a mutation", async () => { await bindA(); const adapter = jest.fn(async (config: InternalAxiosRequestConfig) => { throw new AxiosError("Session mismatch", undefined, config, undefined, { ...response(config, { code: "session_context_mismatch" }), status: 409 }); }); harness.api.defaults.adapter = adapter; await expect(harness.api.post("/cv/variants", { name: "A draft" })).rejects.toBeDefined(); expect(adapter).toHaveBeenCalledTimes(1); expect(harness.boundary.isSessionInvalidated()).toBe(true); }); test("BFCache restoration invalidates old private state", async () => { await bindA(); window.dispatchEvent(new PageTransitionEvent("pageshow", { persisted: true })); expect(harness.boundary.isSessionInvalidated()).toBe(true); }); test("anonymous public-page history does not require a new login", () => { window.dispatchEvent(new PageTransitionEvent("pageshow", { persisted: true })); expect(harness.boundary.isSessionInvalidated()).toBe(false); }); test("cookie fallback still works without a cross-tab storage event", async () => { await bindA(); cookie("csrf-b"); jest.advanceTimersByTime(1000); expect(harness.boundary.isSessionInvalidated()).toBe(true); });