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.
| fc623bf | 1 | /** |
| 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 | ||
| 15 | import { describe, expect, it } from "bun:test"; | |
| 16 | import { parseIdNumber, parseIdUuid } from "../lib/route-params"; | |
| 17 | ||
| 18 | describe("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 | ||
| 45 | describe("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 | ||
| 66 | describe("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 | }); |