Files
diegosouzapw--omniroute/tests/unit/build/check-openapi-breaking.test.ts
2026-07-13 13:39:12 +08:00

175 lines
6.7 KiB
TypeScript

// tests/unit/build/check-openapi-breaking.test.ts
// TDD unit tests for scripts/check/check-openapi-breaking.mjs — Fase 8 B.4 oasdiff.
//
// Strategy:
// • parseOasdiffBreaking() — pure parser, tested with synthetic oasdiff JSON
// (the REAL shape emitted by `oasdiff breaking --format json`, verified at 1.19.1).
// • binary-absent SKIP — spawn the gate with a PATH stripped of oasdiff and assert
// it prints `openapiBreaking=SKIP reason=binary-absent` and exits 0 (advisory).
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import { parseOasdiffBreaking } from "../../../scripts/check/check-openapi-breaking.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "../../..");
const GATE = path.join(REPO_ROOT, "scripts/check/check-openapi-breaking.mjs");
// ---------------------------------------------------------------------------
// Fixtures — REAL shape of `oasdiff breaking --format json` (oasdiff 1.19.1).
// e.g. removing a path emits:
// {"id":"api-path-removed-without-deprecation","text":"api path removed...",
// "level":3,"operation":"GET","path":"/api/bar","section":"paths",...}
// ---------------------------------------------------------------------------
function makeBreaking(
overrides: {
id?: string;
text?: string;
level?: number;
operation?: string;
path?: string;
} = {}
) {
return {
id: overrides.id ?? "api-path-removed-without-deprecation",
text: overrides.text ?? "api path removed without deprecation",
level: overrides.level ?? 3,
operation: overrides.operation ?? "GET",
path: overrides.path ?? "/api/foo",
section: "paths",
baseSource: { file: "/tmp/base.yaml", line: 12, column: 5 },
fingerprint: "33e8aeb41d4a",
};
}
// ---------------------------------------------------------------------------
// parseOasdiffBreaking — empty / invalid input
// ---------------------------------------------------------------------------
test("parseOasdiffBreaking: null retorna count=0", () => {
const r = parseOasdiffBreaking(null);
assert.equal(r.count, 0);
assert.deepEqual(r.byId, {});
assert.deepEqual(r.byPath, {});
assert.deepEqual(r.items, []);
});
test("parseOasdiffBreaking: undefined retorna count=0", () => {
const r = parseOasdiffBreaking(undefined as unknown as null);
assert.equal(r.count, 0);
});
test("parseOasdiffBreaking: array vazio (sem breaking change) retorna count=0", () => {
const r = parseOasdiffBreaking([]);
assert.equal(r.count, 0);
assert.deepEqual(r.byId, {});
assert.deepEqual(r.byPath, {});
});
test("parseOasdiffBreaking: objeto (não-array) retorna count=0", () => {
const r = parseOasdiffBreaking({ id: "x" } as unknown as null);
assert.equal(r.count, 0);
});
test("parseOasdiffBreaking: string retorna count=0", () => {
const r = parseOasdiffBreaking("breaking" as unknown as null);
assert.equal(r.count, 0);
});
test("parseOasdiffBreaking: número retorna count=0", () => {
const r = parseOasdiffBreaking(42 as unknown as null);
assert.equal(r.count, 0);
});
// ---------------------------------------------------------------------------
// parseOasdiffBreaking — counting
// ---------------------------------------------------------------------------
test("parseOasdiffBreaking: 1 breaking change retorna count=1", () => {
const r = parseOasdiffBreaking([makeBreaking()]);
assert.equal(r.count, 1);
assert.equal(r.items.length, 1);
});
test("parseOasdiffBreaking: 3 breaking changes retorna count=3", () => {
const r = parseOasdiffBreaking([
makeBreaking({ id: "api-path-removed-without-deprecation", path: "/api/a" }),
makeBreaking({ id: "request-parameter-became-required", path: "/api/b" }),
makeBreaking({ id: "response-property-removed", path: "/api/c" }),
]);
assert.equal(r.count, 3);
});
test("parseOasdiffBreaking: ignora entradas null/não-objeto no array", () => {
const r = parseOasdiffBreaking([makeBreaking(), null, 5, makeBreaking({ path: "/api/x" })]);
assert.equal(r.count, 2);
});
// ---------------------------------------------------------------------------
// parseOasdiffBreaking — grouping
// ---------------------------------------------------------------------------
test("parseOasdiffBreaking: agrupa por id em byId", () => {
const r = parseOasdiffBreaking([
makeBreaking({ id: "api-path-removed-without-deprecation", path: "/api/a" }),
makeBreaking({ id: "response-property-removed", path: "/api/b" }),
makeBreaking({ id: "api-path-removed-without-deprecation", path: "/api/c" }),
]);
assert.equal(r.byId["api-path-removed-without-deprecation"], 2);
assert.equal(r.byId["response-property-removed"], 1);
});
test("parseOasdiffBreaking: agrupa por path em byPath", () => {
const r = parseOasdiffBreaking([
makeBreaking({ path: "/api/chat", operation: "POST" }),
makeBreaking({ path: "/api/chat", operation: "GET" }),
makeBreaking({ path: "/api/models" }),
]);
assert.equal(r.byPath["/api/chat"], 2);
assert.equal(r.byPath["/api/models"], 1);
});
test("parseOasdiffBreaking: id ausente usa 'unknown'", () => {
const r = parseOasdiffBreaking([{ path: "/api/x", text: "weird" }]);
assert.equal(r.count, 1);
assert.equal(r.byId["unknown"], 1);
});
test("parseOasdiffBreaking: path ausente usa 'unknown'", () => {
const r = parseOasdiffBreaking([{ id: "some-rule", text: "weird" }]);
assert.equal(r.count, 1);
assert.equal(r.byPath["unknown"], 1);
});
// ---------------------------------------------------------------------------
// Integration — binary-absent SKIP (advisory, exit 0)
//
// Run the gate with a PATH that does NOT contain oasdiff. `findOasdiff()` then
// returns null and the gate must SKIP gracefully with exit 0. We point HOME to a
// temp dir too so a user-local ~/.local/bin/oasdiff cannot leak into the lookup.
// ---------------------------------------------------------------------------
test("gate: SKIP graceful (exit 0) quando oasdiff ausente do PATH", () => {
const res = spawnSync(process.execPath, [GATE, "--quiet"], {
cwd: REPO_ROOT,
encoding: "utf8",
timeout: 30_000,
env: {
// Minimal PATH with only the dir holding the node binary — no oasdiff.
PATH: path.dirname(process.execPath),
HOME: "/nonexistent-home-for-oasdiff-test",
BASE_REF: "origin/release/v3.8.26",
},
});
assert.equal(res.status, 0, `expected exit 0, got ${res.status}; stderr: ${res.stderr}`);
assert.match(
res.stdout,
/openapiBreaking=SKIP reason=binary-absent/,
`expected binary-absent SKIP, got stdout: ${res.stdout}`
);
});