From 63f1fce3a9523f34b8d53ec24fe94b62c3613774 Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 14:55:02 -0400 Subject: [PATCH 01/12] feat: fetch more pin data --- api/pin.js | 22 ++++++++ src/cards/types.d.ts | 8 +++ src/common/ops.js | 22 ++++++-- src/fetchers/repo.js | 77 +++++++++++--------------- src/fetchers/types.d.ts | 5 ++ tests/fetchRepo.test.js | 76 ++++++++++++-------------- tests/ops.test.js | 7 ++- tests/pin.test.js | 118 +++++++++++++++++++++++++++++++++------- 8 files changed, 223 insertions(+), 112 deletions(-) diff --git a/api/pin.js b/api/pin.js index ada955169910a..6690ca9206aec 100644 --- a/api/pin.js +++ b/api/pin.js @@ -23,6 +23,10 @@ export default async (req, res) => { username, repo, hide_border, + hide_title, + hide_text, + stats_only, + all_stats, title_color, icon_color, text_color, @@ -34,6 +38,10 @@ export default async (req, res) => { border_radius, border_color, description_lines_count, + show_issues, + show_prs, + show_age, + age_metric, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -81,9 +89,19 @@ export default async (req, res) => { setCacheHeaders(res, cacheSeconds); + const statsOnly = parseBoolean(stats_only) === true; + const allStats = parseBoolean(all_stats) === true; + + const finalShowIssues = allStats ? true : parseBoolean(show_issues); + const finalShowPrs = allStats ? true : parseBoolean(show_prs); + const finalShowAge = allStats ? true : parseBoolean(show_age); + return res.send( renderRepoCard(repoData, { hide_border: parseBoolean(hide_border), + hide_title: statsOnly ? true : parseBoolean(hide_title), + hide_text: statsOnly ? true : parseBoolean(hide_text), + stats_only: statsOnly, title_color, icon_color, text_color, @@ -94,6 +112,10 @@ export default async (req, res) => { show_owner: parseBoolean(show_owner), locale: locale ? locale.toLowerCase() : null, description_lines_count, + show_issues: finalShowIssues, + show_prs: finalShowPrs, + show_age: finalShowAge, + age_metric: age_metric || "first", }), ); } catch (err) { diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 7535df35bbe6f..6839e9fdb2fb8 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -35,6 +35,14 @@ export type StatCardOptions = CommonOptions & { export type RepoCardOptions = CommonOptions & { show_owner: boolean; description_lines_count: number; + hide_title?: boolean; + hide_text?: boolean; + stats_only?: boolean; + all_stats?: boolean; + show_issues?: boolean; + show_prs?: boolean; + show_age?: boolean; + age_metric?: "created" | "pushed" | "first"; }; export type TopLangOptions = CommonOptions & { diff --git a/src/common/ops.js b/src/common/ops.js index b4db6e60c8a92..9a8335fcff3e7 100644 --- a/src/common/ops.js +++ b/src/common/ops.js @@ -3,9 +3,11 @@ import toEmoji from "emoji-name-map"; /** - * Returns boolean if value is either "true" or "false" else the value as it is. + * Parses a boolean-like value from query params. + * Accepts booleans, "true"/"false" (case-insensitive), and "1"/"0". + * Returns undefined for unrecognized values. * - * @param {string | boolean} value The value to parse. + * @param {string | boolean | number | undefined} value The value to parse. * @returns {boolean | undefined } The parsed value. */ const parseBoolean = (value) => { @@ -13,10 +15,22 @@ const parseBoolean = (value) => { return value; } + if (typeof value === "number") { + if (value === 1) { + return true; + } + if (value === 0) { + return false; + } + return undefined; + } + if (typeof value === "string") { - if (value.toLowerCase() === "true") { + const normalized = value.toLowerCase().trim(); + if (normalized === "true" || normalized === "1") { return true; - } else if (value.toLowerCase() === "false") { + } + if (normalized === "false" || normalized === "0") { return false; } } diff --git a/src/fetchers/repo.js b/src/fetchers/repo.js index 304aba5fdc23c..33e0e51f89b17 100644 --- a/src/fetchers/repo.js +++ b/src/fetchers/repo.js @@ -21,34 +21,25 @@ const fetcher = (variables, token) => { isPrivate isArchived isTemplate - stargazers { - totalCount - } + createdAt + pushedAt + stargazers { totalCount } + issues(states: OPEN) { totalCount } + pullRequests(states: OPEN) { totalCount } description - primaryLanguage { - color - id - name - } + primaryLanguage { color id name } forkCount } - query getRepo($login: String!, $repo: String!) { - user(login: $login) { - repository(name: $repo) { - ...RepoInfo - } - } - organization(login: $login) { - repository(name: $repo) { - ...RepoInfo - } + query getRepo($owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + ...RepoInfo } } `, variables, }, { - Authorization: `token ${token}`, + Authorization: `bearer ${token}`, }, ); }; @@ -77,41 +68,35 @@ const fetchRepo = async (username, reponame) => { throw new MissingParamError(["repo"], urlExample); } - let res = await retryer(fetcher, { login: username, repo: reponame }); + let res = await retryer(fetcher, { owner: username, repo: reponame }); - const data = res.data.data; + const data = res && res.data ? res.data.data : null; + const errors = (res && res.data && res.data.errors) || []; - if (!data.user && !data.organization) { - throw new Error("Not found"); + if (errors.length) { + throw new Error(errors[0].message || "GitHub GraphQL error"); } - const isUser = data.organization === null && data.user; - const isOrg = data.user === null && data.organization; - - if (isUser) { - if (!data.user.repository || data.user.repository.isPrivate) { - throw new Error("User Repository Not found"); - } - return { - ...data.user.repository, - starCount: data.user.repository.stargazers.totalCount, - }; + if (!data) { + throw new Error("Invalid response from GitHub API"); } - if (isOrg) { - if ( - !data.organization.repository || - data.organization.repository.isPrivate - ) { - throw new Error("Organization Repository Not found"); - } - return { - ...data.organization.repository, - starCount: data.organization.repository.stargazers.totalCount, - }; + const repo = data.repository; + if (!repo || repo.isPrivate) { + throw new Error("Repository Not found"); } - throw new Error("Unexpected behavior"); + return { + ...repo, + starCount: repo.stargazers.totalCount, + ...(repo.issues ? { openIssuesCount: repo.issues.totalCount } : {}), + ...(repo.pullRequests + ? { openPrsCount: repo.pullRequests.totalCount } + : {}), + ...(repo.createdAt ? { createdAt: repo.createdAt } : {}), + ...(repo.pushedAt ? { pushedAt: repo.pushedAt } : {}), + firstCommitDate: repo.createdAt || null, + }; }; export { fetchRepo }; diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts index affb407b816b0..ce22a5765d262 100644 --- a/src/fetchers/types.d.ts +++ b/src/fetchers/types.d.ts @@ -13,6 +13,8 @@ export type RepositoryData = { isPrivate: boolean; isArchived: boolean; isTemplate: boolean; + createdAt?: string; + pushedAt?: string; stargazers: { totalCount: number }; description: string; primaryLanguage: { @@ -22,6 +24,9 @@ export type RepositoryData = { }; forkCount: number; starCount: number; + openIssuesCount?: number; + openPrsCount?: number; + firstCommitDate: string | null; }; export type StatsData = { diff --git a/tests/fetchRepo.test.js b/tests/fetchRepo.test.js index e976dd72a47ce..13fb31afb113b 100644 --- a/tests/fetchRepo.test.js +++ b/tests/fetchRepo.test.js @@ -7,7 +7,19 @@ import { fetchRepo } from "../src/fetchers/repo.js"; const data_repo = { repository: { name: "convoychat", + createdAt: "2020-01-01T00:00:00Z", + pushedAt: "2020-01-02T00:00:00Z", stargazers: { totalCount: 38000 }, + issues: { totalCount: 12 }, + pullRequests: { totalCount: 3 }, + defaultBranchRef: { + name: "main", + target: { + history: { + nodes: [{ committedDate: "2019-12-01T00:00:00Z" }], + }, + }, + }, description: "Help us take over the world! React + TS + GraphQL Chat App", primaryLanguage: { color: "#2b7489", @@ -18,17 +30,9 @@ const data_repo = { }, }; -const data_user = { - data: { - user: { repository: data_repo.repository }, - organization: null, - }, -}; - -const data_org = { +const data_repository = { data: { - user: null, - organization: { repository: data_repo.repository }, + repository: data_repo.repository, }, }; @@ -39,67 +43,59 @@ afterEach(() => { }); describe("Test fetchRepo", () => { - it("should fetch correct user repo", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + it("should fetch repository by owner/name", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); let repo = await fetchRepo("anuraghazra", "convoychat"); expect(repo).toStrictEqual({ ...data_repo.repository, starCount: data_repo.repository.stargazers.totalCount, - }); - }); - - it("should fetch correct org repo", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data_org); - - let repo = await fetchRepo("anuraghazra", "convoychat"); - expect(repo).toStrictEqual({ - ...data_repo.repository, - starCount: data_repo.repository.stargazers.totalCount, + openIssuesCount: data_repo.repository.issues.totalCount, + openPrsCount: data_repo.repository.pullRequests.totalCount, + createdAt: data_repo.repository.createdAt, + pushedAt: data_repo.repository.pushedAt, + firstCommitDate: data_repo.repository.createdAt, }); }); it("should throw error if user is found but repo is null", async () => { - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: { repository: null }, organization: null } }); + mock.onPost("https://api.github.com/graphql").reply(200, { + data: { repository: null }, + }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "User Repository Not found", + "Repository Not found", ); }); - it("should throw error if org is found but repo is null", async () => { - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: null, organization: { repository: null } } }); + it("should throw error if repo is null", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, { + data: { repository: null }, + }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "Organization Repository Not found", + "Repository Not found", ); }); it("should throw error if both user & org data not found", async () => { - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: null, organization: null } }); + mock.onPost("https://api.github.com/graphql").reply(200, { + data: { repository: null }, + }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "Not found", + "Repository Not found", ); }); it("should throw error if repository is private", async () => { mock.onPost("https://api.github.com/graphql").reply(200, { - data: { - user: { repository: { ...data_repo, isPrivate: true } }, - organization: null, - }, + data: { repository: { ...data_repo.repository, isPrivate: true } }, }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "User Repository Not found", + "Repository Not found", ); }); }); diff --git a/tests/ops.test.js b/tests/ops.test.js index dd4e08b88c3bc..4bd4e406b134b 100644 --- a/tests/ops.test.js +++ b/tests/ops.test.js @@ -21,8 +21,11 @@ describe("Test ops.js", () => { expect(parseBoolean("TRUE")).toBe(true); expect(parseBoolean("FALSE")).toBe(false); - expect(parseBoolean("1")).toBe(undefined); - expect(parseBoolean("0")).toBe(undefined); + expect(parseBoolean("1")).toBe(true); + expect(parseBoolean("0")).toBe(false); + expect(parseBoolean(1)).toBe(true); + expect(parseBoolean(0)).toBe(false); + expect(parseBoolean("2")).toBe(undefined); expect(parseBoolean("")).toBe(undefined); // @ts-ignore expect(parseBoolean(undefined)).toBe(undefined); diff --git a/tests/pin.test.js b/tests/pin.test.js index 6bc8bff96c0da..0984c33814a06 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -11,7 +11,7 @@ import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; const data_repo = { repository: { - username: "anuraghazra", + nameWithOwner: "anuraghazra", name: "convoychat", stargazers: { totalCount: 38000, @@ -24,13 +24,15 @@ const data_repo = { }, forkCount: 100, isTemplate: false, + isPrivate: false, + isArchived: false, + firstCommitDate: "2018-10-01T00:00:00Z", }, }; -const data_user = { +const data_repository = { data: { - user: { repository: data_repo.repository }, - organization: null, + repository: data_repo.repository, }, }; @@ -52,7 +54,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); await pin(req, res); @@ -76,13 +78,15 @@ describe("Test /api/pin", () => { text_color: "fff", bg_color: "fff", full_name: "1", + hide_title: true, + hide_text: true, }, }; const res = { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); await pin(req, res); @@ -99,30 +103,104 @@ describe("Test /api/pin", () => { ); }); - it("should render error card if user repo not found", async () => { + it("should make stats_only take precedence over hide flags", async () => { const req = { query: { username: "anuraghazra", repo: "convoychat", + hide_title: "false", + hide_text: "false", + stats_only: "true", }, }; const res = { setHeader: jest.fn(), send: jest.fn(), }; - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: { repository: null }, organization: null } }); + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); await pin(req, res); expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "User Repository Not found" }), + const expectedSvg = renderRepoCard( + { + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }, + { + hide_border: undefined, + hide_title: true, + hide_text: true, + stats_only: true, + title_color: undefined, + icon_color: undefined, + text_color: undefined, + bg_color: undefined, + theme: undefined, + border_radius: undefined, + border_color: undefined, + show_owner: undefined, + locale: undefined, + description_lines_count: undefined, + show_issues: undefined, + show_prs: undefined, + show_age: undefined, + age_metric: "first", + }, + ); + expect(res.send).toHaveBeenCalledWith(expectedSvg); + }); + + it("should make all_stats enable issues, PRs, and age", async () => { + const req = { + query: { + username: "anuraghazra", + repo: "convoychat", + show_issues: "false", + show_prs: "false", + show_age: "false", + all_stats: "true", + }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + + await pin(req, res); + + expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); + const expectedSvg = renderRepoCard( + { + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }, + { + hide_border: undefined, + hide_title: undefined, + hide_text: undefined, + title_color: undefined, + icon_color: undefined, + text_color: undefined, + bg_color: undefined, + theme: undefined, + border_radius: undefined, + border_color: undefined, + show_owner: undefined, + locale: undefined, + description_lines_count: undefined, + show_issues: false, + show_prs: false, + show_age: false, + all_stats: true, + age_metric: "first", + }, ); + expect(res.send).toHaveBeenCalledWith(expectedSvg); }); - it("should render error card if org repo not found", async () => { + it("should render error card if repo not found", async () => { const req = { query: { username: "anuraghazra", @@ -133,15 +211,15 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: null, organization: { repository: null } } }); + mock.onPost("https://api.github.com/graphql").reply(200, { + data: { repository: null }, + }); await pin(req, res); expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Organization Repository Not found" }), + renderError({ message: "Repository Not found" }), ); }); @@ -156,7 +234,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); await pin(req, res); @@ -182,7 +260,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); await pin(req, res); @@ -228,7 +306,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + mock.onPost("https://api.github.com/graphql").reply(200, data_repository); await pin(req, res); From 432b6b06819b70e7b95a88fce4876d55a6623081 Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 15:06:00 -0400 Subject: [PATCH 02/12] chore: undo 0-1 boolean conversion logic --- src/common/ops.js | 22 ++++------------------ tests/ops.test.js | 7 ++----- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/common/ops.js b/src/common/ops.js index 9a8335fcff3e7..b4db6e60c8a92 100644 --- a/src/common/ops.js +++ b/src/common/ops.js @@ -3,11 +3,9 @@ import toEmoji from "emoji-name-map"; /** - * Parses a boolean-like value from query params. - * Accepts booleans, "true"/"false" (case-insensitive), and "1"/"0". - * Returns undefined for unrecognized values. + * Returns boolean if value is either "true" or "false" else the value as it is. * - * @param {string | boolean | number | undefined} value The value to parse. + * @param {string | boolean} value The value to parse. * @returns {boolean | undefined } The parsed value. */ const parseBoolean = (value) => { @@ -15,22 +13,10 @@ const parseBoolean = (value) => { return value; } - if (typeof value === "number") { - if (value === 1) { - return true; - } - if (value === 0) { - return false; - } - return undefined; - } - if (typeof value === "string") { - const normalized = value.toLowerCase().trim(); - if (normalized === "true" || normalized === "1") { + if (value.toLowerCase() === "true") { return true; - } - if (normalized === "false" || normalized === "0") { + } else if (value.toLowerCase() === "false") { return false; } } diff --git a/tests/ops.test.js b/tests/ops.test.js index 4bd4e406b134b..dd4e08b88c3bc 100644 --- a/tests/ops.test.js +++ b/tests/ops.test.js @@ -21,11 +21,8 @@ describe("Test ops.js", () => { expect(parseBoolean("TRUE")).toBe(true); expect(parseBoolean("FALSE")).toBe(false); - expect(parseBoolean("1")).toBe(true); - expect(parseBoolean("0")).toBe(false); - expect(parseBoolean(1)).toBe(true); - expect(parseBoolean(0)).toBe(false); - expect(parseBoolean("2")).toBe(undefined); + expect(parseBoolean("1")).toBe(undefined); + expect(parseBoolean("0")).toBe(undefined); expect(parseBoolean("")).toBe(undefined); // @ts-ignore expect(parseBoolean(undefined)).toBe(undefined); From beebf6c51fb2eb5d6d640050c93cd0f802e96752 Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 15:22:38 -0400 Subject: [PATCH 03/12] refactor: tweak --- api/pin.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/pin.js b/api/pin.js index 6690ca9206aec..64d6fd303c930 100644 --- a/api/pin.js +++ b/api/pin.js @@ -95,12 +95,14 @@ export default async (req, res) => { const finalShowIssues = allStats ? true : parseBoolean(show_issues); const finalShowPrs = allStats ? true : parseBoolean(show_prs); const finalShowAge = allStats ? true : parseBoolean(show_age); + const finalHideTitle = statsOnly ? true : parseBoolean(hide_title); + const finalHideText = statsOnly ? true : parseBoolean(hide_text); return res.send( renderRepoCard(repoData, { hide_border: parseBoolean(hide_border), - hide_title: statsOnly ? true : parseBoolean(hide_title), - hide_text: statsOnly ? true : parseBoolean(hide_text), + hide_title: finalHideTitle, + hide_text: finalHideText, stats_only: statsOnly, title_color, icon_color, From e3d20602ad5446aacf43c412f989031847f1c8eb Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 15:31:39 -0400 Subject: [PATCH 04/12] refactor: to fit upstream --- src/fetchers/repo.js | 98 +++++++++++++++++++++++++++++------------ tests/fetchRepo.test.js | 65 +++++++++++++++++++-------- tests/pin.test.js | 23 +++++----- 3 files changed, 128 insertions(+), 58 deletions(-) diff --git a/src/fetchers/repo.js b/src/fetchers/repo.js index 33e0e51f89b17..7d97ee0794805 100644 --- a/src/fetchers/repo.js +++ b/src/fetchers/repo.js @@ -23,23 +23,40 @@ const fetcher = (variables, token) => { isTemplate createdAt pushedAt - stargazers { totalCount } - issues(states: OPEN) { totalCount } - pullRequests(states: OPEN) { totalCount } + stargazers { + totalCount + } + issues(states: OPEN) { + totalCount + } + pullRequests(states: OPEN) { + totalCount + } description - primaryLanguage { color id name } + primaryLanguage { + color + id + name + } forkCount } - query getRepo($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - ...RepoInfo + query getRepo($login: String!, $repo: String!) { + user(login: $login) { + repository(name: $repo) { + ...RepoInfo + } + } + organization(login: $login) { + repository(name: $repo) { + ...RepoInfo + } } } `, variables, }, { - Authorization: `bearer ${token}`, + Authorization: `token ${token}`, }, ); }; @@ -68,35 +85,58 @@ const fetchRepo = async (username, reponame) => { throw new MissingParamError(["repo"], urlExample); } - let res = await retryer(fetcher, { owner: username, repo: reponame }); + let res = await retryer(fetcher, { login: username, repo: reponame }); - const data = res && res.data ? res.data.data : null; - const errors = (res && res.data && res.data.errors) || []; + const data = res.data.data; - if (errors.length) { - throw new Error(errors[0].message || "GitHub GraphQL error"); + if (!data.user && !data.organization) { + throw new Error("Not found"); } - if (!data) { - throw new Error("Invalid response from GitHub API"); + const isUser = data.organization === null && data.user; + const isOrg = data.user === null && data.organization; + + if (isUser) { + const repository = data.user.repository; + if (!repository || repository.isPrivate) { + throw new Error("User Repository Not found"); + } + return { + ...repository, + starCount: repository.stargazers.totalCount, + ...(repository.issues + ? { openIssuesCount: repository.issues.totalCount } + : {}), + ...(repository.pullRequests + ? { openPrsCount: repository.pullRequests.totalCount } + : {}), + ...(repository.createdAt ? { createdAt: repository.createdAt } : {}), + ...(repository.pushedAt ? { pushedAt: repository.pushedAt } : {}), + firstCommitDate: repository.createdAt || null, + }; } - const repo = data.repository; - if (!repo || repo.isPrivate) { - throw new Error("Repository Not found"); + if (isOrg) { + const repository = data.organization.repository; + if (!repository || repository.isPrivate) { + throw new Error("Organization Repository Not found"); + } + return { + ...repository, + starCount: repository.stargazers.totalCount, + ...(repository.issues + ? { openIssuesCount: repository.issues.totalCount } + : {}), + ...(repository.pullRequests + ? { openPrsCount: repository.pullRequests.totalCount } + : {}), + ...(repository.createdAt ? { createdAt: repository.createdAt } : {}), + ...(repository.pushedAt ? { pushedAt: repository.pushedAt } : {}), + firstCommitDate: repository.createdAt || null, + }; } - return { - ...repo, - starCount: repo.stargazers.totalCount, - ...(repo.issues ? { openIssuesCount: repo.issues.totalCount } : {}), - ...(repo.pullRequests - ? { openPrsCount: repo.pullRequests.totalCount } - : {}), - ...(repo.createdAt ? { createdAt: repo.createdAt } : {}), - ...(repo.pushedAt ? { pushedAt: repo.pushedAt } : {}), - firstCommitDate: repo.createdAt || null, - }; + throw new Error("Unexpected behavior"); }; export { fetchRepo }; diff --git a/tests/fetchRepo.test.js b/tests/fetchRepo.test.js index 13fb31afb113b..162fb05bcf4c3 100644 --- a/tests/fetchRepo.test.js +++ b/tests/fetchRepo.test.js @@ -30,9 +30,17 @@ const data_repo = { }, }; -const data_repository = { +const data_user = { data: { - repository: data_repo.repository, + user: { repository: data_repo.repository }, + organization: null, + }, +}; + +const data_org = { + data: { + user: null, + organization: { repository: data_repo.repository }, }, }; @@ -43,8 +51,8 @@ afterEach(() => { }); describe("Test fetchRepo", () => { - it("should fetch repository by owner/name", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + it("should fetch repository for user owner", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, data_user); let repo = await fetchRepo("anuraghazra", "convoychat"); @@ -59,43 +67,64 @@ describe("Test fetchRepo", () => { }); }); - it("should throw error if user is found but repo is null", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, { - data: { repository: null }, + it("should fetch repository for organization owner", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, data_org); + + let repo = await fetchRepo("anuraghazra", "convoychat"); + + expect(repo).toStrictEqual({ + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + openIssuesCount: data_repo.repository.issues.totalCount, + openPrsCount: data_repo.repository.pullRequests.totalCount, + createdAt: data_repo.repository.createdAt, + pushedAt: data_repo.repository.pushedAt, + firstCommitDate: data_repo.repository.createdAt, }); + }); + + it("should throw error if user is found but repo is null", async () => { + mock + .onPost("https://api.github.com/graphql") + .reply(200, { data: { user: { repository: null }, organization: null } }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "Repository Not found", + "User Repository Not found", ); }); it("should throw error if repo is null", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, { - data: { repository: null }, - }); + mock + .onPost("https://api.github.com/graphql") + .reply(200, { data: { user: null, organization: { repository: null } } }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "Repository Not found", + "Organization Repository Not found", ); }); it("should throw error if both user & org data not found", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, { - data: { repository: null }, - }); + mock + .onPost("https://api.github.com/graphql") + .reply(200, { data: { user: null, organization: null } }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "Repository Not found", + "Not found", ); }); it("should throw error if repository is private", async () => { mock.onPost("https://api.github.com/graphql").reply(200, { - data: { repository: { ...data_repo.repository, isPrivate: true } }, + data: { + user: { + repository: { ...data_repo.repository, isPrivate: true }, + }, + organization: null, + }, }); await expect(fetchRepo("anuraghazra", "convoychat")).rejects.toThrow( - "Repository Not found", + "User Repository Not found", ); }); }); diff --git a/tests/pin.test.js b/tests/pin.test.js index 0984c33814a06..2c1e703f5991d 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -30,9 +30,10 @@ const data_repo = { }, }; -const data_repository = { +const data_user = { data: { - repository: data_repo.repository, + user: { repository: data_repo.repository }, + organization: null, }, }; @@ -54,7 +55,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); @@ -86,7 +87,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); @@ -117,7 +118,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); @@ -166,7 +167,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); @@ -212,14 +213,14 @@ describe("Test /api/pin", () => { send: jest.fn(), }; mock.onPost("https://api.github.com/graphql").reply(200, { - data: { repository: null }, + data: { user: null, organization: null }, }); await pin(req, res); expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Repository Not found" }), + renderError({ message: "Not found" }), ); }); @@ -234,7 +235,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); @@ -260,7 +261,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); @@ -306,7 +307,7 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data_repository); + mock.onPost("https://api.github.com/graphql").reply(200, data_user); await pin(req, res); From 2f6661fb25bc3a47773d81b349e0490f3cdd68d3 Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 15:56:51 -0400 Subject: [PATCH 05/12] refactor: simplify syntax --- src/fetchers/repo.js | 48 +++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/fetchers/repo.js b/src/fetchers/repo.js index 7d97ee0794805..4e818b31385b6 100644 --- a/src/fetchers/repo.js +++ b/src/fetchers/repo.js @@ -101,19 +101,25 @@ const fetchRepo = async (username, reponame) => { if (!repository || repository.isPrivate) { throw new Error("User Repository Not found"); } - return { + const result = { ...repository, starCount: repository.stargazers.totalCount, - ...(repository.issues - ? { openIssuesCount: repository.issues.totalCount } - : {}), - ...(repository.pullRequests - ? { openPrsCount: repository.pullRequests.totalCount } - : {}), - ...(repository.createdAt ? { createdAt: repository.createdAt } : {}), - ...(repository.pushedAt ? { pushedAt: repository.pushedAt } : {}), firstCommitDate: repository.createdAt || null, }; + if (repository.issues) { + // `issues` can be omitted entirely when disabled, so only expose the count when available. + result.openIssuesCount = repository.issues.totalCount; + } + if (repository.pullRequests) { + result.openPrsCount = repository.pullRequests.totalCount; + } + if (repository.createdAt) { + result.createdAt = repository.createdAt; + } + if (repository.pushedAt) { + result.pushedAt = repository.pushedAt; + } + return result; } if (isOrg) { @@ -121,19 +127,25 @@ const fetchRepo = async (username, reponame) => { if (!repository || repository.isPrivate) { throw new Error("Organization Repository Not found"); } - return { + const result = { ...repository, starCount: repository.stargazers.totalCount, - ...(repository.issues - ? { openIssuesCount: repository.issues.totalCount } - : {}), - ...(repository.pullRequests - ? { openPrsCount: repository.pullRequests.totalCount } - : {}), - ...(repository.createdAt ? { createdAt: repository.createdAt } : {}), - ...(repository.pushedAt ? { pushedAt: repository.pushedAt } : {}), firstCommitDate: repository.createdAt || null, }; + if (repository.issues) { + // `issues` can be omitted entirely when disabled, so only expose the count when available. + result.openIssuesCount = repository.issues.totalCount; + } + if (repository.pullRequests) { + result.openPrsCount = repository.pullRequests.totalCount; + } + if (repository.createdAt) { + result.createdAt = repository.createdAt; + } + if (repository.pushedAt) { + result.pushedAt = repository.pushedAt; + } + return result; } throw new Error("Unexpected behavior"); From ffa86babf95af6d50f5120e924de04c86a2e0c8b Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 16:13:29 -0400 Subject: [PATCH 06/12] chore: rename variable (against type convention) --- tests/pin.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pin.test.js b/tests/pin.test.js index 2c1e703f5991d..9020eed1b5154 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -11,7 +11,7 @@ import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; const data_repo = { repository: { - nameWithOwner: "anuraghazra", + username: "anuraghazra", name: "convoychat", stargazers: { totalCount: 38000, From cd978fd9e0f98474b10fef0e2d463a316dea975f Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 16:21:52 -0400 Subject: [PATCH 07/12] chore: minor test refactor --- tests/pin.test.js | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/tests/pin.test.js b/tests/pin.test.js index 9020eed1b5154..dd8bb46618146 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -129,23 +129,9 @@ describe("Test /api/pin", () => { starCount: data_repo.repository.stargazers.totalCount, }, { - hide_border: undefined, + stats_only: true, hide_title: true, hide_text: true, - stats_only: true, - title_color: undefined, - icon_color: undefined, - text_color: undefined, - bg_color: undefined, - theme: undefined, - border_radius: undefined, - border_color: undefined, - show_owner: undefined, - locale: undefined, - description_lines_count: undefined, - show_issues: undefined, - show_prs: undefined, - show_age: undefined, age_metric: "first", }, ); @@ -178,22 +164,9 @@ describe("Test /api/pin", () => { starCount: data_repo.repository.stargazers.totalCount, }, { - hide_border: undefined, - hide_title: undefined, - hide_text: undefined, - title_color: undefined, - icon_color: undefined, - text_color: undefined, - bg_color: undefined, - theme: undefined, - border_radius: undefined, - border_color: undefined, - show_owner: undefined, - locale: undefined, - description_lines_count: undefined, - show_issues: false, - show_prs: false, - show_age: false, + show_issues: true, + show_prs: true, + show_age: true, all_stats: true, age_metric: "first", }, From b04589b8eb3557108d1ac4e564e6ba841f2d594c Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 16:24:55 -0400 Subject: [PATCH 08/12] refactor: restore parity with upstream --- tests/pin.test.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/pin.test.js b/tests/pin.test.js index dd8bb46618146..70ebf763e5981 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -174,7 +174,7 @@ describe("Test /api/pin", () => { expect(res.send).toHaveBeenCalledWith(expectedSvg); }); - it("should render error card if repo not found", async () => { + it("should render error card if user repo not found", async () => { const req = { query: { username: "anuraghazra", @@ -185,15 +185,38 @@ describe("Test /api/pin", () => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, { - data: { user: null, organization: null }, - }); + mock + .onPost("https://api.github.com/graphql") + .reply(200, { data: { user: { repository: null }, organization: null } }); await pin(req, res); expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Not found" }), + renderError({ message: "User Repository Not found" }), + ); + }); + + it("should render error card if org repo not found", async () => { + const req = { + query: { + username: "anuraghazra", + repo: "convoychat", + }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + mock + .onPost("https://api.github.com/graphql") + .reply(200, { data: { user: null, organization: { repository: null } } }); + + await pin(req, res); + + expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); + expect(res.send).toHaveBeenCalledWith( + renderError({ message: "Organization Repository Not found" }), ); }); From 978b5d9978e46a8542b7698f80df298d9a62b373 Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 21:37:17 -0400 Subject: [PATCH 09/12] docs: update readme with new pins data fields --- readme.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/readme.md b/readme.md index 78fe9ae42fb69..7968c1a329975 100644 --- a/readme.md +++ b/readme.md @@ -422,6 +422,18 @@ You can customize the appearance and behavior of the pinned repository card usin | `show_owner` | Shows the repo's owner name. | boolean | `false` | | `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` | +The following data points are also exposed via query params, however they have not been incorporated visually into any of the repo's themes as of yet: + +| Name | Description | Type | Default value | +| --- | --- | --- | --- | +| `show_issues` | Shows the number of open issues that the repo has. | boolean | `false` | +| `show_prs` | Shows the number of open PRs that the repo has. | boolean | `false` | +| `show_age` | Shows the age of the repo (per the `age_metric`). | boolean | `false` | +| `age_metric` | The metric by which to measure the repository's age. Options: (i) `first`: first committerdate; (ii) `pushed`: date first pushed to GitHub; (iii) `created`: creation date | enum | `first` | +| `all_stats` | Shows all the metrics listed above; shorthand for `?shows_issues=true&show_prs=true&show_age=true` | boolean | `false` | +| `stats_only` | Hides the title and the description. | boolean | `false` | + + ### Demo ![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=anuraghazra\&repo=github-readme-stats) From 46afd0f23d211780be1dfd0e4265b8f974fe6e9e Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 21:50:26 -0400 Subject: [PATCH 10/12] docs: add some comments --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 7968c1a329975..bcd024a09c1de 100644 --- a/readme.md +++ b/readme.md @@ -432,7 +432,8 @@ The following data points are also exposed via query params, however they have n | `age_metric` | The metric by which to measure the repository's age. Options: (i) `first`: first committerdate; (ii) `pushed`: date first pushed to GitHub; (iii) `created`: creation date | enum | `first` | | `all_stats` | Shows all the metrics listed above; shorthand for `?shows_issues=true&show_prs=true&show_age=true` | boolean | `false` | | `stats_only` | Hides the title and the description. | boolean | `false` | - + + ### Demo From d66e7f83491c279d38402d9d5dc5970d42172d2e Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 22:00:57 -0400 Subject: [PATCH 11/12] chore: update type of age_metric to enum --- api/pin.js | 13 ++++++++++++- src/cards/types.d.ts | 2 +- tests/pin.test.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/api/pin.js b/api/pin.js index 64d6fd303c930..85dd90d0b7699 100644 --- a/api/pin.js +++ b/api/pin.js @@ -17,6 +17,16 @@ import { renderError } from "../src/common/render.js"; import { fetchRepo } from "../src/fetchers/repo.js"; import { isLocaleAvailable } from "../src/translations.js"; +const normalizeAgeMetric = (value) => { + if (typeof value !== "string") { + return "first"; + } + const lowered = value.toLowerCase(); + return lowered === "created" || lowered === "pushed" || lowered === "first" + ? lowered + : "first"; +}; + // @ts-ignore export default async (req, res) => { const { @@ -97,6 +107,7 @@ export default async (req, res) => { const finalShowAge = allStats ? true : parseBoolean(show_age); const finalHideTitle = statsOnly ? true : parseBoolean(hide_title); const finalHideText = statsOnly ? true : parseBoolean(hide_text); + const finalAgeMetric = normalizeAgeMetric(age_metric); return res.send( renderRepoCard(repoData, { @@ -117,7 +128,7 @@ export default async (req, res) => { show_issues: finalShowIssues, show_prs: finalShowPrs, show_age: finalShowAge, - age_metric: age_metric || "first", + age_metric: finalAgeMetric, }), ); } catch (err) { diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 6839e9fdb2fb8..749ca6e23f3fc 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -42,7 +42,7 @@ export type RepoCardOptions = CommonOptions & { show_issues?: boolean; show_prs?: boolean; show_age?: boolean; - age_metric?: "created" | "pushed" | "first"; + age_metric: "created" | "pushed" | "first"; }; export type TopLangOptions = CommonOptions & { diff --git a/tests/pin.test.js b/tests/pin.test.js index 70ebf763e5981..b7a3c51749177 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -174,6 +174,37 @@ describe("Test /api/pin", () => { expect(res.send).toHaveBeenCalledWith(expectedSvg); }); + it("should default age_metric to first when invalid", async () => { + const req = { + query: { + username: "anuraghazra", + repo: "convoychat", + show_age: "true", + age_metric: "unknown-value", + }, + }; + const res = { + setHeader: jest.fn(), + send: jest.fn(), + }; + mock.onPost("https://api.github.com/graphql").reply(200, data_user); + + await pin(req, res); + + expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); + const expectedSvg = renderRepoCard( + { + ...data_repo.repository, + starCount: data_repo.repository.stargazers.totalCount, + }, + { + show_age: true, + age_metric: "first", + }, + ); + expect(res.send).toHaveBeenCalledWith(expectedSvg); + }); + it("should render error card if user repo not found", async () => { const req = { query: { From e78eb4aae2f56cebf9e24b0066df0ced040138ad Mon Sep 17 00:00:00 2001 From: Really Him Date: Wed, 22 Oct 2025 23:13:18 -0400 Subject: [PATCH 12/12] feat: remove age_metric - age is measured by GitHub createdAt --- api/pin.js | 13 ------------- readme.md | 3 +-- src/cards/types.d.ts | 1 - tests/pin.test.js | 33 --------------------------------- 4 files changed, 1 insertion(+), 49 deletions(-) diff --git a/api/pin.js b/api/pin.js index 85dd90d0b7699..ac30ed9cef747 100644 --- a/api/pin.js +++ b/api/pin.js @@ -17,16 +17,6 @@ import { renderError } from "../src/common/render.js"; import { fetchRepo } from "../src/fetchers/repo.js"; import { isLocaleAvailable } from "../src/translations.js"; -const normalizeAgeMetric = (value) => { - if (typeof value !== "string") { - return "first"; - } - const lowered = value.toLowerCase(); - return lowered === "created" || lowered === "pushed" || lowered === "first" - ? lowered - : "first"; -}; - // @ts-ignore export default async (req, res) => { const { @@ -51,7 +41,6 @@ export default async (req, res) => { show_issues, show_prs, show_age, - age_metric, } = req.query; res.setHeader("Content-Type", "image/svg+xml"); @@ -107,7 +96,6 @@ export default async (req, res) => { const finalShowAge = allStats ? true : parseBoolean(show_age); const finalHideTitle = statsOnly ? true : parseBoolean(hide_title); const finalHideText = statsOnly ? true : parseBoolean(hide_text); - const finalAgeMetric = normalizeAgeMetric(age_metric); return res.send( renderRepoCard(repoData, { @@ -128,7 +116,6 @@ export default async (req, res) => { show_issues: finalShowIssues, show_prs: finalShowPrs, show_age: finalShowAge, - age_metric: finalAgeMetric, }), ); } catch (err) { diff --git a/readme.md b/readme.md index bcd024a09c1de..2a8b5071bcb5d 100644 --- a/readme.md +++ b/readme.md @@ -428,8 +428,7 @@ The following data points are also exposed via query params, however they have n | --- | --- | --- | --- | | `show_issues` | Shows the number of open issues that the repo has. | boolean | `false` | | `show_prs` | Shows the number of open PRs that the repo has. | boolean | `false` | -| `show_age` | Shows the age of the repo (per the `age_metric`). | boolean | `false` | -| `age_metric` | The metric by which to measure the repository's age. Options: (i) `first`: first committerdate; (ii) `pushed`: date first pushed to GitHub; (iii) `created`: creation date | enum | `first` | +| `show_age` | Shows the age of the repo (based on when the repository was created on GitHub). | boolean | `false` | | `all_stats` | Shows all the metrics listed above; shorthand for `?shows_issues=true&show_prs=true&show_age=true` | boolean | `false` | | `stats_only` | Hides the title and the description. | boolean | `false` | diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 749ca6e23f3fc..4b8d5ac0ee9d2 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -42,7 +42,6 @@ export type RepoCardOptions = CommonOptions & { show_issues?: boolean; show_prs?: boolean; show_age?: boolean; - age_metric: "created" | "pushed" | "first"; }; export type TopLangOptions = CommonOptions & { diff --git a/tests/pin.test.js b/tests/pin.test.js index b7a3c51749177..c91985935da50 100644 --- a/tests/pin.test.js +++ b/tests/pin.test.js @@ -132,7 +132,6 @@ describe("Test /api/pin", () => { stats_only: true, hide_title: true, hide_text: true, - age_metric: "first", }, ); expect(res.send).toHaveBeenCalledWith(expectedSvg); @@ -168,38 +167,6 @@ describe("Test /api/pin", () => { show_prs: true, show_age: true, all_stats: true, - age_metric: "first", - }, - ); - expect(res.send).toHaveBeenCalledWith(expectedSvg); - }); - - it("should default age_metric to first when invalid", async () => { - const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - show_age: "true", - age_metric: "unknown-value", - }, - }; - const res = { - setHeader: jest.fn(), - send: jest.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - const expectedSvg = renderRepoCard( - { - ...data_repo.repository, - starCount: data_repo.repository.stargazers.totalCount, - }, - { - show_age: true, - age_metric: "first", }, ); expect(res.send).toHaveBeenCalledWith(expectedSvg);