Files
suaveplan/packages/api-stack/api/api-openapi-ui-scalar/src/handler/handler.test.ts
2026-05-27 23:29:53 +01:00

216 lines
7.7 KiB
TypeScript

import type { Counter, Meter, MeterProvider } from "@opentelemetry/api";
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { BundledAssetProbe } from "../assets/assets.js";
import { createScalarUiHandler } from "./handler.js";
describe("createScalarUiHandler", () => {
it("handler returns HTML referencing openapiUrl", { timeout: 5000 }, () => {
const bundle = createScalarUiHandler({ openapiUrl: "/openapi.json" });
const response = bundle.handler({
method: "GET",
path: "/docs/ui/scalar",
});
expect(response.status).toBe(200);
expect(response.headers["Content-Type"]).toBe(
"text/html; charset=utf-8"
);
// The OpenAPI URL appears in the Scalar configuration block.
const occurrences = response.body.split("/openapi.json").length - 1;
expect(occurrences).toBe(1);
});
it("emits strict CSP — never permits inline scripts", {
timeout: 5000,
}, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "/openapi.json",
assetMode: "cdn",
});
const response = handler({ method: "GET", path: "/docs/ui/scalar" });
const csp = response.headers["Content-Security-Policy"];
expect(csp).toBeDefined();
expect(csp).not.toContain("script-src 'self' 'unsafe-inline'");
expect(response.headers["X-Content-Type-Options"]).toBe("nosniff");
expect(response.headers["Referrer-Policy"]).toBe("no-referrer");
});
it("cdn mode whitelists the CDN origin in CSP", { timeout: 5000 }, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "/openapi.json",
assetMode: "cdn",
cdnUrl: "https://cdn.example.com/scalar/api-reference",
});
const response = handler({ method: "GET", path: "/docs/ui/scalar" });
const csp = response.headers["Content-Security-Policy"] ?? "";
expect(csp).toContain("https://cdn.example.com");
});
it("bundled mode keeps CSP origin-restricted (no cdn host)", {
timeout: 5000,
}, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "/openapi.json",
assetMode: "bundled",
});
const response = handler({ method: "GET", path: "/docs/ui/scalar" });
const csp = response.headers["Content-Security-Policy"] ?? "";
expect(csp).not.toContain("cdn.jsdelivr.net");
});
it("embeds caller-supplied nonce on inline script tag", {
timeout: 5000,
}, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "/openapi.json",
});
const response = handler({
method: "GET",
path: "/docs/ui/scalar",
nonce: "n-1234567",
});
expect(response.body).toContain('nonce="n-1234567"');
});
it("is idempotent across requests", { timeout: 5000 }, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "/openapi.json",
});
const a = handler({ method: "GET", path: "/docs/ui/scalar" });
const b = handler({ method: "GET", path: "/docs/ui/scalar" });
expect(a.body).toBe(b.body);
expect(a.headers).toEqual(b.headers);
});
it("openapiUrl carrying < is JSON-escaped inside the config block", {
timeout: 5000,
}, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "/openapi.json?q=<script>",
});
const { body } = handler({
method: "GET",
path: "/docs/ui/scalar",
});
expect(body).not.toContain("?q=<script>");
expect(body).toContain("\\u003cscript>");
});
it("assetHandler serves bundled JS with cache headers", {
timeout: 5000,
}, () => {
const bundle = createScalarUiHandler({
openapiUrl: "/openapi.json",
assetMode: "bundled",
});
const response = bundle.assetHandler({
method: "GET",
path: `${bundle.options.uiEndpoint}/assets/scalar.js`,
});
expect(response.status).toBe(200);
expect(response.headers["Content-Type"]).toBe(
"application/javascript; charset=utf-8"
);
expect(response.headers["Cache-Control"]).toBe(
"public, max-age=31536000, immutable"
);
});
it("assetHandler tolerates filename without prefix", {
timeout: 5000,
}, () => {
const bundle = createScalarUiHandler({
openapiUrl: "/openapi.json",
});
const response = bundle.assetHandler({
method: "GET",
path: "scalar.css",
});
expect(response.status).toBe(200);
expect(response.headers["Content-Type"]).toBe(
"text/css; charset=utf-8"
);
});
it("assetHandler resolves trailing segment of any path", {
timeout: 5000,
}, () => {
const bundle = createScalarUiHandler({
openapiUrl: "/openapi.json",
});
const response = bundle.assetHandler({
method: "GET",
path: "/some/unrelated/path/scalar.js",
});
expect(response.status).toBe(200);
expect(response.headers["Content-Type"]).toBe(
"application/javascript; charset=utf-8"
);
});
it("accepts no-arg invocation (defaults everywhere)", {
timeout: 5000,
}, () => {
const bundle = createScalarUiHandler();
expect(bundle.options.openapiUrl).toBe("/openapi.json");
});
it("exposes name + version + parsed options", { timeout: 5000 }, () => {
const bundle = createScalarUiHandler({ openapiUrl: "/openapi.json" });
expect(bundle.name).toBe("@suaveplan/api-openapi-ui-scalar");
expect(bundle.version).toBe("0.1.0");
expect(bundle.options.uiEndpoint).toBe("/docs/ui/scalar");
expect(bundle.runtimeUrl.length).toBeGreaterThan(0);
});
it("absolute openapiUrl threads origin into connect-src", {
timeout: 5000,
}, () => {
const { handler } = createScalarUiHandler({
openapiUrl: "https://api.example.com/openapi.json",
});
const response = handler({
method: "GET",
path: "/docs/ui/scalar",
});
const csp = response.headers["Content-Security-Policy"] ?? "";
expect(csp).toContain("connect-src 'self' https://api.example.com");
});
it("honours caller-supplied bundledAssetProbe + meterProvider", {
timeout: 5000,
}, () => {
const observations: Array<{
value: number;
attributes: Record<string, unknown>;
}> = [];
const counter = {
add(value: number, attributes?: Record<string, unknown>): void {
observations.push({
value,
attributes: { ...(attributes ?? {}) },
});
},
} as unknown as Counter;
const meter = {
createCounter(): Counter {
return counter;
},
} as unknown as Meter;
const meterProvider = {
getMeter(): Meter {
return meter;
},
} as unknown as MeterProvider;
const probe: BundledAssetProbe = { hasBundle: () => true };
const bundle = createScalarUiHandler({
openapiUrl: "/openapi.json",
assetMode: "bundled",
meterProvider,
bundledAssetProbe: probe,
});
bundle.handler({ method: "GET", path: "/docs/ui/scalar" });
expect(observations.length).toBe(1);
expect(observations[0]?.attributes).toEqual({ ui: "scalar" });
});
});