Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/openapi-guard.test.ts
T

110 lines
4.8 KiB
TypeScript

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<string, unknown>;
}
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");
});
});