Testing GitHub Stats Card API Implementation in github-readme-stats
This test suite validates the API functionality of github-readme-stats, focusing on request handling, error cases, and stat card rendering. It ensures proper generation of GitHub statistics cards with various customization options and error handling scenarios.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
anuraghazra/github-readme-stats
tests/api.test.js
import { jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import api from "../api/index.js";
import { calculateRank } from "../src/calculateRank.js";
import { renderStatsCard } from "../src/cards/stats-card.js";
import { CONSTANTS, renderError } from "../src/common/utils.js";
import { expect, it, describe, afterEach } from "@jest/globals";
const stats = {
name: "Anurag Hazra",
totalStars: 100,
totalCommits: 200,
totalIssues: 300,
totalPRs: 400,
totalPRsMerged: 320,
mergedPRsPercentage: 80,
totalReviews: 50,
totalDiscussionsStarted: 10,
totalDiscussionsAnswered: 40,
contributedTo: 50,
rank: null,
};
stats.rank = calculateRank({
all_commits: false,
commits: stats.totalCommits,
prs: stats.totalPRs,
reviews: stats.totalReviews,
issues: stats.totalIssues,
repos: 1,
stars: stats.totalStars,
followers: 0,
});
const data_stats = {
data: {
user: {
name: stats.name,
repositoriesContributedTo: { totalCount: stats.contributedTo },
contributionsCollection: {
totalCommitContributions: stats.totalCommits,
totalPullRequestReviewContributions: stats.totalReviews,
},
pullRequests: { totalCount: stats.totalPRs },
mergedPullRequests: { totalCount: stats.totalPRsMerged },
openIssues: { totalCount: stats.totalIssues },
closedIssues: { totalCount: 0 },
followers: { totalCount: 0 },
repositoryDiscussions: { totalCount: stats.totalDiscussionsStarted },
repositoryDiscussionComments: {
totalCount: stats.totalDiscussionsAnswered,
},
repositories: {
totalCount: 1,
nodes: [{ stargazers: { totalCount: 100 } }],
pageInfo: {
hasNextPage: false,
endCursor: "cursor",
},
},
},
},
};
const error = {
errors: [
{
type: "NOT_FOUND",
path: ["user"],
locations: [],
message: "Could not fetch user",
},
],
};
const mock = new MockAdapter(axios);
const faker = (query, data) => {
const req = {
query: {
username: "anuraghazra",
...query,
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
return { req, res };
};
afterEach(() => {
mock.reset();
});
describe("Test /api/", () => {
it("should test the request", async () => {
const { req, res } = faker({}, data_stats);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(renderStatsCard(stats, { ...req.query }));
});
it("should render error card on error", async () => {
const { req, res } = faker({}, error);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError(
error.errors[0].message,
"Make sure the provided username is not an organization",
),
);
});
it("should render error card in same theme as requested card", async () => {
const { req, res } = faker({ theme: "merko" }, error);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError(
error.errors[0].message,
"Make sure the provided username is not an organization",
{ theme: "merko" },
),
);
});
it("should get the query options", async () => {
const { req, res } = faker(
{
username: "anuraghazra",
hide: "issues,prs,contribs",
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
},
data_stats,
);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderStatsCard(stats, {
hide: ["issues", "prs", "contribs"],
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
}),
);
});
it("should set shorter cache when error", async () => {
const { req, res } = faker({}, error);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
CONSTANTS.ERROR_CACHE_SECONDS
}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
],
]);
});
it("should allow changing ring_color", async () => {
const { req, res } = faker(
{
username: "anuraghazra",
hide: "issues,prs,contribs",
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
ring_color: "0000ff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
},
data_stats,
);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderStatsCard(stats, {
hide: ["issues", "prs", "contribs"],
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
ring_color: "0000ff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
}),
);
});
it("should render error card if username in blacklist", async () => {
const { req, res } = faker({ username: "renovate-bot" }, data_stats);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError("Something went wrong", "This username is blacklisted"),
);
});
it("should render error card when wrong locale is provided", async () => {
const { req, res } = faker({ locale: "asdf" }, data_stats);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError("Something went wrong", "Language not found"),
);
});
it("should render error card when include_all_commits true and upstream API fails", async () => {
mock
.onGet("https://api.github.com/search/commits?q=author:anuraghazra")
.reply(200, { error: "Some test error message" });
const { req, res } = faker(
{ username: "anuraghazra", include_all_commits: true },
data_stats,
);
await api(req, res);
expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toBeCalledWith(
renderError("Could not fetch total commits.", "Please try again later"),
);
// Received SVG output should not contain string "https://tiny.one/readme-stats"
expect(res.send.mock.calls[0][0]).not.toContain(
"https://tiny.one/readme-stats",
);
});
});