109 lines
4.9 KiB
TypeScript
109 lines
4.9 KiB
TypeScript
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");
|
|
});
|
|
});
|