import { execFile } from "node:child_process"; import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { describe, expect, test } from "vitest"; import { renderApiInventoryGuardResult, validateApiInventory } from "./check-api-inventory.js"; const execFileAsync = promisify(execFile); async function makeFixture() { const root = await mkdtemp(path.join(os.tmpdir(), "api-inventory-")); const appSourcePath = path.join(root, "app.ts"); const removedWebApiRoutesRoot = path.join(root, "routes", "api"); await writeFile(appSourcePath, 'import { Hono } from "hono";\nconst app = new Hono();\napp.get("/healthz", (c) => c.json({ ok: true }));\n', "utf8"); return { root, appSourcePath, removedWebApiRoutesRoot }; } describe("api inventory guard", () => { test("passes when current inventory has no deprecated route keys", async () => { const result = await validateApiInventory(); expect(result).toEqual({ ok: true, violations: [], missingRoots: [] }); expect(renderApiInventoryGuardResult(result)).toMatch(/^PASS api-inventory/); }); test("fails on denylisted Hono route keys without printing handler source", async () => { const fixture = await makeFixture(); await writeFile( fixture.appSourcePath, 'import { Hono } from "hono";\nconst app = new Hono();\napp.get("/v1/auth/me", () => {\n const token = "SECRET_TOKEN_SHOULD_NOT_PRINT";\n return token;\n});\n', "utf8", ); const result = await validateApiInventory({ appSourcePath: fixture.appSourcePath, removedWebApiRoutesRoot: fixture.removedWebApiRoutesRoot }); const output = renderApiInventoryGuardResult(result); expect(result.ok).toBe(false); expect(output).toContain("GET /v1/auth/me"); expect(output).toContain(fixture.appSourcePath); expect(output).toContain(":3:1"); expect(output).not.toContain("SECRET_TOKEN_SHOULD_NOT_PRINT"); }); test("fails when removed React Router API route files are present", async () => { const fixture = await makeFixture(); await mkdir(path.join(fixture.removedWebApiRoutesRoot, "auth"), { recursive: true }); await writeFile(path.join(fixture.removedWebApiRoutesRoot, "auth", "verify-token.tsx"), "export async function loader() {}\n", "utf8"); const result = await validateApiInventory({ appSourcePath: fixture.appSourcePath, removedWebApiRoutesRoot: fixture.removedWebApiRoutesRoot }); const output = renderApiInventoryGuardResult(result); expect(result.ok).toBe(false); expect(output).toContain("GET /v1/auth/verify-token"); expect(output).toContain("removed-react-router-api"); expect(output).toContain(path.join(fixture.removedWebApiRoutesRoot, "auth", "verify-token.tsx")); }); test("blocks unrelated removed web API runtime routes too", async () => { const fixture = await makeFixture(); await mkdir(path.join(fixture.removedWebApiRoutesRoot, "entities"), { recursive: true }); await writeFile(path.join(fixture.removedWebApiRoutesRoot, "entities", "index.tsx"), "export async function loader() {}\n", "utf8"); const result = await validateApiInventory({ appSourcePath: fixture.appSourcePath, removedWebApiRoutesRoot: fixture.removedWebApiRoutesRoot }); expect(result.ok).toBe(false); expect(result.violations).toEqual([ expect.objectContaining({ routeKey: "REMOVED /api web route", surface: "removed-react-router-api", sourcePath: path.join(fixture.removedWebApiRoutesRoot, "entities", "index.tsx"), }), ]); }); test("passes when the removed web API route root is absent", async () => { const fixture = await makeFixture(); const missingRemovedRoutesRoot = path.join(fixture.root, "missing", "api"); const result = await validateApiInventory({ appSourcePath: fixture.appSourcePath, removedWebApiRoutesRoot: missingRemovedRoutesRoot }); expect(result).toEqual({ ok: true, violations: [], missingRoots: [] }); }); test("CLI fails on unknown flags", async () => { await expect(execFileAsync("pnpm", ["exec", "tsx", "scripts/check-api-inventory.ts", "--unknown"])).rejects.toMatchObject({ code: 1, stderr: expect.stringContaining("FAIL api-inventory: unknown arguments"), }); }); test("diagnostics are route-key and location based", async () => { const fixture = await makeFixture(); await writeFile( fixture.appSourcePath, 'const app = { get() {} };\napp.get("/v1/auth/me", () => "PASSWORD=never-print");\n', "utf8", ); const result = await validateApiInventory({ appSourcePath: fixture.appSourcePath, removedWebApiRoutesRoot: fixture.removedWebApiRoutesRoot }); const output = renderApiInventoryGuardResult(result); expect(output).toMatch(/GET \/v1\/auth\/me \[hono\] .+app\.ts:2:1/); expect(output).not.toContain("PASSWORD=never-print"); expect(output).not.toContain("handler"); }); });