summaryrefslogtreecommitdiff
path: root/cgithub/src/app.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cgithub/src/app.test.ts')
-rw-r--r--cgithub/src/app.test.ts483
1 files changed, 483 insertions, 0 deletions
diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts
new file mode 100644
index 0000000..450c1cc
--- /dev/null
+++ b/cgithub/src/app.test.ts
@@ -0,0 +1,483 @@
+import assert from "node:assert";
+import path from "node:path";
+import { describe, it, type TestContext } from "node:test";
+import { Eta } from "eta";
+import { createApp } from "./app.ts";
+
+// The routes exercised here redirect without scraping, so no network is needed.
+const eta = new Eta({ views: path.join(import.meta.dirname, "..", "views") });
+const app = createApp(eta);
+
+describe("branch picker", () => {
+ const repo = { owner: "actions", name: "deploy-pages" };
+ const refs = [{ name: "main", isDefault: true }, { name: "v4" }];
+
+ it("should keep the current view and path", () => {
+ const blob = eta.render("_refs.eta", {
+ repo,
+ refs,
+ type: "branches",
+ view: "blob",
+ path: "src/main.ts",
+ });
+ const tree = eta.render("_refs.eta", {
+ repo,
+ refs,
+ type: "branches",
+ view: "tree",
+ path: "src",
+ });
+
+ assert.match(blob, /href="\/actions\/deploy-pages\/blob\/main\/src\/main.ts"/);
+ assert.match(blob, /href="\/actions\/deploy-pages\/blob\/v4\/src\/main.ts"/);
+ assert.match(tree, /href="\/actions\/deploy-pages\/tree\/v4\/src"/);
+ });
+
+ it("should link to the root tree without a path", () => {
+ const html = eta.render("_refs.eta", { repo, refs, type: "branches", view: "tree", path: "" });
+
+ assert.match(html, /href="\/actions\/deploy-pages\/tree\/main"/);
+ });
+
+ it("should not build ref links for views it does not serve", async () => {
+ const res = await app.request("http://cgithub.example/api/a/b/refs/branches/raw/README.md");
+
+ assert.strictEqual(res.status, 404);
+ });
+
+ it("should tell the refs endpoint where the picker was opened", () => {
+ const nav = { repo, branch: "main", path: "src" };
+
+ const blob = eta.render("_nav.eta", { ...nav, view: "blob", path: "src/main.ts" });
+ assert.match(blob, /data-view="blob" data-path="src\/main.ts"/);
+ assert.match(
+ eta.render("_nav.eta", { ...nav, view: "tree" }),
+ /data-view="tree" data-path="src"/,
+ );
+ assert.doesNotMatch(eta.render("_nav.eta", { ...nav, view: "tree", path: "" }), /data-path=/);
+ });
+});
+
+describe("sidebar fragment", () => {
+ const repo = { owner: "actions", name: "deploy-pages" };
+ const languages = [{ name: "JavaScript", percentage: 100, color: "#f1e05a" }];
+ const latestRelease = { tag: "v5.0.0", publishedAt: "2026-03-25T16:59:14Z" };
+
+ // The release replaces the empty element it names, beside the release count.
+ it("should render the latest release and the languages", () => {
+ const html = eta.render("_sidebar.eta", { repo, languages, latestRelease });
+
+ assert.match(html, /<span data-target="latest-release">\(<a href/);
+ assert.match(html, /href="\/actions\/deploy-pages\/releases\/tag\/v5.0.0"/);
+ assert.match(html, /<time class="timestamp" title="2026-03-25T16:59:14Z">2026-03-25<\/time>/);
+ assert.match(html, /<div class="languages box">/);
+ });
+
+ it("should name a spot the repo page holds open", () => {
+ const html = eta.render("repo.eta", {
+ repo,
+ branch: "main",
+ path: "/",
+ items: [],
+ info: { description: null, website: null, stars: null, forks: null, numReleases: 38 },
+ });
+
+ assert.match(html, /<span id="latest-release"><\/span>/);
+ });
+
+ it("should render nothing when the repository has neither", () => {
+ const html = eta.render("_sidebar.eta", { repo, languages: [], latestRelease: null });
+
+ assert.strictEqual(html.trim(), "");
+ });
+});
+
+describe("blob view", () => {
+ const blob = {
+ repo: { owner: "justrajdeep", name: "fonts" },
+ branch: "master",
+ path: "Times New Roman.ttf",
+ language: null,
+ size: "815 KB",
+ image: false,
+ renderFileType: null,
+ textLines: null,
+ htmlLines: null,
+ htmlContent: null,
+ };
+
+ it("should embed a PDF", () => {
+ const html = eta.render("blob.eta", {
+ ...blob,
+ path: "test/pdfs/basicapi.pdf",
+ renderFileType: "pdf",
+ });
+
+ assert.match(html, /<object[^>]*type="application\/pdf"/);
+ assert.match(html, /data="\/justrajdeep\/fonts\/embed\/pdf\/master\/test\/pdfs\/basicapi.pdf"/);
+ });
+
+ it("should not embed a file type it has no viewer for", () => {
+ const html = eta.render("blob.eta", { ...blob, renderFileType: "stl" });
+
+ assert.doesNotMatch(html, /<object/);
+ assert.match(html, /This file cannot be displayed\./);
+ });
+
+ it("should give each syntax-highlighted line an anchor", () => {
+ const html = eta.render("blob.eta", {
+ ...blob,
+ path: "src/app.ts",
+ htmlLines: ['<span class="pl-k">const</span> x;', "x++;"],
+ });
+
+ assert.match(
+ html,
+ /<code><span id="L1"><span class="pl-k">const<\/span> x;<\/span>\n<span id="L2">x\+\+;<\/span><\/code>/,
+ );
+ });
+
+ it("should give each plain-text line an anchor", () => {
+ const html = eta.render("blob.eta", {
+ ...blob,
+ path: "notes.txt",
+ textLines: ["a < b", "c"],
+ });
+
+ assert.match(html, /<code><span id="L1">a &lt; b<\/span>\n<span id="L2">c<\/span><\/code>/);
+ });
+
+ it("should offer the raw file when there is nothing to display", () => {
+ const html = eta.render("blob.eta", blob);
+
+ assert.match(html, /This file cannot be displayed\./);
+ assert.match(html, /href="\/justrajdeep\/fonts\/raw\/master\/Times New Roman.ttf">View raw/);
+ });
+});
+
+describe("commit history", () => {
+ const repo = { owner: "actions", name: "deploy-pages" };
+ const oid = "b39c421b98f49d83ae50ec502c3ddfc3bf28f2c6";
+ const commitGroups = [
+ {
+ title: "Apr 18, 2023",
+ commits: [
+ { oid, shortMessage: "Remove deprecated input parameters", authors: [{ login: "jmg" }] },
+ ],
+ },
+ ];
+ const history = { repo, branch: "main", path: "src/index.js", commitGroups };
+
+ // A commits payload never says whether its path is a file or a directory, so
+ // every link goes to the tree and GitHub redirects a file's to its blob.
+ it("should browse a file's history at each of its commits", () => {
+ const html = eta.render("commits.eta", history);
+
+ assert.match(html, new RegExp(`href="/actions/deploy-pages/blob/${oid}/src/index.js">Browse`));
+ });
+
+ it("should browse a directory's history the same way", () => {
+ const html = eta.render("commits.eta", { ...history, path: "src" });
+
+ assert.match(html, new RegExp(`href="/actions/deploy-pages/blob/${oid}/src">Browse`));
+ });
+
+ it("should browse the whole repository when the history has no path", () => {
+ const html = eta.render("commits.eta", { ...history, path: "" });
+
+ assert.match(html, new RegExp(`href="/actions/deploy-pages/blob/${oid}">Browse`));
+ });
+
+ // Walking up from a history stays in the history, and its last crumb is the
+ // page you are already on, just as a blob or a tree ends on itself.
+ it("should walk up through the history of each parent directory", () => {
+ const html = eta.render("commits.eta", { ...history, path: "src/lib/index.js" });
+
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main">\(top\)</);
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main\/src">src</);
+ assert.match(
+ html,
+ /href="\/actions\/deploy-pages\/commits\/main\/src\/lib">lib<\/a>\/index\.js/,
+ );
+ });
+
+ it("should say so when a path has no history", () => {
+ const html = eta.render("commits.eta", { ...history, commitGroups: [] });
+
+ assert.match(html, /No commits found\./);
+ });
+
+ it("should link a blob to its own history", () => {
+ const html = eta.render("blob.eta", { ...history, textLines: ["x"] });
+
+ assert.match(html, /href="\/actions\/deploy-pages\/commits\/main\/src\/index.js">History/);
+ });
+});
+
+describe("embedded files", () => {
+ function stubGitHub(t: TestContext, body: string, contentType: string, status = 200) {
+ t.mock.method(
+ globalThis,
+ "fetch",
+ async () => new Response(body, { status, headers: { "Content-Type": contentType } }),
+ );
+ }
+
+ it("should retype a PDF GitHub sends as a download", async (t) => {
+ stubGitHub(t, "%PDF-1.6", "application/octet-stream");
+
+ const res = await app.request("http://cgithub.example/mozilla/pdf.js/embed/pdf/master/doc.pdf");
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.headers.get("Content-Type"), "application/pdf");
+ assert.strictEqual(await res.text(), "%PDF-1.6");
+ });
+
+ it("should pass on a type GitHub already got right", async (t) => {
+ stubGitHub(t, "%PDF-1.6", "application/pdf");
+
+ const res = await app.request("http://cgithub.example/mozilla/pdf.js/embed/pdf/master/doc.pdf");
+
+ assert.strictEqual(res.headers.get("Content-Type"), "application/pdf");
+ });
+
+ it("should not serve a file type it has no viewer for", async (t) => {
+ stubGitHub(t, "<script>", "text/html");
+
+ const res = await app.request("http://cgithub.example/mozilla/pdf.js/embed/html/master/x.html");
+
+ assert.strictEqual(res.status, 404);
+ });
+
+ it("should pass on the status of a file GitHub does not have", async (t) => {
+ stubGitHub(t, "404: Not Found", "text/plain", 404);
+
+ const res = await app.request(
+ "http://cgithub.example/mozilla/pdf.js/embed/pdf/master/nope.pdf",
+ );
+
+ assert.strictEqual(res.status, 404);
+ });
+});
+
+describe("wiki", () => {
+ const repo = { owner: "ariya", name: "phantomjs" };
+ const page = {
+ repo,
+ title: "Home",
+ updatedBy: "pixiuPL",
+ updatedAt: "2018-02-08T18:57:56Z",
+ bodyHtml: "<p>New to <strong>PhantomJS</strong>?</p>",
+ sidebarHtml: null,
+ footerHtml: null,
+ pages: [
+ { name: "Home", href: "/ariya/phantomjs/wiki" },
+ { name: "API Reference WebPage", href: "/ariya/phantomjs/wiki/API-Reference-WebPage" },
+ ],
+ };
+
+ it("should render the page beside who last edited it", () => {
+ const html = eta.render("wiki.eta", page);
+
+ assert.match(html, /<p>New to <strong>PhantomJS<\/strong>\?<\/p>/);
+ assert.match(html, /pixiuPL edited this page/);
+ assert.match(html, /<time title="2018-02-08T18:57:56Z">2018-02-08<\/time>/);
+ });
+
+ // The _Sidebar and _Footer pages a wiki can define, which GitHub renders
+ // alongside every page rather than as pages of their own.
+ it("should render the sidebar and footer only when the wiki has them", () => {
+ // The page's own content is the last thing in its box, and the box is the
+ // only one on the page.
+ const bare = eta.render("wiki.eta", page);
+ assert.match(bare, /New to <strong>PhantomJS<\/strong>\?<\/p>\s*<\/div>/);
+ assert.doesNotMatch(bare, /Sidebar/);
+
+ const html = eta.render("wiki.eta", {
+ ...page,
+ sidebarHtml: "<ul><li>nav</li></ul>",
+ footerHtml: "<p>fine print</p>",
+ });
+ assert.match(html, /<footer>\s*<p>fine print<\/p>/);
+ assert.match(html, /<header><span>Sidebar<\/span><\/header>\s*<ul><li>nav<\/li><\/ul>/);
+ });
+
+ it("should link every page of the wiki", () => {
+ const html = eta.render("wiki.eta", page);
+
+ assert.match(html, /<summary>2 pages<\/summary>/);
+ assert.match(
+ html,
+ /href="\/ariya\/phantomjs\/wiki\/API-Reference-WebPage">API Reference WebPage/,
+ );
+ });
+
+ it("should list the pages with when each was last updated", () => {
+ const html = eta.render("wikiPages.eta", {
+ repo,
+ pages: [
+ {
+ name: "Home",
+ href: "/ariya/phantomjs/wiki",
+ updatedAt: "2018-02-08T18:57:56Z",
+ },
+ ],
+ });
+
+ assert.match(html, /href="\/ariya\/phantomjs\/wiki">Home/);
+ assert.match(html, /<time title="2018-02-08T18:57:56Z">2018-02-08<\/time>/);
+ });
+
+ // The editor and the timestamp are scraped independently, so a page missing
+ // one of them should still show the other.
+ it("should render each part of the header meta it has", () => {
+ assert.doesNotMatch(eta.render("wiki.eta", { ...page, updatedAt: null }), /<time/);
+ assert.match(eta.render("wiki.eta", { ...page, updatedAt: null }), /pixiuPL edited this page/);
+ assert.doesNotMatch(eta.render("wiki.eta", { ...page, updatedBy: null }), /edited this page/);
+ });
+
+ // GitHub reserves underscore-prefixed names for its own actions, so a page
+ // can never be one; the rest of them are for signed-in users only.
+ it("should not scrape a reserved wiki name as a page", async () => {
+ const reserved = ["/ariya/phantomjs/wiki/_new", "/ariya/phantomjs/wiki/Home/_edit"];
+ for (const path of reserved) {
+ const res = await app.request(`http://cgithub.example${path}`);
+
+ assert.strictEqual(res.status, 200);
+ assert.match(await res.text(), new RegExp(`url=https://github.com${path}"`));
+ }
+ });
+
+ it("should only read a revision off a path that names one", async () => {
+ const res = await app.request("http://cgithub.example/ariya/phantomjs/wiki/Home/latest");
+
+ assert.match(
+ await res.text(),
+ /url=https:\/\/github.com\/ariya\/phantomjs\/wiki\/Home\/latest"/,
+ );
+ });
+});
+
+describe("global search", () => {
+ it("should keep the rest of the query around the qualifier", async () => {
+ const res = await app.request(
+ "http://cgithub.example/search?q=" +
+ encodeURIComponent("is:issue repo:actions/deploy-pages label:bug") +
+ "&type=issues",
+ );
+
+ assert.strictEqual(
+ res.headers.get("location"),
+ "/actions/deploy-pages/search?q=is%3Aissue+label%3Abug&type=issues",
+ );
+ });
+
+ it("should redirect searches it cannot scope to a repository", async () => {
+ for (const url of [
+ "http://cgithub.example/search?q=dms&type=code",
+ // An owner-wide search is not a single repository.
+ "http://cgithub.example/search?q=" +
+ encodeURIComponent("org:nix-community dms") +
+ "&type=code",
+ // Without a type there is no search to hand over.
+ "http://cgithub.example/search?q=" + encodeURIComponent("repo:actions/deploy-pages dms"),
+ ]) {
+ const res = await app.request(url);
+
+ assert.strictEqual(res.status, 200, url);
+ assert.match(await res.text(), /url=https:\/\/github.com\/search\?/);
+ }
+ });
+});
+
+describe("redirects to GitHub", () => {
+ // An HTTP redirect would be caught by the extension's declarativeNetRequest
+ // rules and bounced straight back here.
+ it("should redirect unknown paths with a meta refresh", async () => {
+ const res = await app.request("http://cgithub.example/a/b/c?x=1");
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.headers.get("location"), null);
+
+ const body = await res.text();
+ assert.match(body, /<meta name="referrer" content="no-referrer">/);
+ assert.match(
+ body,
+ /<meta http-equiv="refresh" content="1; url=https:\/\/github.com\/a\/b\/c\?x=1">/,
+ );
+ assert.doesNotMatch(body, /<script/);
+ });
+
+ // Same-origin requests are mostly subresources, which render an HTML page as
+ // a broken image rather than following its refresh.
+ it("should redirect over HTTP when the referer is one of our own pages", async () => {
+ const res = await app.request("http://cgithub.example/a/b/c?x=1", {
+ headers: { Referer: "http://cgithub.example/a/b" },
+ });
+
+ assert.strictEqual(res.status, 302);
+ assert.strictEqual(res.headers.get("location"), "https://github.com/a/b/c?x=1");
+ assert.strictEqual(res.headers.get("Referrer-Policy"), "no-referrer");
+ });
+
+ it("should use a meta refresh when the referer is another site", async () => {
+ const res = await app.request("http://cgithub.example/a/b/c?x=1", {
+ headers: { Referer: "https://github.example/a/b" },
+ });
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.headers.get("location"), null);
+ });
+
+ it("should redirect unhandled search types", async () => {
+ const res = await app.request(
+ "http://cgithub.example/actions/deploy-pages/search?q=x&type=code",
+ );
+
+ assert.strictEqual(res.status, 200);
+ assert.match(
+ await res.text(),
+ /https:\/\/github.com\/actions\/deploy-pages\/search\?q=x&amp;type=code/,
+ );
+ });
+
+ it("should redirect notifications without scraping them as a profile", async () => {
+ const res = await app.request("http://cgithub.example/notifications?query=is%3Aunread");
+
+ assert.strictEqual(res.status, 200);
+ assert.match(await res.text(), /url=https:\/\/github.com\/notifications\?query=is%3Aunread"/);
+ });
+
+ it("should redirect raw file requests to raw.githubusercontent.com", async () => {
+ const res = await app.request("http://cgithub.example/actions/deploy-pages/raw/main/README.md");
+
+ assert.strictEqual(res.status, 200);
+ assert.match(
+ await res.text(),
+ /url=https:\/\/raw.githubusercontent.com\/actions\/deploy-pages\/main\/README.md"/,
+ );
+ });
+});
+
+describe("error page", () => {
+ it("should render inside the page shell", () => {
+ const html = eta.render("error.eta", {
+ title: "Not Found",
+ message: "GitHub responded with HTTP 404 Not Found",
+ buildCommit: "v0-g1234567",
+ githubUrl: "https://github.com/actions/deploy-pages",
+ });
+
+ assert.match(html, /<title>Not Found<\/title>/);
+ assert.match(html, /GitHub responded with HTTP 404 Not Found/);
+ assert.match(html, /<footer>Generated by cgithub commit <code>v0-g1234567<\/code>/);
+ assert.match(html, /href="https:\/\/github.com\/actions\/deploy-pages"/);
+ });
+
+ // The title falls back to the message for errors that carry no status text.
+ it("should title itself with the message when it has no title", () => {
+ const html = eta.render("error.eta", { message: "Error: boom" });
+
+ assert.match(html, /<title>Error: boom<\/title>/);
+ });
+});