Files
Arkie-Library-Frontend/src/api.test.ts

114 lines
3.6 KiB
TypeScript
Raw Normal View History

2026-05-16 18:21:37 +08:00
import { describe, expect, it, vi } from "vitest";
async function loadApi(apiUrl = "") {
vi.resetModules();
vi.stubEnv("VITE_API_URL", apiUrl);
2026-05-26 14:07:10 +08:00
vi.stubEnv("VITE_API_PREFIX", "");
2026-05-16 18:21:37 +08:00
return import("./api");
}
function jsonResponse(body: unknown, init?: ResponseInit) {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "Content-Type": "application/json" },
...init,
});
}
describe("api helpers", () => {
it("normalizes nullish API arrays", async () => {
const { itemsOrEmpty } = await loadApi();
expect(itemsOrEmpty(null)).toEqual([]);
expect(itemsOrEmpty(undefined)).toEqual([]);
expect(itemsOrEmpty(["a"])).toEqual(["a"]);
});
it("builds asset URLs from API base while preserving absolute URLs", async () => {
const { assetUrl } = await loadApi("https://api.example.com");
expect(assetUrl("/uploads/file.png")).toBe(
"https://api.example.com/uploads/file.png",
);
expect(assetUrl("https://cdn.example.com/file.png")).toBe(
"https://cdn.example.com/file.png",
);
expect(assetUrl(undefined)).toBe("");
});
it("fetches JSON with API base and throws response text on failure", async () => {
const { getJSON } = await loadApi("https://api.example.com");
const fetchMock = vi
.fn()
.mockResolvedValueOnce(jsonResponse({ ok: true }))
.mockResolvedValueOnce(new Response("boom", { status: 500 }));
vi.stubGlobal("fetch", fetchMock);
await expect(getJSON("/api/resources")).resolves.toEqual({ ok: true });
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/api/resources",
);
await expect(getJSON("/api/fail")).rejects.toThrow("boom");
});
2026-05-31 02:44:44 +08:00
it("cleans expired JSON cache entries", async () => {
vi.useFakeTimers();
try {
vi.setSystemTime(new Date("2026-05-31T00:00:00Z"));
const { getJSON, readJSONCache } = await loadApi();
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ items: [1] }));
vi.stubGlobal("fetch", fetchMock);
await getJSON("/api/posts");
expect(readJSONCache("/api/posts")).toEqual({ items: [1] });
vi.setSystemTime(new Date("2026-05-31T00:05:01Z"));
expect(readJSONCache("/api/posts")).toBeNull();
expect(window.localStorage.length).toBe(0);
} finally {
vi.useRealTimers();
}
});
it("prunes old JSON cache entries beyond the entry limit", async () => {
vi.useFakeTimers();
try {
const { getJSON, readJSONCache } = await loadApi();
const fetchMock = vi.fn((url: string) =>
Promise.resolve(jsonResponse({ url })),
);
vi.stubGlobal("fetch", fetchMock);
for (let index = 0; index < 81; index += 1) {
vi.setSystemTime(new Date(2026, 4, 31, 0, 0, index));
await getJSON(`/api/cache-${index}`);
}
expect(readJSONCache("/api/cache-0")).toBeNull();
expect(readJSONCache("/api/cache-80")).toEqual({ url: "/api/cache-80" });
expect(window.localStorage.length).toBe(80);
} finally {
vi.useRealTimers();
}
});
2026-05-16 18:21:37 +08:00
it("posts JSON with optional bearer token", async () => {
const { postJSON } = await loadApi();
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1 }));
vi.stubGlobal("fetch", fetchMock);
await expect(
postJSON("/api/admin/resources", { title: "Demo" }, "token-123"),
).resolves.toEqual({ id: 1 });
expect(fetchMock).toHaveBeenCalledWith("/api/admin/resources", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token-123",
},
body: JSON.stringify({ title: "Demo" }),
});
});
});