deploy-pages π<\/h1>/;
assert.match(data.htmlContent ?? "", firstLine);
});
it("should fetch code blob", async () => {
const data = await getGitHubBlob("actions", "deploy-pages", "main", ".gitattributes");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.branch, "main");
assert.strictEqual(data.path, ".gitattributes");
assert.strictEqual(data.size, "39 Bytes / 1 lines / 1 loc");
assert.strictEqual(data.language, "Git Attributes");
assert.strictEqual(data.image, false);
assert.deepStrictEqual(data.textLines, ["dist/** -diff linguist-generated=true "]);
assert.deepStrictEqual(data.htmlLines, [
'\u003cspan class="pl-e"\u003edist\u003c/span\u003e/\u003cspan class="pl-k"\u003e**\u003c/span\u003e \u003cspan class="pl-k"\u003e-\u003c/span\u003e\u003cspan class="pl-v"\u003ediff\u003c/span\u003e \u003cspan class="pl-v"\u003elinguist-generated\u003c/span\u003e=\u003cspan class="pl-c1"\u003etrue\u003c/span\u003e ',
]);
assert.strictEqual(data.htmlContent, null);
});
it("should fetch issues", async () => {
const data = await getGitHubIssues("actions", "deploy-pages");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.q, undefined);
const issue402 = data.issues.find((issue) => issue.number === 402);
assert.ok(issue402);
assert.strictEqual(issue402.title, "Dry Run");
assert.strictEqual(issue402.state, "OPEN");
assert.strictEqual(issue402.createdAt, "2025-07-03T19:07:08Z");
});
it("should fetch filtered issues with q", async () => {
const data = await getGitHubIssues("actions", "deploy-pages", {
q: "is:issue state:closed",
});
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.q, "is:issue state:closed");
const issue417 = data.issues.find((issue) => issue.number === 417);
assert.ok(issue417);
assert.strictEqual(issue417.title, "Chats");
assert.ok(data.issues.every((issue) => issue.title !== ""));
});
it("should fetch pull requests", async () => {
const data = await getGitHubPulls("actions", "deploy-pages", { q: "state:closed" });
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
const pr411 = data.pulls.find((pull) => pull.number === 411);
assert.ok(pr411);
assert.strictEqual(pr411.title, "update node version");
});
it("should fetch commits", async () => {
const data = await getGitHubCommits("actions", "deploy-pages", "main", "");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.branch, "main");
assert.strictEqual(data.path, "");
assert.ok(data.commitGroups.length > 0);
const firstGroup = data.commitGroups[0];
assert.ok(firstGroup.title.length > 0);
assert.ok(firstGroup.commits.length > 0);
const firstCommit = firstGroup.commits[0];
assert.ok(firstCommit.oid.length === 40);
assert.ok(firstCommit.shortMessage.length > 0);
assert.ok(firstCommit.authors.length > 0);
});
it("should fetch commits with path", async () => {
const data = await getGitHubCommits("actions", "deploy-pages", "main", "src");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.branch, "main");
assert.strictEqual(data.path, "src");
assert.ok(data.commitGroups.length > 0);
});
// GitHub serves a file's history from the same route as a directory's.
it("should fetch the history of a blob", async () => {
const data = await getGitHubCommits("actions", "deploy-pages", "main", "src/index.js");
assert.strictEqual(data.path, "src/index.js");
assert.ok(data.commitGroups.length > 0);
const commit = data.commitGroups[0].commits[0];
assert.strictEqual(commit.oid.length, 40);
assert.ok(commit.shortMessage.length > 0);
});
// What a history page's "Browse" link resolves to: the file as it stood at
// one of its commits. Nothing in a commits payload says whether the path is
// a file or a directory, so those links all point at the tree and rely on
// GitHub redirecting a file's to its blob.
it("should redirect a blob browsed at a commit to its blob view", async () => {
const oid = "b39c421b98f49d83ae50ec502c3ddfc3bf28f2c6";
await assert.rejects(getGitHubTree("actions", "deploy-pages", oid, "src/index.js"), (err) => {
assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError");
assert.strictEqual(err.location, `/actions/deploy-pages/blob/${oid}/src/index.js`);
return true;
});
const data = await getGitHubBlob("actions", "deploy-pages", oid, "src/index.js");
assert.strictEqual(data.branch, oid);
assert.strictEqual(data.path, "src/index.js");
assert.strictEqual(data.size, "1.37 KB / 51 lines / 40 loc");
assert.strictEqual(data.textLines?.length, 51);
});
it("should fetch releases", async () => {
const data = await getGitHubReleases("actions", "deploy-pages");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.ok(data.releases.length > 0);
const firstRelease = data.releases[0];
assert.ok(firstRelease.tagName.length > 0);
assert.ok(firstRelease.title.match(/^v\d/));
assert.ok(firstRelease.publishedAt.length > 0);
assert.ok(firstRelease.bodyHtml.length > 0);
});
it("should fetch latest commit for a path", async () => {
const data = await getGitHubLatestCommit("actions", "deploy-pages", "main", "src");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.ok(data.commit);
assert.strictEqual(data.commit.oid.length, 40);
assert.ok(data.commit.shortMessageHtml.length > 0);
assert.ok(data.commit.date.length > 0);
assert.ok(data.commit.authors.length > 0);
});
it("should fetch the sidebar", async () => {
const data = await getGitHubSidebar("actions", "deploy-pages");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.deepStrictEqual(data.languages, [
{ name: "JavaScript", percentage: 100, color: "#f1e05a" },
]);
assert.strictEqual(data.latestRelease?.tag, "v5.0.1");
assert.ok(data.latestRelease?.publishedAt?.startsWith("2026-09-01"));
});
it("should fetch the sidebar for a multi-language repository", async () => {
const data = await getGitHubSidebar("rust-lang", "rust");
const rust = data.languages.find((language) => language.name === "Rust");
assert.ok(rust);
assert.ok(rust.percentage > 50);
assert.strictEqual(rust.color, "#dea584");
// The aggregate bucket comes back without a name.
const other = data.languages.find((language) => language.name === "Other");
assert.ok(other);
const total = data.languages.reduce((sum, language) => sum + language.percentage, 0);
assert.ok(Math.abs(total - 100) < 1);
});
it("should fetch branches", async () => {
const data = await getGitHubRefs("actions", "deploy-pages", "branches");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.type, "branches");
assert.ok(data.refs.length > 0);
const main = data.refs.find((ref) => ref.name === "main");
assert.ok(main);
assert.strictEqual(main.isDefault, true);
});
it("should fetch tags", async () => {
const data = await getGitHubRefs("actions", "deploy-pages", "tags");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.type, "tags");
assert.ok(data.refs.length > 0);
const tag = data.refs.find((ref) => ref.name === "v5.0.0");
assert.ok(tag);
assert.ok(tag.date?.length);
assert.strictEqual(data.hasMore, true);
});
it("should fetch single release", async () => {
const data = await getGitHubRelease("actions", "deploy-pages", "v4.0.5");
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.ok(data.release.tagName.length > 0);
assert.ok(data.release.title.length > 0);
assert.ok(data.release.publishedAt.length > 0);
assert.ok(data.release.bodyHtml.length > 0);
assert.ok(data.release.assets && data.release.assets.length >= 2);
assert.strictEqual(data.release.assets[0].name, "Source code (zip)");
assert.strictEqual(
data.release.assets[0].url,
"https://github.com/actions/deploy-pages/archive/refs/tags/v4.0.5.zip",
);
});
});
describe("dustinlyons/nixos-config repository", () => {
it("should fetch nix blob with combined overlapping styling directives", async () => {
const data = await getGitHubBlob("dustinlyons", "nixos-config", "main", "flake.nix");
assert.strictEqual(data.repo.owner, "dustinlyons");
assert.strictEqual(data.repo.name, "nixos-config");
assert.strictEqual(data.path, "flake.nix");
assert.strictEqual(data.image, false);
assert.strictEqual(data.htmlContent, null);
assert.ok(data.htmlLines !== null && data.htmlLines.length > 0);
// Line 2 has directives [2,13,"pl-c1"] and [2,13,"pl-s1"] with identical ranges.
// They should be combined into one span, not duplicated.
const line2 = data.htmlLines[1];
assert.ok(line2.includes('class="pl-c1 pl-s1"'), `expected combined classes in: ${line2}`);
assert.ok(
(line2.match(/description/g) ?? []).length === 1,
`expected "description" to appear once in: ${line2}`,
);
});
});
describe("rich file types", () => {
it("should support PNG", async () => {
const data = await getGitHubBlob("github", "docs", "main", "assets/images/site/logo.png");
assert.strictEqual(data.repo.owner, "github");
assert.strictEqual(data.repo.name, "docs");
assert.strictEqual(data.branch, "main");
assert.strictEqual(data.path, "assets/images/site/logo.png");
assert.strictEqual(data.size, "1.26 KB");
assert.strictEqual(data.language, null);
assert.strictEqual(data.image, true);
assert.strictEqual(data.renderFileType, null);
assert.strictEqual(data.textLines, null);
assert.strictEqual(data.htmlLines, null);
assert.strictEqual(data.htmlContent, null);
});
// GitHub renders a PDF with a viewer of its own, so the page carries no
// content for it beyond the file type.
it("should support PDF", async () => {
const data = await getGitHubBlob("mozilla", "pdf.js", "master", "test/pdfs/basicapi.pdf");
assert.strictEqual(data.repo.owner, "mozilla");
assert.strictEqual(data.repo.name, "pdf.js");
assert.strictEqual(data.branch, "master");
assert.strictEqual(data.path, "test/pdfs/basicapi.pdf");
assert.strictEqual(data.size, "103 KB");
assert.strictEqual(data.language, null);
assert.strictEqual(data.image, false);
assert.strictEqual(data.renderFileType, "pdf");
assert.strictEqual(data.textLines, null);
assert.strictEqual(data.htmlLines, null);
assert.strictEqual(data.htmlContent, null);
});
// Why the embed route exists rather than the blob view pointing a viewer
// at GitHub: as application/octet-stream a browser downloads the file
// instead of displaying it.
it("should fetch a raw PDF that GitHub types as a download", async () => {
const response = await getGitHubRaw("mozilla", "pdf.js", "master", "test/pdfs/basicapi.pdf");
assert.strictEqual(response.headers.get("Content-Type"), "application/octet-stream");
assert.ok((await response.text()).startsWith("%PDF-"));
});
// A binary file GitHub has no preview for: no content of any kind, and the
// path exercises escaping of a space.
it("should support an unpreviewable binary file", async () => {
const data = await getGitHubBlob("justrajdeep", "fonts", "master", "Times New Roman.ttf");
assert.strictEqual(data.repo.owner, "justrajdeep");
assert.strictEqual(data.repo.name, "fonts");
assert.strictEqual(data.branch, "master");
assert.strictEqual(data.path, "Times New Roman.ttf");
assert.strictEqual(data.size, "815 KB");
assert.strictEqual(data.language, null);
assert.strictEqual(data.image, false);
assert.strictEqual(data.renderFileType, null);
assert.strictEqual(data.textLines, null);
assert.strictEqual(data.htmlLines, null);
assert.strictEqual(data.htmlContent, null);
});
});
describe("wikis", () => {
it("should fetch the home page a wiki serves off /wiki itself", async () => {
const data = await getGitHubWiki("microsoft", "vscode");
assert.strictEqual(data.repo.owner, "microsoft");
assert.strictEqual(data.repo.name, "vscode");
assert.strictEqual(data.title, "Home");
assert.strictEqual(data.updatedBy, "Greg Van Liew");
assert.strictEqual(data.updatedAt, "2019-11-22T03:08:32Z");
assert.match(data.bodyHtml, /Welcome to the Visual Studio Code Wiki/);
// The _Sidebar and _Footer pages this wiki defines.
assert.match(data.sidebarHtml ?? "", /href="\/microsoft\/vscode\/wiki\/Roadmap"/);
assert.match(data.footerHtml ?? "", /Want to contribute to this Wiki\?/);
});
it("should fetch a page by its slug", async () => {
const data = await getGitHubWiki("microsoft", "vscode", "Commit-Signing");
assert.strictEqual(data.title, "Commit Signing");
assert.match(data.bodyHtml, /GPG/);
});
// The slug arrives from our route already decoded, so it has to be encoded
// again rather than interpolated into the URL as-is.
it("should fetch a page whose slug is not path-safe", async () => {
const data = await getGitHubWiki(
"microsoft",
"vscode",
"[DEV]-Perf-Tools-for-VS-Code-Development",
);
assert.strictEqual(data.title, "[DEV] Perf Tools for VS Code Development");
assert.ok(data.bodyHtml.length > 0);
});
it("should fetch an older revision of a page", async () => {
const oid = "f1c60035ba38b68ba383f58e482c312218daf86c";
const data = await getGitHubWiki("microsoft", "vscode", "Commit-Signing", oid);
assert.strictEqual(data.title, "Commit Signing");
assert.strictEqual(data.updatedBy, "Joaqu\u00edn Ruales");
assert.strictEqual(data.updatedAt, "2025-10-17T15:07:14Z");
});
// The sidebar list is complete; its "Show N more pagesβ¦" button only
// unhides the rest, so no second request is needed to link every page.
it("should collect every page from the sidebar of any page", async () => {
const data = await getGitHubWiki("microsoft", "vscode");
assert.ok(data.pages.length > 15);
assert.deepStrictEqual(data.pages[0], { name: "Home", href: "/microsoft/vscode/wiki" });
assert.ok(data.pages.some((page) => page.href === "/microsoft/vscode/wiki/Commit-Signing"));
});
// An archived repository, so neither its wiki nor these dates can move.
it("should fetch the page index with its last-updated dates", async () => {
const data = await getGitHubWikiPages("ariya", "phantomjs");
assert.strictEqual(data.repo.owner, "ariya");
assert.ok(data.pages.length > 1);
assert.ok(data.pages.every((page) => page.updatedAt));
const home = data.pages.find((page) => page.name === "Home");
assert.ok(home);
assert.strictEqual(home.href, "/ariya/phantomjs/wiki");
assert.strictEqual(home.updatedAt, "2018-02-08T18:57:56Z");
});
it("should redirect to the repository when it has no wiki", async () => {
await assert.rejects(getGitHubWiki("git", "git"), (err) => {
assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError");
assert.strictEqual(err.location, "/git/git");
return true;
});
});
});
describe("issues", () => {
it("should fetch an issue with its comments", async () => {
const data = await getGitHubIssue("actions", "deploy-pages", 402);
assert.strictEqual(data.repo.owner, "actions");
assert.strictEqual(data.repo.name, "deploy-pages");
assert.strictEqual(data.number, 402);
assert.strictEqual(data.titleHtml, "Dry Run");
assert.strictEqual(data.state, "OPEN");
assert.strictEqual(data.stateReason, null);
assert.strictEqual(data.createdAt, "2025-07-03T19:07:08Z");
// The issue's own post reads like the comments that follow it.
assert.strictEqual(data.body.anchor, "issue-3200386867");
assert.strictEqual(data.body.author, "wmcnamee-coreweave");
assert.strictEqual(data.body.createdAt, "2025-07-03T19:07:08Z");
assert.match(data.body.bodyHtml, /I'd like to do a dry run/);
const comments = data.timeline.filter((item) => item.kind === "comment");
const comment = comments.find((item) => item.anchor === "issuecomment-3090316111");
assert.ok(comment);
assert.strictEqual(comment.author, "yoannchaudet");
assert.strictEqual(comment.association, "Contributor");
assert.match(comment.bodyHtml, /dry run/);
});
it("should fetch the events an issue's timeline carries", async () => {
const data = await getGitHubIssue("actions", "deploy-pages", 417);
assert.strictEqual(data.state, "CLOSED");
assert.strictEqual(data.stateReason, "COMPLETED");
const events = data.timeline.filter((item) => item.kind === "event");
const closed = events.find((event) => event.description === "closed this as completed");
assert.ok(closed);
assert.strictEqual(closed.anchor, "event-25375447940");
assert.strictEqual(closed.actor, "beingme8583-netizen");
assert.strictEqual(closed.label, null);
assert.strictEqual(closed.link, null);
});
it("should fetch the labels, assignees, and milestone of an issue", async () => {
const data = await getGitHubIssue("microsoft", "vscode", 199954);
assert.strictEqual(data.state, "CLOSED");
assert.deepStrictEqual(data.assignees, ["jrieken"]);
assert.deepStrictEqual(data.milestone, {
title: "June 2025",
href: "/microsoft/vscode/milestone/308",
});
const label = data.labels.find((label) => label.name === "feature-request");
assert.ok(label);
assert.strictEqual(label.color, "dcdcdc");
assert.strictEqual(label.description, "Request for new features or functionality");
// A labelling event names the label it applied.
const events = data.timeline.filter((item) => item.kind === "event");
const labelled = events.find((event) => event.label?.name === "feature-request");
assert.ok(labelled);
assert.strictEqual(labelled.description, "added the");
assert.strictEqual(labelled.actor, "jrieken");
// GitHub only sends the oldest page of a long timeline with the page.
assert.ok(data.hiddenItems > 0);
});
it("should redirect an issue number that names a pull request", async () => {
await assert.rejects(getGitHubIssue("actions", "deploy-pages", 411), (err) => {
assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError");
assert.strictEqual(err.location, "/actions/deploy-pages/pull/411");
return true;
});
});
});
describe("discussions", () => {
it("should fetch the discussion list with its categories", async () => {
const data = await getGitHubDiscussions("jj-vcs", "jj");
assert.strictEqual(data.repo.owner, "jj-vcs");
assert.strictEqual(data.repo.name, "jj");
assert.strictEqual(data.q, undefined);
assert.strictEqual(data.category, null);
assert.ok(data.discussions.length > 0);
assert.ok(data.discussions.every((discussion) => discussion.number > 0));
assert.ok(data.discussions.every((discussion) => discussion.titleHtml !== ""));
assert.ok(data.discussions.every((discussion) => discussion.createdAt));
assert.deepStrictEqual(
data.categories.find((category) => category.slug === "q-a"),
{ slug: "q-a", name: "Q&A", emoji: "\u{1F64F}" },
);
});
// The category link in a row carries the filter it would apply as a query,
// which is not part of the slug we route on.
it("should narrow the list to one category", async () => {
const data = await getGitHubDiscussions("jj-vcs", "jj", { category: "q-a" });
assert.deepStrictEqual(data.category, { slug: "q-a", name: "Q&A", emoji: "\u{1F64F}" });
assert.ok(data.discussions.length > 0);
assert.ok(data.discussions.every((discussion) => discussion.category?.slug === "q-a"));
});
it("should search the list with q", async () => {
const data = await getGitHubDiscussions("jj-vcs", "jj", { q: "advance-branches" });
assert.strictEqual(data.q, "advance-branches");
const feedback = data.discussions.find((discussion) => discussion.number === 3549);
assert.ok(feedback);
assert.strictEqual(feedback.author, "emesterhazy");
assert.strictEqual(feedback.category?.slug, "general");
assert.strictEqual(feedback.createdAt, "2024-04-20T14:32:15Z");
assert.strictEqual(feedback.answered, false);
});
it("should fetch a discussion with its comments and replies", async () => {
const data = await getGitHubDiscussion("jj-vcs", "jj", 3549);
assert.strictEqual(data.repo.owner, "jj-vcs");
assert.strictEqual(data.number, 3549);
// Titles are Markdown, so the code span survives into the HTML.
assert.strictEqual(
data.titleHtml,
"Feedback for the experimental advance-branches feature",
);
assert.strictEqual(data.author, "emesterhazy");
assert.deepStrictEqual(data.category, { slug: "general", name: "General", emoji: null });
assert.strictEqual(data.answerAnchor, null);
assert.strictEqual(data.body.anchor, "discussion-6532740");
assert.strictEqual(data.body.author, "emesterhazy");
assert.strictEqual(data.body.createdAt, "2024-04-20T14:32:15Z");
assert.match(data.body.bodyHtml, /Please leave any feedback/);
assert.ok(data.comments.length > 0);
assert.ok(data.comments.every((comment) => comment.anchor.startsWith("discussioncomment-")));
// Replies hang off the comment they answer.
const threaded = data.comments.filter((comment) => comment.replies.length > 0);
assert.ok(threaded.length > 0);
// GitHub collapses the older replies of the longest threads.
assert.ok(data.comments.some((comment) => comment.hiddenReplies > 0));
});
it("should mark the comment a discussion was answered by", async () => {
const data = await getGitHubDiscussion("mozilla", "pdf.js", 18821);
assert.strictEqual(data.repo.owner, "mozilla");
assert.strictEqual(data.repo.name, "pdf.js");
assert.strictEqual(data.number, 18821);
assert.strictEqual(
data.titleHtml,
"[Bug]: TypeError Promise.withResolvers is not a function",
);
assert.strictEqual(data.author, "techpranav");
assert.deepStrictEqual(data.category, { slug: "q-a", name: "Q&A", emoji: null });
assert.strictEqual(data.body.anchor, "discussion-7249716");
assert.strictEqual(data.body.createdAt, "2024-09-29T08:35:02Z");
assert.strictEqual(data.body.isAnswer, false);
assert.strictEqual(data.answerAnchor, "discussioncomment-10945480");
const answer = data.comments.find((comment) => comment.isAnswer);
assert.ok(answer);
assert.strictEqual(answer.anchor, data.answerAnchor);
assert.strictEqual(answer.author, "Snuffleupagus");
assert.deepStrictEqual(answer.labels, ["Collaborator"]);
assert.strictEqual(answer.replies.length, 1);
assert.strictEqual(answer.replies[0].isAnswer, false);
});
// A comment GitHub collapsed is served as its reason alone: signed out,
// there is no author, no timestamp, and no body to show.
it("should keep the reason a comment was hidden", async () => {
const data = await getGitHubDiscussion("mozilla", "pdf.js", 18821);
const replies = data.comments.flatMap((comment) => comment.replies);
const hidden = replies.find((reply) => reply.anchor === "discussioncomment-10945516");
assert.ok(hidden);
assert.strictEqual(hidden.hiddenReason, "This comment has been hidden.");
assert.strictEqual(hidden.author, null);
assert.strictEqual(hidden.createdAt, null);
assert.strictEqual(hidden.bodyHtml, "");
// The comment it is a reply to is shown as usual, hidden reply and all.
const parent = data.comments.find((comment) =>
comment.replies.some((reply) => reply === hidden),
);
assert.ok(parent);
assert.strictEqual(parent.hiddenReason, null);
assert.ok(parent.bodyHtml !== "");
});
it("should throw for a repository with discussions turned off", async () => {
await assert.rejects(getGitHubDiscussions("torvalds", "linux"), (err) => {
assert(err instanceof GitHubHTTPError, "error should be an HTTPError");
assert.strictEqual(err.status, 404);
return true;
});
});
});
describe("owner pages", () => {
it("should fetch user profile", async () => {
const data = await getGitHubOwner("torvalds");
assert.strictEqual(data.type, "user");
assert.strictEqual(data.login, "torvalds");
assert.strictEqual(data.name, "Linus Torvalds");
assert.strictEqual(data.company, "Linux Foundation");
assert.strictEqual(data.location, "Portland, OR");
assert.ok(data.avatarUrl?.startsWith("https://avatars.githubusercontent.com/"));
assert.ok(data.followers);
assert.ok(data.following !== null);
assert.ok(data.pinned.length > 0);
const linux = data.pinned.find((repo) => repo.name === "linux");
assert.ok(linux);
assert.strictEqual(linux.owner, "torvalds");
assert.strictEqual(linux.description, "Linux kernel source tree");
assert.strictEqual(linux.language, "C");
assert.ok(linux.stars);
assert.ok(linux.forks);
assert.deepStrictEqual(data.repos, []);
const navTexts = data.nav.map((item) => item.text);
assert.deepStrictEqual(navTexts, ["Repositories", "Projects", "Packages"]);
const reposTab = data.nav.find((item) => item.text === "Repositories");
assert.strictEqual(reposTab?.href, "/torvalds?tab=repositories");
});
it("should fetch user bio and links", async () => {
const data = await getGitHubOwner("sindresorhus");
assert.strictEqual(data.login, "sindresorhus");
assert.ok(data.bio && data.bio.length > 0);
assert.ok(data.links.length > 0);
assert.ok(data.links.every((link) => link.href.startsWith("https://")));
assert.ok(data.nav.some((item) => item.text === "Sponsoring"));
});
it("should fetch organization profile", async () => {
const data = await getGitHubOwner("github");
assert.strictEqual(data.type, "organization");
assert.strictEqual(data.login, "github");
assert.strictEqual(data.name, "GitHub");
assert.strictEqual(data.bio, "How people build software.");
assert.strictEqual(data.location, "United States of America");
assert.ok(data.avatarUrl?.startsWith("https://avatars.githubusercontent.com/"));
assert.ok(data.followers);
assert.strictEqual(data.following, null);
assert.ok(data.links.some((link) => link.href === "https://github.com/about"));
assert.ok(data.pinned.length > 0);
assert.ok(data.pinned.length <= 6, "pinned should not include the repository list");
assert.ok(data.pinned.every((repo) => repo.owner === "github"));
assert.ok(data.pinned.some((repo) => repo.stars && repo.forks));
assert.ok(data.repos.length > 0);
assert.ok(data.repos.every((repo) => repo.owner === "github"));
const navTexts = data.nav.map((item) => item.text);
assert.deepStrictEqual(navTexts, [
"Repositories",
"Projects",
"Packages",
"People",
"Sponsoring",
]);
const reposTab = data.nav.find((item) => item.text === "Repositories");
assert.strictEqual(reposTab?.href, "/orgs/github/repositories");
});
it("should redirect to GitHub for non-profile pages", async () => {
await assert.rejects(getGitHubOwner("features"), (err) => {
assert(err instanceof ExternalRedirectError, "error should be an ExternalRedirectError");
assert.strictEqual(err.location, "https://github.com/features");
return true;
});
});
it("should throw for non-existent owner", async () => {
await assert.rejects(getGitHubOwner("nosuchuserzzzz"), (err) => {
assert(err instanceof GitHubHTTPError, "error should be an HTTPError");
assert.strictEqual(err.status, 404);
return true;
});
});
});
describe("error handling", () => {
it("should throw for non-existent repository", async () => {
await assert.rejects(getGitHubRepo("nosuchowner", "nosuchrepo"), (err) => {
assert(err instanceof GitHubHTTPError, "error should be an HTTPError");
assert.strictEqual(err.status, 404);
return true;
});
});
it("should redirect when fetching a directory as a blob", async () => {
await assert.rejects(
getGitHubBlob("earendil-works", "pi", "main", "packages/agent"),
(err) => {
assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError");
assert.strictEqual(err.location, "/earendil-works/pi/tree/main/packages/agent");
return true;
},
);
});
it("should redirect when fetching a file as a tree", async () => {
await assert.rejects(
getGitHubTree("earendil-works", "pi", "main", "packages/agent/package.json"),
(err) => {
assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError");
assert.strictEqual(
err.location,
"/earendil-works/pi/blob/main/packages/agent/package.json",
);
return true;
},
);
});
});
});