import { describe, it, expect, vi, beforeEach } from "vitest"; // Mock Supabase client const mockGetSession = vi.fn(); vi.mock("@/lib/supabase/client", () => ({ createClient: () => ({ auth: { getSession: mockGetSession, }, }), })); // Must import after mock setup const { api } = await import("@/lib/api"); describe("ApiClient", () => { beforeEach(() => { vi.restoreAllMocks(); localStorage.clear(); mockGetSession.mockResolvedValue({ data: { session: { access_token: "test-token-123" } }, }); }); it("constructs correct URL with /api base", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({ cases: [], total: 0 }), { status: 200 }), ); await api.get("/cases"); expect(fetchSpy).toHaveBeenCalledWith( "/api/cases", expect.objectContaining({ method: "GET" }), ); }); it("does not double-prefix /api/", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.get("/deadlines"); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toBe("/api/deadlines"); expect(url).not.toContain("/api/api/"); }); it("sets Authorization header from Supabase session", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.get("/cases"); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; const headers = requestInit.headers as Record; expect(headers["Authorization"]).toBe("Bearer test-token-123"); }); it("sets X-Tenant-ID header from localStorage", async () => { localStorage.setItem("kanzlai_tenant_id", "tenant-uuid-123"); const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.get("/cases"); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; const headers = requestInit.headers as Record; expect(headers["X-Tenant-ID"]).toBe("tenant-uuid-123"); }); it("omits X-Tenant-ID when not in localStorage", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.get("/cases"); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; const headers = requestInit.headers as Record; expect(headers["X-Tenant-ID"]).toBeUndefined(); }); it("omits Authorization when no session", async () => { mockGetSession.mockResolvedValue({ data: { session: null }, }); const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.get("/cases"); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; const headers = requestInit.headers as Record; expect(headers["Authorization"]).toBeUndefined(); }); it("sends POST with JSON body", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({ id: "new-id" }), { status: 201 }), ); const body = { case_number: "TEST/001", title: "Test Case" }; await api.post("/cases", body); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; expect(requestInit.method).toBe("POST"); expect(requestInit.body).toBe(JSON.stringify(body)); const headers = requestInit.headers as Record; expect(headers["Content-Type"]).toBe("application/json"); }); it("sends PUT with JSON body", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.put("/cases/123", { title: "Updated" }); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; expect(requestInit.method).toBe("PUT"); }); it("sends PATCH with JSON body", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.patch("/deadlines/123/complete", {}); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; expect(requestInit.method).toBe("PATCH"); }); it("sends DELETE", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({}), { status: 200 }), ); await api.delete("/cases/123"); const requestInit = fetchSpy.mock.calls[0][1] as RequestInit; expect(requestInit.method).toBe("DELETE"); }); it("throws ApiError on non-ok response", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({ error: "not found" }), { status: 404 }), ); await expect(api.get("/cases/nonexistent")).rejects.toEqual({ error: "not found", status: 404, }); }); it("handles 204 No Content response", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(null, { status: 204 }), ); const result = await api.delete("/appointments/123"); expect(result).toBeUndefined(); }); it("handles error response without JSON body", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response("Internal Server Error", { status: 500, statusText: "Internal Server Error", }), ); await expect(api.get("/broken")).rejects.toEqual({ error: "Internal Server Error", status: 500, }); }); });