feat(github): contar repositorios proprios (publicos e privados) pela API no servidor
Consulta /user/repos com token fine-grained somente leitura de metadados (GITHUB_STATS_TOKEN), pagina, exclui forks e arquivados e devolve null se o token faltar ou a API falhar. Renovacao diaria via revalidate de 86400 s. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
b2f3899dc2
commit
4ba955e2c3
2 files changed
+170
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
countOwnRepos,
|
||||
GITHUB_STATS_REVALIDATE_SECONDS,
|
||||
loadGithubRepoCounts,
|
||||
} 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;
|
||||
}
|
||||
|
||||
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 never calls the API", async () => {
|
||||
const fetchImpl = jest.fn();
|
||||
|
||||
await expect(
|
||||
loadGithubRepoCounts(undefined, fetchImpl),
|
||||
).resolves.toBeNull();
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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)).resolves.toEqual({
|
||||
total: 101,
|
||||
public: 100,
|
||||
private: 1,
|
||||
});
|
||||
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns null when the API rejects the token", async () => {
|
||||
const fetchImpl = jest
|
||||
.fn()
|
||||
.mockResolvedValue(jsonResponse({ message: "Bad credentials" }, 401));
|
||||
|
||||
await expect(loadGithubRepoCounts("bad", fetchImpl)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("returns null on network failure instead of breaking the page", async () => {
|
||||
const fetchImpl = jest.fn().mockRejectedValue(new Error("offline"));
|
||||
|
||||
await expect(loadGithubRepoCounts("token", fetchImpl)).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user