/** * The API's description is the routes' own, committed: emitting it twice * says the same bytes, the file in git is what the table says now, and the * app serves exactly that file. */ import { afterAll, expect, test } from "bun:test"; import { mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { emit, openapi } from "./openapi.ts"; import { routes } from "./routes.ts"; import { serve } from "./server.ts"; const committed = readFileSync( new URL("../contract/openapi.json", import.meta.url), "utf8", ); test("emission is stable", () => { expect(emit()).toBe(emit()); }); test("contract/openapi.json is what the routes say now (else: bun run contract:emit)", () => { const now = JSON.parse(emit()); const stale = !Bun.deepEquals(JSON.parse(committed), now); if (stale) // Generated app's contract CLI: stderr is its output. console.error( "contract/openapi.json is stale: run `bun run contract:emit` and commit it", ); expect(stale).toBe(false); }); test("every API route is described, and every door names its cell of the table", () => { const doc = openapi() as { paths: Record< string, Record< string, { "x-contract"?: { resource: string; verb: string; least: string } } > >; }; for (const route of routes.filter((r) => r.path.startsWith("/api/"))) { const op = doc.paths[route.path.replace(/:(\w+)/g, "{$1}")]?.[ route.method.toLowerCase() ]; expect([route.method, route.path, op !== undefined]).toEqual([ route.method, route.path, true, ]); expect(typeof op?.["x-contract"]?.least).toBe("string"); } expect(doc.paths["/api/audit"]?.get?.["x-contract"]).toEqual({ resource: "audit", verb: "list", least: "admin", }); }); test("GET /openapi.json serves the committed file", async () => { const server = serve({ port: 0, dataDir: mkdtempSync(join(tmpdir(), "app-openapi-")), identityKey: "k", }); afterAll(() => server.stop(true)); const response = await fetch( `http://${server.hostname}:${server.port}/openapi.json`, ); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.text()).toBe(committed); });