Public: anyone can read this; the crew can change it.
A test pins that ref links never smuggle a path: `pathCrumbs` and `refMenu` encode refs and escape names #6771
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6750-a-test-pins-that-ref-links-never-smuggle → main 1 commit fork
2 files
changed +55
−1
src/web/pages/refs.ts +4 −1
| @@ -129,7 +129,10 @@ export function pathCrumbs( | ||
| 129 | 129 | { label: name, href: `${base}/tree/${encoded}` }, |
| 130 | 130 | ]; |
| 131 | 131 | parts.forEach((part, i) => { |
| 132 | const walked = parts.slice(0, i + 1).join("/"); | |
| 132 | const walked = parts | |
| 133 | .slice(0, i + 1) | |
| 134 | .map(encodeURIComponent) | |
| 135 | .join("/"); | |
| 133 | 136 | const last = i === parts.length - 1; |
| 134 | 137 | items.push( |
| 135 | 138 | last && leafIsFile |
test/web-refs-links.test.ts added +51
| @@ -0,0 +1,51 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { pathCrumbs, refMenu } from "../src/web/pages/refs.ts"; | |
| 3 | ||
| 4 | const hrefs = (h: string) => [...h.matchAll(/href="([^"]*)"/g)].map((m) => m[1]).filter((h) => !h!.startsWith("#i-")); | |
| 5 | ||
| 6 | test("pathCrumbs links each walked directory under one encoded ref segment; the leaf is text", () => { | |
| 7 | const out = pathCrumbs("/r/a", "repo", "feature/a", "src/x.ts", true).toString(); | |
| 8 | expect(hrefs(out)).toEqual(["/r/a/tree/feature%2Fa", "/r/a/tree/feature%2Fa/src"]); | |
| 9 | expect(out).toMatch(/>\s*x\.ts\s*<\/span/); | |
| 10 | expect(out).not.toContain("x.ts</a>"); | |
| 11 | }); | |
| 12 | ||
| 13 | test("pathCrumbs with a directory leaf still treats the last crumb as current text", () => { | |
| 14 | const out = pathCrumbs("/r", "repo", "main", "src/lib", false).toString(); | |
| 15 | expect(hrefs(out)).toEqual(["/r/tree/main", "/r/tree/main/src"]); | |
| 16 | }); | |
| 17 | ||
| 18 | test("pathCrumbs encodes hostile refs and segments in hrefs and escapes them in text", () => { | |
| 19 | const out = pathCrumbs("/r", "<n>", `a"<?#`, `d<"?#/e?#/f`, true).toString(); | |
| 20 | expect(hrefs(out)).toEqual([ | |
| 21 | "/r/tree/a%22%3C%3F%23", | |
| 22 | "/r/tree/a%22%3C%3F%23/d%3C%22%3F%23", | |
| 23 | "/r/tree/a%22%3C%3F%23/d%3C%22%3F%23/e%3F%23", | |
| 24 | ]); | |
| 25 | expect(out).toContain("<n>"); | |
| 26 | expect(out).toContain("d<"?#"); | |
| 27 | expect(out).not.toMatch(/<n>|d<"/); | |
| 28 | }); | |
| 29 | ||
| 30 | test("pathCrumbs with an empty path is just the repository crumb", () => { | |
| 31 | const out = pathCrumbs("/r", "repo", "main", "", false).toString(); | |
| 32 | expect(out).toContain("repo"); | |
| 33 | expect(out.match(/class="sep"/g)).toBeNull(); | |
| 34 | expect(out.match(/<a /g)).toBeNull(); | |
| 35 | }); | |
| 36 | ||
| 37 | test("refMenu encodes each href, escapes names and marks the current ref", () => { | |
| 38 | const refs = { branches: ["main", "feature/a", `x"<?#`], tags: ["v1/2"] }; | |
| 39 | const out = refMenu(refs, "feature/a", (r) => `/tree/${encodeURIComponent(r)}`).toString(); | |
| 40 | expect(hrefs(out)).toEqual([ | |
| 41 | "/tree/main", | |
| 42 | "/tree/feature%2Fa", | |
| 43 | "/tree/x%22%3C%3F%23", | |
| 44 | "/tree/v1%2F2", | |
| 45 | ]); | |
| 46 | expect(out).toContain("x"<?#"); | |
| 47 | expect(out).not.toContain(`x"<`); | |
| 48 | expect(out.match(/aria-current/g)?.length).toBeGreaterThanOrEqual(1); | |
| 49 | expect(out).toContain("Branches"); | |
| 50 | expect(out).toContain("Tags"); | |
| 51 | }); |
new above
offline