data/github-stats.json guarda 68 repositorios proprios (25 publicos e 43 privados, sem forks) conferidos em 2026-10-08. Com token valido a contagem ao vivo vale mais; o retrato evita que o hero fique sem o numero. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
import {
|
|
countOwnRepos,
|
|
GITHUB_STATS_REVALIDATE_SECONDS,
|
|
loadGithubRepoCounts,
|
|
loadGithubStatsSnapshot,
|
|
} from "./github-stats";
|
|
|
|
const repo = (
|
|
overrides: Partial<{ private: boolean; fork: boolean; archived: boolean }>,
|
|
) => ({ private: false, fork: false, archived: false, ...overrides });
|
|
|
|
function jsonResponse(body: unknown, status = 200) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
json: async () => body,
|
|
} as unknown as Response;
|
|
}
|
|
|
|
const noSnapshot = async () => null;
|
|
const snapshot = { total: 68, public: 25, private: 43 };
|
|
|
|
describe("countOwnRepos", () => {
|
|
it("counts public and private repos and ignores forks and archived ones", () => {
|
|
const counts = countOwnRepos([
|
|
repo({}),
|
|
repo({}),
|
|
repo({ private: true }),
|
|
repo({ fork: true }),
|
|
repo({ private: true, fork: true }),
|
|
repo({ archived: true }),
|
|
repo({ private: true, archived: true }),
|
|
]);
|
|
|
|
expect(counts).toEqual({ total: 3, public: 2, private: 1 });
|
|
});
|
|
});
|
|
|
|
describe("loadGithubRepoCounts", () => {
|
|
it("returns null without a token and without a snapshot, and never calls the API", async () => {
|
|
const fetchImpl = jest.fn();
|
|
|
|
await expect(
|
|
loadGithubRepoCounts(undefined, fetchImpl, noSnapshot),
|
|
).resolves.toBeNull();
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("falls back to the last known snapshot without a token", async () => {
|
|
const fetchImpl = jest.fn();
|
|
|
|
await expect(
|
|
loadGithubRepoCounts(undefined, fetchImpl, async () => snapshot),
|
|
).resolves.toEqual(snapshot);
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("prefers the live count over the snapshot", async () => {
|
|
const fetchImpl = jest
|
|
.fn()
|
|
.mockResolvedValue(jsonResponse([repo({}), repo({ private: true })]));
|
|
|
|
await expect(
|
|
loadGithubRepoCounts("token", fetchImpl, async () => snapshot),
|
|
).resolves.toEqual({ total: 2, public: 1, private: 1 });
|
|
});
|
|
|
|
it("sends the token only to the GitHub API and revalidates daily", async () => {
|
|
const fetchImpl = jest.fn().mockResolvedValue(jsonResponse([repo({})]));
|
|
|
|
await loadGithubRepoCounts("secret-token", fetchImpl, noSnapshot);
|
|
|
|
const [url, init] = fetchImpl.mock.calls[0];
|
|
expect(new URL(url).origin).toBe("https://api.github.com");
|
|
expect(url).toContain("affiliation=owner");
|
|
expect(url).toContain("visibility=all");
|
|
expect(init.headers.authorization).toBe("Bearer secret-token");
|
|
expect(init.next.revalidate).toBe(GITHUB_STATS_REVALIDATE_SECONDS);
|
|
expect(GITHUB_STATS_REVALIDATE_SECONDS).toBe(86_400);
|
|
});
|
|
|
|
it("walks every page before counting", async () => {
|
|
const firstPage = Array.from({ length: 100 }, () => repo({}));
|
|
const secondPage = [repo({ private: true }), repo({ fork: true })];
|
|
const fetchImpl = jest
|
|
.fn()
|
|
.mockResolvedValueOnce(jsonResponse(firstPage))
|
|
.mockResolvedValueOnce(jsonResponse(secondPage));
|
|
|
|
await expect(
|
|
loadGithubRepoCounts("token", fetchImpl, noSnapshot),
|
|
).resolves.toEqual({
|
|
total: 101,
|
|
public: 100,
|
|
private: 1,
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("uses the snapshot when the API rejects the token", async () => {
|
|
const fetchImpl = jest
|
|
.fn()
|
|
.mockResolvedValue(jsonResponse({ message: "Bad credentials" }, 401));
|
|
|
|
await expect(
|
|
loadGithubRepoCounts("bad", fetchImpl, async () => snapshot),
|
|
).resolves.toEqual(snapshot);
|
|
});
|
|
|
|
it("uses the snapshot on network failure instead of breaking the page", async () => {
|
|
const fetchImpl = jest.fn().mockRejectedValue(new Error("offline"));
|
|
|
|
await expect(
|
|
loadGithubRepoCounts("token", fetchImpl, async () => snapshot),
|
|
).resolves.toEqual(snapshot);
|
|
});
|
|
});
|
|
|
|
describe("loadGithubStatsSnapshot", () => {
|
|
it("reads a coherent snapshot from data/github-stats.json", async () => {
|
|
const counts = await loadGithubStatsSnapshot();
|
|
|
|
expect(counts).not.toBeNull();
|
|
expect(counts?.total).toBe((counts?.public ?? 0) + (counts?.private ?? 0));
|
|
});
|
|
});
|