/* @vitest-environment node */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { apiFetch, ApiClientError, getFiles, login, getMe, search, changePassword, adminListUsers, adminCreateUser, adminUpdateUser, adminDeleteUser, adminResetUserPassword, } 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"); 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 maps standardized backend errors into ApiClientError", 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", }); }); 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({ files: [] }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); await getFiles("NL01", "2024", "10", "23"); const [url] = fetch.mock.calls[0]; 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"); }); it("getMe calls /api/auth/me", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ user: null }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); const res = await getMe(); expect(res).toEqual({ user: null }); const [url, init] = fetch.mock.calls[0]; expect(url).toBe("/api/auth/me"); expect(init.method).toBe("GET"); }); it("search builds expected query string", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ items: [], nextCursor: null }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); await search({ q: "x", branch: "NL01", limit: 100 }); const [url] = fetch.mock.calls[0]; const u = new URL(url, "http://localhost"); expect(u.pathname).toBe("/api/search"); expect(u.searchParams.get("q")).toBe("x"); expect(u.searchParams.get("branch")).toBe("NL01"); expect(u.searchParams.get("limit")).toBe("100"); }); it("changePassword calls POST /api/auth/change-password", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); await changePassword({ currentPassword: "a", newPassword: "b" }); const [url, init] = fetch.mock.calls[0]; expect(url).toBe("/api/auth/change-password"); expect(init.method).toBe("POST"); }); it("adminUpdateUser calls PATCH /api/admin/users/:id", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); await adminUpdateUser("507f1f77bcf86cd799439011", { role: "admin" }); const [url, init] = fetch.mock.calls[0]; expect(url).toBe("/api/admin/users/507f1f77bcf86cd799439011"); expect(init.method).toBe("PATCH"); }); it("adminDeleteUser calls DELETE /api/admin/users/:id", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); await adminDeleteUser("507f1f77bcf86cd799439099"); const [url, init] = fetch.mock.calls[0]; expect(url).toBe("/api/admin/users/507f1f77bcf86cd799439099"); expect(init.method).toBe("DELETE"); }); it("adminResetUserPassword calls POST /api/admin/users/:id", async () => { fetch.mockResolvedValue( new Response(JSON.stringify({ ok: true, temporaryPassword: "x" }), { status: 200, headers: { "Content-Type": "application/json" }, }), ); await adminResetUserPassword("507f1f77bcf86cd799439099"); const [url, init] = fetch.mock.calls[0]; expect(url).toBe("/api/admin/users/507f1f77bcf86cd799439099"); expect(init.method).toBe("POST"); }); });