87 lines
2.9 KiB
TypeScript
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);
|
|
});
|