Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/check-api-inventory.test.ts
T

130 lines
5.7 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 { createRequire } from "node:module";
import { promisify } from "node:util";
import { describe, expect, test } from "vitest";
import { renderApiInventoryGuardResult, validateApiInventory } from "./check-api-inventory.js";
const execFileAsync = promisify(execFile);
const tsxCli = createRequire(import.meta.url).resolve("tsx/cli");
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("permanently blocks the removed AD-26 AI upload route", async () => {
const fixture = await makeFixture();
await writeFile(
fixture.appSourcePath,
'import { Hono } from "hono";\nconst app = new Hono();\napp.post("/v1/ai/upload", () => "must-stay-removed");\n',
"utf8",
);
const result = await validateApiInventory({ appSourcePath: fixture.appSourcePath, removedWebApiRoutesRoot: fixture.removedWebApiRoutesRoot });
expect(result.ok).toBe(false);
expect(result.violations).toContainEqual(
expect.objectContaining({
routeKey: "POST /v1/ai/upload",
surface: "hono",
}),
);
});
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(process.execPath, [tsxCli, "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");
});
});