2026-07-02 15:46:41 +08:00

87 lines
2.9 KiB
TypeScript

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<string, string> | 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);
});