deadhead-space-freight-tycoon/apps/web/test/api-client.test.ts

73 lines
2.6 KiB
TypeScript

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<number>();
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/locations/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.",
});
});
});