| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
import { afterAll, expect, test } from "bun:test"; |
| 7 |
import { mkdtempSync, readFileSync } from "node:fs"; |
| 8 |
import { tmpdir } from "node:os"; |
| 9 |
import { join } from "node:path"; |
| 10 |
import { emit, openapi } from "./openapi.ts"; |
| 11 |
import { routes } from "./routes.ts"; |
| 12 |
import { serve } from "./server.ts"; |
| 13 |
import { sign } from "./who.ts"; |
| 14 |
|
| 15 |
const committed = readFileSync( |
| 16 |
new URL("../contract/openapi.json", import.meta.url), |
| 17 |
"utf8", |
| 18 |
); |
| 19 |
|
| 20 |
test("emission is stable", () => { |
| 21 |
expect(emit()).toBe(emit()); |
| 22 |
}); |
| 23 |
|
| 24 |
test("contract/openapi.json is what the routes say now (else: bun run contract:emit)", () => { |
| 25 |
const now = JSON.parse(emit()); |
| 26 |
const stale = !Bun.deepEquals(JSON.parse(committed), now); |
| 27 |
if (stale) |
| 28 |
console.error( |
| 29 |
"contract/openapi.json is stale: run `bun run contract:emit` and commit it", |
| 30 |
); |
| 31 |
expect(stale).toBe(false); |
| 32 |
}); |
| 33 |
|
| 34 |
test("every API route is described, and every door names its cell of the table", () => { |
| 35 |
const doc = openapi() as { |
| 36 |
paths: Record< |
| 37 |
string, |
| 38 |
Record< |
| 39 |
string, |
| 40 |
{ "x-contract"?: { resource: string; verb: string; least: string } } |
| 41 |
> |
| 42 |
>; |
| 43 |
}; |
| 44 |
for (const route of routes.filter((r) => r.path.startsWith("/api/"))) { |
| 45 |
const op = |
| 46 |
doc.paths[route.path.replace(/:(\w+)/g, "{$1}")]?.[ |
| 47 |
route.method.toLowerCase() |
| 48 |
]; |
| 49 |
expect([route.method, route.path, op !== undefined]).toEqual([ |
| 50 |
route.method, |
| 51 |
route.path, |
| 52 |
true, |
| 53 |
]); |
| 54 |
expect(typeof op?.["x-contract"]?.least).toBe("string"); |
| 55 |
} |
| 56 |
expect(doc.paths["/api/audit"]?.get?.["x-contract"]).toEqual({ |
| 57 |
resource: "audit", |
| 58 |
verb: "list", |
| 59 |
least: "admin", |
| 60 |
}); |
| 61 |
}); |
| 62 |
|
| 63 |
test("GET /openapi.json serves the committed file", async () => { |
| 64 |
const server = serve({ |
| 65 |
port: 0, |
| 66 |
dataDir: mkdtempSync(join(tmpdir(), "app-openapi-")), |
| 67 |
identityKey: "k", |
| 68 |
}); |
| 69 |
afterAll(() => server.stop(true)); |
| 70 |
|
| 71 |
const identity = `u1|alice|user|${Date.now() + 60_000}`; |
| 72 |
const response = await fetch( |
| 73 |
`http://${server.hostname}:${server.port}/openapi.json`, |
| 74 |
{ headers: { "x-forge-identity": identity, "x-forge-signature": sign(identity, "k") } }, |
| 75 |
); |
| 76 |
expect(response.status).toBe(200); |
| 77 |
expect(response.headers.get("content-type")).toContain("application/json"); |
| 78 |
expect(await response.text()).toBe(committed); |
| 79 |
}); |