import { describe, expect, test } from "vitest"; import { openApiDocument } from "../apps/api/src/openapi.js"; import { renderOpenApiSecurityGuardResult, validateOpenApiSecurity, type OpenApiDocument } from "./openapi-guard.js"; function cloneDocument(): OpenApiDocument { return structuredClone(openApiDocument) as OpenApiDocument; } function operation(document: OpenApiDocument, path: string, method: string) { const pathItem = document.paths?.[path]; if (!pathItem) throw new Error(`Missing fixture path ${path}`); const candidate = pathItem[method]; if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) { throw new Error(`Missing fixture operation ${method.toUpperCase()} ${path}`); } return candidate as Record; } describe("openapi security guard", () => { test("passes the current OpenAPI document without mutating it", () => { const document = cloneDocument(); const before = JSON.stringify(document); const result = validateOpenApiSecurity(document); expect(result).toEqual({ ok: true, violations: [] }); expect(JSON.stringify(document)).toEqual(before); expect(renderOpenApiSecurityGuardResult(result)).toMatch(/^PASS openapi-security/); }); test("fails when an operation references an undeclared security scheme", () => { const document = cloneDocument(); operation(document, "/v1/session/current-user", "get").security = [{ missingScheme: [] }]; const result = validateOpenApiSecurity(document); expect(result.ok).toBe(false); expect(result.violations).toContain("GET /v1/session/current-user getCurrentUserSession: references undeclared security scheme 'missingScheme'"); }); test("fails when a protected operation omits non-empty security", () => { const document = cloneDocument(); delete operation(document, "/v1/session/current-user", "get").security; const result = validateOpenApiSecurity(document); expect(result.ok).toBe(false); expect(result.violations).toContain("GET /v1/session/current-user getCurrentUserSession: protected operation must declare non-empty security"); }); test("fails when the document has no operation surface", () => { const result = validateOpenApiSecurity({ components: { securitySchemes: { bearerAuth: {}, apiKeyHeader: {} } }, paths: {} }); expect(result.ok).toBe(false); expect(result.violations).toContain("OpenAPI document must define at least one path"); expect(result.violations).toContain("OpenAPI document must define at least one operation"); }); test("fails when required security schemes are missing", () => { const document = cloneDocument(); document.components = { securitySchemes: {} }; const result = validateOpenApiSecurity(document); expect(result.ok).toBe(false); expect(result.violations).toContain("OpenAPI document must declare security scheme 'bearerAuth'"); expect(result.violations).toContain("OpenAPI document must declare security scheme 'apiKeyHeader'"); }); test("enforces public and optional-anonymous allowlist behavior", () => { const publicDocument = cloneDocument(); operation(publicDocument, "/healthz", "get").security = [{ bearerAuth: [] }]; expect(validateOpenApiSecurity(publicDocument).violations).toContain("GET /healthz getHealthz: public allowlisted operation must omit security or declare security: []"); const optionalDocument = cloneDocument(); expect(validateOpenApiSecurity(optionalDocument).ok).toBe(true); const anonymousProtectedDocument = cloneDocument(); operation(anonymousProtectedDocument, "/v1/session/current-user", "get").security = [{ bearerAuth: [] }, {}]; expect(validateOpenApiSecurity(anonymousProtectedDocument).violations).toContain( "GET /v1/session/current-user getCurrentUserSession: anonymous security requirement is only allowed for optional-auth allowlisted operations", ); }); test("fails when scoped apiKeyHeader operations omit permission metadata", () => { const document = cloneDocument(); delete operation(document, "/v1/entities/{entity}/rows", "get")["x-api-key-permissions"]; const result = validateOpenApiSecurity(document); expect(result.ok).toBe(false); expect(result.violations).toContain("GET /v1/entities/{entity}/rows listLegacyEntityRows: scoped apiKeyHeader operation must include x-api-key-permissions"); }); test("fails when service-token metadata operations include scoped permission metadata", () => { const document = cloneDocument(); operation(document, "/v1/entities/{entity}", "get")["x-api-key-permissions"] = { credential: "apiKeyHeader" }; const result = validateOpenApiSecurity(document); expect(result.ok).toBe(false); expect(result.violations).toContain("GET /v1/entities/{entity} getLegacyEntityByName: service-token metadata operation must not include x-api-key-permissions"); }); });