Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

api-error-format.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

api-error-format.test.tsBlame63 lines · 1 contributor
4abf6f1ccantynz-alt1/**
2 * Machine surfaces must fail as JSON, not HTML.
3 *
4 * The global app.notFound / app.onError handlers render the shared HTML
5 * error pages. Before this was gated, an unmatched /api/* path returned a
6 * full HTML document with `content-type: text/html`, so REST, MCP and OCI
7 * registry clients hit a JSON.parse error instead of a readable envelope.
8 * The production-readiness gate (scripts/production-readiness.mjs, "api-json")
9 * checks exactly this against the live site.
10 */
11
12import { describe, expect, it } from "bun:test";
13import app from "../app";
14
15async function get(path: string, headers: Record<string, string> = {}) {
16 return app.fetch(new Request(`http://localhost${path}`, { headers }));
17}
18
19describe("unmatched machine-surface paths return JSON", () => {
20 // `/v2/*` (OCI registry) and `/npm/*` are deliberately absent: their own
21 // routers claim the whole subtree and emit their own spec-shaped JSON
22 // errors, so the app-level fallback never fires for them. They stay in
23 // JSON_ROOTS purely as a safety net.
24 const jsonPaths = [
25 "/api/definitely-not-real",
26 "/api/v2/definitely-not-real",
27 "/mcp/definitely-not-real",
28 "/graphql/definitely-not-real",
29 "/scim/definitely-not-real",
30 "/.well-known/definitely-not-real",
31 ];
32
33 for (const path of jsonPaths) {
34 it(`${path} → 404 application/json`, async () => {
35 const res = await get(path);
36 expect(res.status).toBe(404);
37 expect(res.headers.get("content-type")).toContain("application/json");
38 const body = await res.json();
39 expect(body.error).toBe("Not found");
40 expect(body.path).toBe(path);
41 });
42 }
43
44 it("a deep unmatched API path is JSON too", async () => {
45 const res = await get("/api/v2/repos/nope/nope/nope/nope");
46 expect(res.status).toBe(404);
47 expect(res.headers.get("content-type")).toContain("application/json");
48 });
49});
50
51describe("HTML surfaces are unaffected", () => {
52 it("a path that merely starts with an API root's letters stays HTML", async () => {
53 // "/mcpanything" must NOT be captured by the "/mcp" machine surface —
54 // it is a one-segment path and belongs to the /:owner profile route.
55 const res = await get("/mcpanything", { accept: "text/html" });
56 expect(res.headers.get("content-type")).toContain("text/html");
57 });
58
59 it("a normal repo path still renders HTML", async () => {
60 const res = await get("/someowner/somerepo", { accept: "text/html" });
61 expect(res.headers.get("content-type")).toContain("text/html");
62 });
63});