diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js index b9493adfdbb43..8f8a820013068 100644 --- a/src/fetchers/stats-fetcher.js +++ b/src/fetchers/stats-fetcher.js @@ -29,10 +29,10 @@ const fetcher = (variables, token) => { totalCommitContributions restrictedContributionsCount } - repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { + repositoriesContributedTo(contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) { totalCount } - pullRequests(first: 1) { + pullRequests { totalCount } openIssues: issues(states: OPEN) { @@ -44,14 +44,41 @@ const fetcher = (variables, token) => { followers { totalCount } - repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}) { + repositories(ownerAffiliations: OWNER) { totalCount + } + } + } + `, + variables, + }, + { + Authorization: `bearer ${token}`, + }, + ); +}; + +/** + * @param {import('axios').AxiosRequestHeaders} variables + * @param {string} token + */ +const repositoriesFetcher = (variables, token) => { + return request( + { + query: ` + query userInfo($login: String!, $after: String) { + user(login: $login) { + repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) { nodes { name stargazers { totalCount } } + pageInfo { + hasNextPage + endCursor + } } } } @@ -99,6 +126,43 @@ const totalCommitsFetcher = async (username) => { return 0; }; +/** + * Fetch all the stars for all the repositories of a given username + * @param {string} username + * @param {array} repoToHide + */ +const totalStarsFetcher = async (username, repoToHide) => { + let nodes = []; + let hasNextPage = true; + let endCursor = null; + while (hasNextPage) { + const variables = { login: username, first: 100, after: endCursor }; + let res = await retryer(repositoriesFetcher, variables); + + if (res.data.errors) { + logger.error(res.data.errors); + throw new CustomError( + res.data.errors[0].message || "Could not fetch user", + CustomError.USER_NOT_FOUND, + ); + } + + const allNodes = res.data.data.user.repositories.nodes; + const nodesWithStars = allNodes.filter( + (node) => node.stargazers.totalCount !== 0, + ); + nodes.push(...nodesWithStars); + hasNextPage = + allNodes.length === nodesWithStars.length && + res.data.data.user.repositories.pageInfo.hasNextPage; + endCursor = res.data.data.user.repositories.pageInfo.endCursor; + } + + return nodes + .filter((data) => !repoToHide[data.name]) + .reduce((prev, curr) => prev + curr.stargazers.totalCount, 0); +}; + /** * @param {string} username * @param {boolean} count_private @@ -166,13 +230,7 @@ async function fetchStats( stats.contributedTo = user.repositoriesContributedTo.totalCount; // Retrieve stars while filtering out repositories to be hidden - stats.totalStars = user.repositories.nodes - .filter((data) => { - return !repoToHide[data.name]; - }) - .reduce((prev, curr) => { - return prev + curr.stargazers.totalCount; - }, 0); + stats.totalStars = await totalStarsFetcher(username, repoToHide); stats.rank = calculateRank({ totalCommits: stats.totalCommits, diff --git a/tests/api.test.js b/tests/api.test.js index b0dfc59f17e2e..a6bb0920449e4 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -40,7 +40,20 @@ const data = { followers: { totalCount: 0 }, repositories: { totalCount: 1, + }, + }, + }, +}; + +const repositoriesData = { + data: { + user: { + repositories: { nodes: [{ stargazers: { totalCount: 100 } }], + pageInfo: { + hasNextPage: false, + cursor: "cursor", + }, }, }, }, @@ -70,7 +83,11 @@ const faker = (query, data) => { setHeader: jest.fn(), send: jest.fn(), }; - mock.onPost("https://api.github.com/graphql").reply(200, data); + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, data) + .onPost("https://api.github.com/graphql") + .replyOnce(200, repositoriesData); return { req, res }; }; @@ -138,7 +155,6 @@ describe("Test /api/", () => { it("should have proper cache", async () => { const { req, res } = faker({}, data); - mock.onPost("https://api.github.com/graphql").reply(200, data); await api(req, res); diff --git a/tests/fetchStats.test.js b/tests/fetchStats.test.js index f8eae98139442..9ccdcb2163f6d 100644 --- a/tests/fetchStats.test.js +++ b/tests/fetchStats.test.js @@ -19,13 +19,61 @@ const data = { followers: { totalCount: 100 }, repositories: { totalCount: 5, + }, + }, + }, +}; + +const firstRepositoriesData = { + data: { + user: { + repositories: { nodes: [ { name: "test-repo-1", stargazers: { totalCount: 100 } }, { name: "test-repo-2", stargazers: { totalCount: 100 } }, { name: "test-repo-3", stargazers: { totalCount: 100 } }, + ], + pageInfo: { + hasNextPage: true, + cursor: "cursor", + }, + }, + }, + }, +}; + +const secondRepositoriesData = { + data: { + user: { + repositories: { + nodes: [ { name: "test-repo-4", stargazers: { totalCount: 50 } }, { name: "test-repo-5", stargazers: { totalCount: 50 } }, ], + pageInfo: { + hasNextPage: false, + cursor: "cursor", + }, + }, + }, + }, +}; + +const repositoriesWithZeroStarsData = { + data: { + user: { + repositories: { + nodes: [ + { name: "test-repo-1", stargazers: { totalCount: 100 } }, + { name: "test-repo-2", stargazers: { totalCount: 100 } }, + { name: "test-repo-3", stargazers: { totalCount: 100 } }, + { name: "test-repo-4", stargazers: { totalCount: 0 } }, + { name: "test-repo-5", stargazers: { totalCount: 0 } }, + ], + pageInfo: { + hasNextPage: true, + cursor: "cursor", + }, }, }, }, @@ -44,14 +92,22 @@ const error = { const mock = new MockAdapter(axios); +beforeEach(() => { + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, data) + .onPost("https://api.github.com/graphql") + .replyOnce(200, firstRepositoriesData) + .onPost("https://api.github.com/graphql") + .replyOnce(200, secondRepositoriesData); +}); + afterEach(() => { mock.reset(); }); describe("Test fetchStats", () => { it("should fetch correct stats", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data); - let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ totalCommits: 100, @@ -74,7 +130,38 @@ describe("Test fetchStats", () => { }); }); + it("should stop fetching when there are repos with zero stars", async () => { + mock.reset(); + mock + .onPost("https://api.github.com/graphql") + .replyOnce(200, data) + .onPost("https://api.github.com/graphql") + .replyOnce(200, repositoriesWithZeroStarsData); + + let stats = await fetchStats("anuraghazra"); + const rank = calculateRank({ + totalCommits: 100, + totalRepos: 5, + followers: 100, + contributions: 61, + stargazers: 300, + prs: 300, + issues: 200, + }); + + expect(stats).toStrictEqual({ + contributedTo: 61, + name: "Anurag Hazra", + totalCommits: 100, + totalIssues: 200, + totalPRs: 300, + totalStars: 300, + rank, + }); + }); + it("should throw error", async () => { + mock.reset(); mock.onPost("https://api.github.com/graphql").reply(200, error); await expect(fetchStats("anuraghazra")).rejects.toThrow( @@ -83,8 +170,6 @@ describe("Test fetchStats", () => { }); it("should fetch and add private contributions", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data); - let stats = await fetchStats("anuraghazra", true); const rank = calculateRank({ totalCommits: 150, @@ -108,7 +193,6 @@ describe("Test fetchStats", () => { }); it("should fetch total commits", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data); mock .onGet("https://api.github.com/search/commits?q=author:anuraghazra") .reply(200, { total_count: 1000 }); @@ -136,7 +220,6 @@ describe("Test fetchStats", () => { }); it("should exclude stars of the `test-repo-1` repository", async () => { - mock.onPost("https://api.github.com/graphql").reply(200, data); mock .onGet("https://api.github.com/search/commits?q=author:anuraghazra") .reply(200, { total_count: 1000 });