// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The OpenAPI 3.1 document and its drift check (AT2.1 done_when). // // `openapi.json` in the repository root is this function's output. Regenerate it with // `pnpm openapi` after changing a procedure's schemas; the diff is then part of the review, which // is the point -- an input schema that changed is an API change somebody's client will feel. import { appBrand } from "@app/config" import { buildRegistry } from "@conusai-kit/core" import { describe, expect, it } from "vitest" import { openapiDocument, openapiJson } from "../apps/web/src/server/openapi" import { appModules } from "../modules/index.js" const registry = buildRegistry([...appModules]) // The title is the app brand's name, as the route serves it (router.ts): renaming the app in // config/brand.ts is a customer's first edit and must not turn the drift check red (AT2.7). const info = { title: `${appBrand.name} API`, version: registry.kit } describe("the OpenAPI document", () => { it("matches the committed openapi.json", async () => { await expect(openapiJson(registry, info)).toMatchFileSnapshot("../openapi.json") }) it("is OpenAPI 3.1 with one path per procedure", () => { const document = openapiDocument(registry, info) as { openapi: string; paths: Record } expect(document.openapi).toBe("3.1.0") expect(Object.keys(document.paths).length).toBe(registry.procedures.size) expect(document.paths["/api/v1/tenancy.org.create"]).toBeTruthy() }) it("describes every refusal as problem+json", () => { const document = openapiDocument(registry, info) as { paths: Record }> } }> } const operation = document.paths["/api/v1/tenancy.member.invite"]?.post expect(Object.keys(operation?.responses ?? {})).toEqual(expect.arrayContaining(["200", "401", "404", "422", "429"])) expect(operation?.responses["404"]?.content).toHaveProperty(["application/problem+json"]) }) it("requires an Idempotency-Key exactly where the contract declares one", () => { type Operation = { parameters: { name: string; required?: boolean }[] } const document = openapiDocument(registry, info) as { paths: Record } for (const [name, { def }] of registry.procedures) { const path = document.paths[`/api/v1/${name}`] const parameters = (path?.post ?? path?.put)?.parameters ?? [] const header = parameters.find((parameter) => parameter.name === "Idempotency-Key") expect(Boolean(header), `${name} is idempotent=${def.idempotent}`).toBe(def.idempotent) } }) it("documents a procedure with a streamed body as a PUT: the bytes as the body, the rest of its input as the query", () => { const document = openapiDocument(registry, info) as { paths: Record< string, { post?: unknown put?: { parameters: { name: string; in: string; required: boolean }[] requestBody: { content: Record } } } > } const streamed = [...registry.procedures].filter(([, { def }]) => def.stream !== undefined).map(([name]) => name) const expected = ["files.file.putPart"] expected.push("guides.runtime.uploadFile") // kit:guides -- a survey file question's file expect(streamed.sort()).toEqual(expected) const path = document.paths["/api/v1/files.file.putPart"] expect(path?.post).toBeUndefined() const query = path?.put?.parameters.filter((parameter) => parameter.in === "query") expect(query?.map((parameter) => parameter.name).sort()).toEqual(["fileId", "org", "part", "project", "workspace"]) expect(query?.every((parameter) => parameter.required)).toBe(true) // The body is the one field that is not a parameter, and it is binary. expect(query?.map((parameter) => parameter.name)).not.toContain("body") expect(path?.put?.requestBody.content["application/octet-stream"]?.schema).toEqual({ type: "string", format: "binary", }) }) it("documents file downloads as authenticated scoped GET streams rather than JSON or signed URLs", () => { type DownloadOperation = { parameters: { name: string; in: string; required: boolean }[] responses: Record }> } const document = openapiDocument(registry, info) as { paths: Record } const path = document.paths["/api/v1/files.file.download"] expect(path?.post).toBeUndefined() expect(path?.get?.parameters.map(({ name, in: location, required }) => [name, location, required])).toEqual([ ["org", "query", true], ["workspace", "query", true], ["project", "query", true], ["fileId", "query", true], ]) expect(path?.get?.responses["200"]?.content).toHaveProperty( ["application/octet-stream", "schema", "format"], "binary", ) }) })