import { beforeEach, describe, expect, it, vi } from "vitest"; import { ApiError, errMessage, fetchJson } from "./api"; import { saveSettings } from "./settings"; import { jsonResponse } from "./test/setup"; describe("api", () => { beforeEach(() => { localStorage.clear(); saveSettings({ serverUrl: "http://srv", token: "sekret" }); }); it("throws 401 when not configured", async () => { localStorage.clear(); await expect(fetchJson("/api/sessions")).rejects.toMatchObject({ status: 401 }); }); it("GET sends bearer header and parses JSON", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse([{ id: "s1" }])); const out = await fetchJson>("/api/sessions"); expect(out).toEqual([{ id: "s1" }]); const [input, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; expect(input).toBe("http://srv/api/sessions"); expect(init.headers).toEqual({ Authorization: "Bearer sekret" }); expect(init.body).toBeUndefined(); }); it("POST sends JSON content-type with body", async () => { const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse({ ok: true })); await fetchJson("/api/sessions/s1/prompt", { method: "POST", body: JSON.stringify({ message: "hi" }) }); const init = fetchMock.mock.calls[0]?.[1] as RequestInit; expect(init.method).toBe("POST"); expect(init.headers).toEqual({ Authorization: "Bearer sekret", "Content-Type": "application/json" }); }); it("throws ApiError with server error message", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse({ error: "boom" }, 500)); const err: unknown = await fetchJson("/api/sessions").catch((e: unknown) => e); expect(err).toBeInstanceOf(ApiError); expect((err as ApiError).message).toBe("boom"); expect((err as ApiError).status).toBe(500); }); it("falls back to status text when body has no error string", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse({ other: 1 }, 404)); const err: unknown = await fetchJson("/api/sessions").catch((e: unknown) => e); expect((err as ApiError).message).toBe("404 StatusText"); }); it("falls back to status text when body is not JSON", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue({ ok: false, status: 502, statusText: "Bad Gateway", json: async () => { throw new SyntaxError("bad json"); }, } as unknown as Response); const err: unknown = await fetchJson("/api/sessions").catch((e: unknown) => e); expect((err as ApiError).message).toBe("502 Bad Gateway"); }); it("rejects null JSON bodies gracefully in error path", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse(null, 409)); const err: unknown = await fetchJson("/api/sessions").catch((e: unknown) => e); expect((err as ApiError).message).toBe("409 StatusText"); }); it("errMessage maps Error and non-Error values", () => { expect(errMessage(new Error("oops"))).toBe("oops"); expect(errMessage(42)).toBe("42"); expect(errMessage(null)).toBe("null"); }); });