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

route-params.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.

route-params.test.tsBlame95 lines · 1 contributor
fc623bfccantynz-alt1/**
2 * Malformed URL ids must 404, not 500.
3 *
4 * Handlers passed raw path segments into database predicates.
5 * `parseInt("abc", 10)` is NaN, and NaN or a malformed UUID makes Postgres
6 * raise, which the global error handler turns into a 500. Live, before the
7 * fix: /:owner/:repo/issues/abc returned 500 while
8 * /:owner/:repo/issues/99999999 correctly returned 404 — so a typo or a stale
9 * link produced a server error rather than "not found".
10 *
11 * Found by the authz-matrix gate, which probes every repo-scoped route with a
12 * placeholder id; eight routes answered 5xx.
13 */
14
15import { describe, expect, it } from "bun:test";
16import { parseIdNumber, parseIdUuid } from "../lib/route-params";
17
18describe("parseIdNumber", () => {
19 it("accepts a positive integer", () => {
20 expect(parseIdNumber("1")).toBe(1);
21 expect(parseIdNumber("42")).toBe(42);
22 expect(parseIdNumber("99999999")).toBe(99999999);
23 });
24
25 for (const bad of ["abc", "", "0", "-1", "1.5", " 1", "1 ", "NaN", "Infinity"]) {
26 it(`rejects ${JSON.stringify(bad)}`, () => {
27 expect(parseIdNumber(bad)).toBeNull();
28 });
29 }
30
31 it("rejects numeric-prefixed junk that parseInt would truncate", () => {
32 // parseInt("12abc", 10) === 12, which would silently load the wrong row.
33 expect(parseIdNumber("12abc")).toBeNull();
34 });
35
36 it("rejects undefined", () => {
37 expect(parseIdNumber(undefined)).toBeNull();
38 });
39
40 it("rejects values beyond safe integer range", () => {
41 expect(parseIdNumber("999999999999999999999")).toBeNull();
42 });
43});
44
45describe("parseIdUuid", () => {
46 it("accepts a well-formed uuid in either case", () => {
47 const u = "df1c027c-a830-4c42-aafc-b3bc7155477c";
48 expect(parseIdUuid(u)).toBe(u);
49 expect(parseIdUuid(u.toUpperCase())).toBe(u.toUpperCase());
50 });
51
52 for (const bad of [
53 "abc",
54 "",
55 "df1c027c-a830-4c42-aafc",
56 "df1c027c_a830_4c42_aafc_b3bc7155477c",
57 "zzzzzzzz-a830-4c42-aafc-b3bc7155477c",
58 "df1c027c-a830-4c42-aafc-b3bc7155477cc",
59 ]) {
60 it(`rejects ${JSON.stringify(bad)}`, () => {
61 expect(parseIdUuid(bad)).toBeNull();
62 });
63 }
64});
65
66describe("the sentinel flows into existing not-found paths", () => {
67 it("callers use `?? -1`, which matches no row", () => {
68 // Deliberate: -1 can never equal a real issue/PR number, so each handler
69 // falls through the not-found branch it already had. No new branches, no
70 // behaviour change for valid input.
71 const { readFileSync } = require("fs") as typeof import("fs");
72 const issues = readFileSync("src/routes/issues.tsx", "utf8");
73 expect(issues).toContain("parseIdNumber(c.req.param(\"number\")) ?? -1");
74 expect(issues).not.toMatch(/parseInt\(c\.req\.param\("number"\), 10\)/);
75 });
76
77 it("no route still parses a number param with bare parseInt", () => {
78 const { readdirSync, readFileSync, statSync } = require("fs") as typeof import("fs");
79 const { join } = require("path") as typeof import("path");
80 const walk = (d: string): string[] =>
81 readdirSync(d).flatMap((e) => {
82 const p = join(d, e);
83 return statSync(p).isDirectory() ? walk(p) : [p];
84 });
85 const offenders: string[] = [];
86 for (const f of walk("src/routes")) {
87 if (!/\.tsx?$/.test(f)) continue;
88 const src = readFileSync(f, "utf8");
89 if (/parseInt\(c\.req\.param\("(number|prNumber|issueNumber)"\), 10\)/.test(src)) {
90 offenders.push(f.replace(/\\/g, "/"));
91 }
92 }
93 expect(offenders).toEqual([]);
94 });
95});