import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; const mockFetch = vi.fn(); describe("fetchApi", () => { beforeEach(() => { vi.stubGlobal("fetch", mockFetch); vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); vi.resetModules(); }); afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); mockFetch.mockReset(); }); async function loadFetchApi() { const mod = await import("../api.js"); return mod.fetchApi; } it("calls the correct URL", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: "test" }), }); const fetchApi = await loadFetchApi(); await fetchApi("/categories"); expect(mockFetch).toHaveBeenCalledOnce(); const calledUrl = mockFetch.mock.calls[0][0]; expect(calledUrl).toBe("http://localhost:3000/api/categories"); }); it("returns parsed JSON on success", async () => { const expected = { categories: [{ id: 1, name: "General" }] }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(expected), }); const fetchApi = await loadFetchApi(); const result = await fetchApi("/categories"); expect(result).toEqual(expected); }); it("throws on non-ok response", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Internal Server Error", }); const fetchApi = await loadFetchApi(); await expect(fetchApi("/fail")).rejects.toThrow( "AppView API error: 500 Internal Server Error" ); }); it("throws on 404 response", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: "Not Found", }); const fetchApi = await loadFetchApi(); await expect(fetchApi("/missing")).rejects.toThrow( "AppView API error: 404 Not Found" ); }); it("forwards cookieHeader as Cookie header when provided", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}), }); const fetchApi = await loadFetchApi(); await fetchApi("/boards", { cookieHeader: "atbb_session=mytoken" }); const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]; expect((init.headers as Record)["Cookie"]).toBe( "atbb_session=mytoken" ); }); it("does not set Cookie header when cookieHeader is not provided", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}), }); const fetchApi = await loadFetchApi(); await fetchApi("/boards"); const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]; expect((init.headers as Record)["Cookie"]).toBeUndefined(); }); it("throws a network error with descriptive message when AppView is unreachable", async () => { mockFetch.mockRejectedValueOnce(new Error("fetch failed: ECONNREFUSED")); const fetchApi = await loadFetchApi(); await expect(fetchApi("/boards")).rejects.toThrow( "AppView network error: fetch failed: ECONNREFUSED" ); }); it("throws a response error when AppView returns malformed JSON", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.reject(new SyntaxError("Unexpected token '<'")), }); const fetchApi = await loadFetchApi(); await expect(fetchApi("/boards")).rejects.toThrow( "AppView response error: invalid JSON from /boards" ); }); });