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