Files
EpicNext-Cms/src/lib/admin-fetch.test.ts
T
Simo b3245a18ea
CI / check (push) Successful in 23s
CI / release (push) Skipped
CI / deploy (push) Successful in 41s
fix: bootstrap admin CSRF tokens
2026-08-02 11:46:43 +02:00

94 lines
2.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { adminFetch } from "./admin-fetch";
const TOKEN = "a".repeat(64);
const REFRESHED_TOKEN = "b".repeat(64);
function installDocument(initialToken: string | null) {
let token = initialToken;
const meta = {
getAttribute: (name: string) => (name === "content" ? token : null),
setAttribute: (name: string, value: string) => {
if (name === "content") token = value;
},
};
vi.stubGlobal("document", {
querySelector: () => (token ? meta : null),
createElement: () => meta,
head: { appendChild: vi.fn() },
});
}
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
describe("adminFetch CSRF handling", () => {
beforeEach(() => {
vi.unstubAllGlobals();
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("bootstraps a missing CSRF token before a mutating request", async () => {
installDocument(null);
const fetchMock = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ ok: true, token: TOKEN }))
.mockResolvedValueOnce(jsonResponse({ ok: true }));
vi.stubGlobal("fetch", fetchMock);
const response = await adminFetch("/api/admin/import/furni", {
method: "POST",
});
expect(response.status).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0]?.[0]).toBe("/api/admin/csrf");
const requestHeaders = new Headers(fetchMock.mock.calls[1]?.[1]?.headers);
expect(requestHeaders.get("x-csrf-token")).toBe(TOKEN);
});
it("refreshes and retries once when the server rejects a stale token", async () => {
installDocument(TOKEN);
const fetchMock = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(
jsonResponse(
{ ok: false, error: "Invalid or missing CSRF token" },
403,
),
)
.mockResolvedValueOnce(jsonResponse({ ok: true, token: REFRESHED_TOKEN }))
.mockResolvedValueOnce(jsonResponse({ ok: true, imported: 1 }));
vi.stubGlobal("fetch", fetchMock);
const response = await adminFetch("/api/admin/import/furni", {
method: "POST",
});
expect(await response.json()).toEqual({ ok: true, imported: 1 });
expect(fetchMock).toHaveBeenCalledTimes(3);
const retryHeaders = new Headers(fetchMock.mock.calls[2]?.[1]?.headers);
expect(retryHeaders.get("x-csrf-token")).toBe(REFRESHED_TOKEN);
});
it("does not bootstrap CSRF for read-only requests", async () => {
installDocument(null);
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse([]));
vi.stubGlobal("fetch", fetchMock);
const response = await adminFetch("/api/admin/import/furni");
expect(response.status).toBe(200);
expect(fetchMock).toHaveBeenCalledOnce();
expect(fetchMock.mock.calls[0]?.[0]).toBe("/api/admin/import/furni");
});
});