import { describe, expect, expectTypeOf, it, vi } from "vitest"; import { ApiError, ApiValidationError, createApiClient } from "../src/api/client"; function mockFetch(status: number, body: unknown) { return vi.fn( async (_url: RequestInfo | URL, _init?: RequestInit) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }), ); } describe("typed API client", () => { it("returns a valid response, typed", async () => { const fetch = mockFetch(200, { realTime: 1000, gameTime: 60_000 }); const api = createApiClient({ fetch }); const time = await api.call("getServerTime", {}); expect(time).toEqual({ realTime: 1000, gameTime: 60_000 }); expectTypeOf(time.realTime).toEqualTypeOf(); expect(fetch).toHaveBeenCalledWith("/api/time", expect.objectContaining({ method: "GET" })); }); it("raises a validation error when the response breaks its schema", async () => { const api = createApiClient({ fetch: mockFetch(200, { realTime: "soon" }) }); await expect(api.call("getServerTime", {})).rejects.toBeInstanceOf(ApiValidationError); }); it("fills path parameters and sends the rest as a JSON body", async () => { const pilot = { id: "p1", name: "Rae Okafor", isAvatar: false, xp: 0, level: 0, skills: {}, skillPointsAvailable: 0, assignedShipId: "s1", status: "idle", locationId: "earth", }; const fetch = mockFetch(200, { pilot }); const api = createApiClient({ fetch }); await api.call("assignPilot", { pilotId: "p1", shipId: "s1" }); const [url, init] = fetch.mock.calls[0]!; expect(url).toBe("/api/pilots/p1/assign"); expect(init).toMatchObject({ method: "POST", credentials: "include" }); expect(JSON.parse(init!.body as string)).toEqual({ shipId: "s1" }); }); it("puts path parameters in GET URLs", async () => { const fetch = mockFetch(200, { contracts: [], periodEndsAt: 7_200_000 }); await createApiClient({ fetch }).call("getContractBoard", { locationId: "moon" }); expect(fetch.mock.calls[0]![0]).toBe("/api/bays/moon/board"); }); it("throws the server's error code and message", async () => { const fetch = mockFetch(409, { error: { code: "insufficient-funds", message: "Not enough credits." }, }); const error = await createApiClient({ fetch }) .call("buyShip", { modelId: "courier" }) .catch((e: unknown) => e); expect(error).toBeInstanceOf(ApiError); expect(error).toMatchObject({ status: 409, code: "insufficient-funds", message: "Not enough credits.", }); }); });