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([]); }); });