| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- /* @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("adminListUsers includes sort in query string", async () => {
- fetch.mockResolvedValue(
- new Response(JSON.stringify({ items: [], nextCursor: null }), {
- status: 200,
- headers: { "Content-Type": "application/json" },
- }),
- );
- await adminListUsers({ q: "dev", sort: "role_rights", limit: 50 });
- const [url, init] = fetch.mock.calls[0];
- const u = new URL(url, "http://localhost");
- expect(u.pathname).toBe("/api/admin/users");
- expect(u.searchParams.get("q")).toBe("dev");
- expect(u.searchParams.get("sort")).toBe("role_rights");
- expect(u.searchParams.get("limit")).toBe("50");
- expect(init.method).toBe("GET");
- });
- 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");
- });
- });
|