Files
template-app/test/openapi.test.ts
T

105 lines
5.0 KiB
TypeScript

// 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<string, unknown> }
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<string, { post: { responses: Record<string, { content?: Record<string, unknown> }> } }>
}
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<string, { post?: Operation; put?: Operation }>
}
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<string, { schema: unknown }> }
}
}
>
}
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<string, { content?: Record<string, { schema?: unknown }> }>
}
const document = openapiDocument(registry, info) as {
paths: Record<string, { get?: DownloadOperation; post?: unknown }>
}
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",
)
})
})