Files
HiveGrid-Wireframes/hivegrid-cloud/tools/api-inventory/scan-api-routes.test.ts
T

181 lines
5.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
assertNoDuplicateApiRoutes,
formatApiRouteKeys,
scanApiRoutesFromFile,
scanApiRoutesFromSource,
type ApiRouteKey,
} from "./scan-api-routes";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const apiAppPath = path.join(repoRoot, "apps/api/src/app.ts");
const expectedCurrentRouteKeys: ApiRouteKey[] = [
"GET /healthz",
"GET /v1/health",
"GET /readyz",
"GET /v1/openapi.json",
"GET /v1/docs",
"GET /v1/session/current-user",
"POST /v1/auth/login",
"POST /v1/auth/refresh-token",
"POST /v1/auth/logout",
"POST /v1/api-keys/validate",
"GET /v1/accounts",
"GET /v1/accounts/:id",
"POST /v1/analytics/events",
"POST /v1/analytics/page-views",
"GET /v1/analytics/public",
"GET /v1/analytics/admin",
"GET /v1/notifications/stream",
"GET /v1/workspaces/:tenantId/auth/providers",
"POST /v1/workspaces/:tenantId/context/revalidate",
"GET /v1/workspaces/:tenantId/invites",
"POST /v1/workspaces/:tenantId/invites",
"POST /v1/workspaces/:tenantId/invites/:inviteId/accept",
"POST /v1/workspaces/:tenantId/invites/:inviteId/revoke",
"POST /v1/ai/upload",
"POST /v1/support/inbound-email",
"POST /v1/email-sequences/mailjet/webhooks",
"POST /v1/events/webhooks/attempts/:id",
"POST /v1/maintenance/export-retention/enqueue",
"GET /v1/exports/:exportJobId/download-url",
"GET /v1/exports/:exportJobId/progress",
"GET /v1/entities/:entity/rows",
"POST /v1/entities/:entity/rows",
"GET /v1/entities/:entity/rows/:rowId",
"PATCH /v1/entities/:entity/rows/:rowId",
"DELETE /v1/entities/:entity/rows/:rowId",
"GET /v1/entities/:entity",
"POST /v1/relationships",
"GET /v1/relationships/:id",
"DELETE /v1/relationships/:id",
"POST /v1/devices/register",
"POST /v1/devices/revoke",
"GET /status",
"GET /metrics",
"GET /v1/usage",
"POST /v1/billing/stripe/webhooks",
];
describe("scanApiRoutesFromSource", () => {
it("returns the complete current route-key set from apps/api/src/app.ts and registered route modules", async () => {
const result = await scanApiRoutesFromFile(apiAppPath);
expect(result.duplicateRoutes).toEqual([]);
expect(result.unresolvedMountedRouters).toEqual([]);
expect(formatApiRouteKeys(result.routes)).toEqual(expectedCurrentRouteKeys);
});
it("finds multiline Hono app route registrations", () => {
const result = scanApiRoutesFromSource(`
app.post(
"/v1/multiline",
multilineHandler
);
app.delete(
'/v1/multiline/:id',
deleteHandler
);
app.put(
"/v1/multiline-put",
putHandler
);
app.options(
"/v1/multiline-options",
optionsHandler
);
app.all(
"/api/v1/*",
redirectHandler
);
`);
expect(formatApiRouteKeys(result.routes)).toEqual([
"POST /v1/multiline",
"DELETE /v1/multiline/:id",
"PUT /v1/multiline-put",
"OPTIONS /v1/multiline-options",
"ALL /api/v1/*",
]);
});
it("finds simple Hono subrouter registrations mounted through app.route", () => {
const result = scanApiRoutesFromSource(`
const workspaceRoutes = new Hono();
workspaceRoutes.get("/workspaces/:tenantId/invites", listInvites);
workspaceRoutes.post(
"/workspaces/:tenantId/invites",
createInvite
);
const ignoredRoutes = new Hono();
ignoredRoutes.get("/not-mounted", handler);
app.route("/v1", workspaceRoutes);
`);
expect(formatApiRouteKeys(result.routes)).toEqual([
"GET /v1/workspaces/:tenantId/invites",
"POST /v1/workspaces/:tenantId/invites",
]);
});
it("finds Hono app.on route registrations", () => {
const result = scanApiRoutesFromSource(`
app.on("GET", "/v1/on-single", handler);
app.on(["POST", "DELETE"], "/v1/on-array/:id", handler);
`);
expect(formatApiRouteKeys(result.routes)).toEqual(["GET /v1/on-single", "POST /v1/on-array/:id", "DELETE /v1/on-array/:id"]);
});
it("reports mounted routers that cannot be resolved from the scanned source", () => {
const result = scanApiRoutesFromSource(`
import { userRoutes } from "./users";
app.route("/v1/users", userRoutes);
`);
expect(result.routes).toEqual([]);
expect(result.unresolvedMountedRouters).toEqual([
{
receiver: "userRoutes",
prefix: "/v1/users",
location: { line: 3, column: 7 },
},
]);
});
it("reports duplicate route keys without collapsing them through a Set", () => {
const result = scanApiRoutesFromSource(`
app.get("/v1/dup", firstHandler);
app.get("/v1/dup", secondHandler);
`);
expect(formatApiRouteKeys(result.routes)).toEqual(["GET /v1/dup", "GET /v1/dup"]);
expect(result.duplicateRoutes).toEqual([
{
key: "GET /v1/dup",
locations: [
{ line: 2, column: 7 },
{ line: 3, column: 7 },
],
},
]);
expect(() => assertNoDuplicateApiRoutes(result)).toThrow(/Duplicate API route keys found: GET \/v1\/dup/);
});
it("ignores non-Hono-app source text", () => {
const result = scanApiRoutesFromSource(`
something.get("/v1/not-a-route", handler);
router.post("/v1/not-a-route", handler);
const text = 'app.delete("/v1/not-a-real-route")';
const templateText = \`app.post("/v1/not-a-real-route")\`;
const objectText = { value: "app.patch('/v1/not-a-real-route')" };
`);
expect(result.routes).toEqual([]);
expect(result.duplicateRoutes).toEqual([]);
});
});