Spaces:
Runtime error
Runtime error
| // 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}` | |
| ); | |
| }); | |