/* @vitest-environment node */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { apiFetch, ApiClientError, getFiles, login } from "./apiClient.js"; beforeEach(() => { vi.restoreAllMocks(); global.fetch = vi.fn(); }); describe("lib/frontend/apiClient", () => { it("apiFetch uses credentials=include and cache=no-store", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, }) ); await apiFetch("/api/health"); expect(fetch).toHaveBeenCalledTimes(1); const [url, init] = fetch.mock.calls[0]; expect(url).toBe("/api/health"); expect(init.credentials).toBe("include"); expect(init.cache).toBe("no-store"); }); it("apiFetch serializes JSON bodies and sets Content-Type", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, }) ); await login({ username: "u", password: "p" }); const [, init] = fetch.mock.calls[0]; expect(init.method).toBe("POST"); expect(init.headers.Accept).toBe("application/json"); expect(init.headers["Content-Type"]).toBe("application/json"); expect(init.body).toBe(JSON.stringify({ username: "u", password: "p" })); }); it("apiFetch throws ApiClientError for standardized backend error payloads", async () => { fetch.mockResolvedValue( new Response( JSON.stringify({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }), { status: 401, headers: { "Content-Type": "application/json" } } ) ); await expect(apiFetch("/api/branches")).rejects.toMatchObject({ name: "ApiClientError", status: 401, code: "AUTH_UNAUTHENTICATED", message: "Unauthorized", }); }); it("apiFetch maps network failures to CLIENT_NETWORK_ERROR", async () => { fetch.mockRejectedValue(new Error("connection refused")); try { await apiFetch("/api/branches"); throw new Error("Expected apiFetch to throw"); } catch (err) { expect(err).toBeInstanceOf(ApiClientError); expect(err.code).toBe("CLIENT_NETWORK_ERROR"); expect(err.status).toBe(0); } }); it("getFiles builds the expected query string", async () => { fetch.mockResolvedValue( new Response( JSON.stringify({ branch: "NL01", year: "2024", month: "10", day: "23", files: [], }), { status: 200, headers: { "Content-Type": "application/json" } } ) ); await getFiles("NL01", "2024", "10", "23"); const [url] = fetch.mock.calls[0]; // We do not rely on param ordering beyond URLSearchParams defaults. expect(url).toContain("/api/files?"); expect(url).toContain("branch=NL01"); expect(url).toContain("year=2024"); expect(url).toContain("month=10"); expect(url).toContain("day=23"); }); });