Skip to content
96 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 PDF_POLICY,
6 USERCONTENT_POLICY,
7 imageSource,
8 isCommit,
9 parseRawPath,
10 rawHeaders,
11 rawPath,
12 signRaw,
13 usercontentPath,
14 verifyRaw,
15} from "./usercontent.ts";
16
17const FILE = { owner: "acme", repo: "web", ref: "feat/new", path: "docs/a b.png" };
18
19test("a raw file's path keeps a ref with slashes in one segment", () => {
20 const path = rawPath(FILE);
21 assert.equal(path, "/acme/web/raw/feat%2Fnew/docs/a%20b.png");
22 assert.deepEqual(parseRawPath(path), FILE);
23});
24
25test("paths that are not a file, or climb out of the repository, are refused", () => {
26 for (const path of ["/acme/web/raw/main", "/acme/web/blob/main/a.png", "/acme/web/raw/main/../x", "/acme/web/raw/main/a//b", "/acme/web/raw/main/%E0%A4%A"]) {
27 assert.equal(parseRawPath(path), null, path);
28 }
29});
30
31test("usercontent is its own host, or a path on the site", () => {
32 const hosted = "https://g1tusercontent.com";
33 assert.equal(usercontentPath(new URL("https://g1tusercontent.com/acme/web/raw/main/a.png"), hosted), "/acme/web/raw/main/a.png");
34 assert.equal(usercontentPath(new URL("https://g1t.sh/acme/web/raw/main/a.png"), hosted), null);
35 const own = "https://git.example.com/-/usercontent";
36 assert.equal(usercontentPath(new URL("http://localhost:8787/-/usercontent/avatars/ab"), own), "/avatars/ab");
37 assert.equal(usercontentPath(new URL("http://localhost:8787/-/usercontentx"), own), null);
38 assert.equal(usercontentPath(new URL("http://localhost:8787/acme/web"), own), null);
39});
40
41test("a token is good for its file alone, until it ends", async () => {
42 const now = Date.UTC(2026, 9, 8, 12, 30);
43 const token = await signRaw("secret", FILE, "repo_1", now);
44 assert.match(token, /^\d+\.repo_1\.[A-Za-z0-9_-]{43}$/);
45 // The same within the hour, so a page's addresses are kept by the browser.
46 assert.equal(await signRaw("secret", FILE, "repo_1", now + 20 * 60_000), token);
47 assert.equal(await verifyRaw("secret", FILE, token, now), "repo_1");
48 assert.equal(await verifyRaw("secret", { ...FILE, owner: "ACME" }, token, now), "repo_1");
49 assert.equal(await verifyRaw("secret", { ...FILE, path: "docs/other.png" }, token, now), null);
50 assert.equal(await verifyRaw("secret", { ...FILE, repo: "api" }, token, now), null);
51 assert.equal(await verifyRaw("secret", { ...FILE, ref: "main" }, token, now), null);
52 assert.equal(await verifyRaw("other", FILE, token, now), null);
53 assert.equal(await verifyRaw("secret", FILE, token.replace("repo_1", "repo_2"), now), null);
54 assert.equal(await verifyRaw("secret", FILE, token, now + 2 * 3600_000), null);
55 assert.equal(await verifyRaw("secret", FILE, "nonsense", now), null);
56});
57
58test("files are served as data that cannot run", () => {
59 const text = new TextEncoder().encode("<script>alert(1)</script>");
60 for (const name of ["index.html", "page.xhtml", "data.xml", "app.js", "README.md"]) {
61 const headers = rawHeaders(name, text);
62 assert.equal(headers.get("content-type"), "text/plain; charset=utf-8", name);
63 assert.equal(headers.get("x-content-type-options"), "nosniff");
64 assert.equal(headers.get("content-security-policy"), USERCONTENT_POLICY);
65 }
66 assert.equal(rawHeaders("logo.png", new Uint8Array([137, 80, 78, 71])).get("content-type"), "image/png");
67 // An SVG shows as an image; opened on its own, its scripts are sandboxed.
68 const svg = rawHeaders("logo.svg", text);
69 assert.equal(svg.get("content-type"), "image/svg+xml");
70 assert.match(svg.get("content-security-policy")!, /sandbox/);
71 assert.equal(rawHeaders("paper.pdf", new Uint8Array([37, 80])).get("content-security-policy"), PDF_POLICY);
72 const binary = rawHeaders("tool.bin", new Uint8Array([0, 1, 2]));
73 assert.equal(binary.get("content-type"), "application/octet-stream");
74 assert.match(binary.get("content-disposition")!, /^attachment; filename="tool.bin"/);
75});
76
77test("a README's relative pictures are the repository's files at the same commit", () => {
78 const root = "/acme/web/raw/abc123";
79 const docs = `${root}/docs`;
80 assert.equal(imageSource("logo.png", root), `${root}/logo.png`);
81 assert.equal(imageSource("./img/a b.png", docs), `${root}/docs/img/a%20b.png`);
82 assert.equal(imageSource("../logo.png?raw=true", docs), `${root}/logo.png`);
83 // From the repository's root, as people write them.
84 assert.equal(imageSource("/assets/x.svg", docs), `${root}/assets/x.svg`);
85 // Never out of the repository.
86 assert.equal(imageSource("../../../../other/repo/raw/main/x.png", docs), undefined);
87 // External pictures, and those with no repository, are as written.
88 assert.equal(imageSource("https://example.com/x.png", docs), "https://example.com/x.png");
89 assert.equal(imageSource("data:image/png;base64,AA", docs), "data:image/png;base64,AA");
90 assert.equal(imageSource("logo.png", undefined), "logo.png");
91});
92
93test("commits are full hashes", () => {
94 assert.equal(isCommit("a".repeat(40)), true);
95 assert.equal(isCommit("main"), false);
96});