105 lines
5.0 KiB
TypeScript
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",
|
|
)
|
|
})
|
|
})
|