import { afterEach, expect, test, vi } from "vitest"; import { apiRequest, setAccessToken, setRefreshHandler, setUnauthorizedHandler } from "./request"; afterEach(() => { vi.useRealTimers(); setAccessToken(""); setRefreshHandler(null); setUnauthorizedHandler(null); vi.unstubAllGlobals(); }); test("retries once after refreshing a stale permission token on 403", async () => { setAccessToken("old-token"); setRefreshHandler(async () => { setAccessToken("new-token"); return true; }); vi.stubGlobal( "fetch", vi.fn(async () => { if (vi.mocked(fetch).mock.calls.length === 1) { return new Response(JSON.stringify({ code: 403, message: "没有操作权限" }), { status: 403 }); } return new Response(JSON.stringify({ code: 0, data: { ok: true } })); }) ); await expect(apiRequest("/v1/admin/countries", { body: {}, method: "POST" })).resolves.toEqual({ ok: true }); expect(fetch).toHaveBeenCalledTimes(2); expect(vi.mocked(fetch).mock.calls[0][1]?.headers).toMatchObject({ Authorization: "Bearer old-token" }); expect(vi.mocked(fetch).mock.calls[1][1]?.headers).toMatchObject({ Authorization: "Bearer new-token" }); }); test("shares one refresh request across concurrent stale access token responses", async () => { setAccessToken("old-token"); let refreshCount = 0; setRefreshHandler(async () => { refreshCount += 1; await new Promise((resolve) => window.setTimeout(resolve, 0)); setAccessToken("new-token"); return true; }); vi.stubGlobal( "fetch", vi.fn(async (_url, init) => { const auth = (init?.headers as Record | undefined)?.Authorization; if (auth === "Bearer old-token") { return new Response(JSON.stringify({ code: 40100, message: "访问凭证已失效" }), { status: 401 }); } return new Response(JSON.stringify({ code: 0, data: { ok: true } })); }) ); await expect(Promise.all([apiRequest("/v1/admin/countries"), apiRequest("/v1/admin/regions")])).resolves.toEqual([ { ok: true }, { ok: true } ]); expect(refreshCount).toBe(1); expect(fetch).toHaveBeenCalledTimes(4); }); test("retries idempotent reads after a transient network failure", async () => { vi.useFakeTimers(); vi.stubGlobal( "fetch", vi .fn() .mockRejectedValueOnce(new TypeError("Failed to fetch")) .mockResolvedValueOnce(new Response(JSON.stringify({ code: 0, data: { ok: true } }))) ); const request = apiRequest("/v1/admin/apps"); await vi.advanceTimersByTimeAsync(350); await expect(request).resolves.toEqual({ ok: true }); expect(fetch).toHaveBeenCalledTimes(2); }); test("does not replay mutations after a network failure", async () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValueOnce(new TypeError("Failed to fetch"))); await expect(apiRequest("/v1/admin/countries", { body: {}, method: "POST" })).rejects.toThrow("Failed to fetch"); expect(fetch).toHaveBeenCalledTimes(1); });