Repository: anuraghazra/github-readme-stats
Pull request #2424 (2023-01-17 07:17:29)

Vercel maxDuration fix + GH Actions workflow for Vercel deployment
Changed default `maxDuration` to `10` so that users can easily fork and deploy to Vercel.

Also added a GH Actions workflow that changes `maxDuration` to `30` so that @anuraghazra can still have 30 for his deployment.

@anuraghazra after merging you have to change Vercel deployment branch from `master` to `vercel`.

vercel[bot]: @Zo-Bro-23 is attempting to deploy a commit to the **github readme stats** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=github%20readme%20stats&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%2246509045771f75dd03be7b77eca887947814539c%22%7D%2C%22id%22%3A%22QmQ123Hz8nHYnLjES6YYwYfUX97vVhYF6eQBe4e2gEihpk%22%2C%22org%22%3A%22anuraghazra%22%2C%22prId%22%3A2424%2C%22repo%22%3A%22github-readme-stats%22%7D).


rickstaa: @anuraghazra this looks like a high quality PR.👌

Patch:
diff --git a/.github/workflows/deploy-prep.py b/.github/workflows/deploy-prep.py
new file mode 100644
index 0000000..794c19a
--- /dev/null
+++ b/.github/workflows/deploy-prep.py
@@ -0,0 +1,10 @@
+import os
+
+file = open('./vercel.json', 'r')
+str = file.read()
+file = open('./vercel.json', 'w')
+
+str = str.replace('"maxDuration": 10', '"maxDuration": 30')
+
+file.write(str)
+file.close()
diff --git a/.github/workflows/deploy-prep.yml b/.github/workflows/deploy-prep.yml
new file mode 100644
index 0000000..0626e13
--- /dev/null
+++ b/.github/workflows/deploy-prep.yml
@@ -0,0 +1,20 @@
+name: Deployment Prep
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - master
+
+jobs:
+  config:
+    if: github.repository == 'anuraghazra/github-readme-stats'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Deployment Prep
+        run: python ./.github/workflows/deploy-prep.py
+      - uses: stefanzweifel/git-auto-commit-action@v4
+        with:
+          branch: vercel
+          create_branch: true
+          push_options: "--force"
diff --git a/readme.md b/readme.md
index 281c487..678c5c0 100644
--- a/readme.md
+++ b/readme.md
@@ -93,7 +93,7 @@ Visit <https://indiafightscorona.giveindia.org> and make a small donation to hel
     -   [Language Card Exclusive Options](#language-card-exclusive-options)
     -   [Wakatime Card Exclusive Option](#wakatime-card-exclusive-options)
 -   [Deploy Yourself](#deploy-on-your-own-vercel-instance)
-    - [Keep your fork up to date](#keep-your-fork-up-to-date)   
+    -   [Keep your fork up to date](#keep-your-fork-up-to-date)
 
 # GitHub Stats Card
 
@@ -264,7 +264,7 @@ You can customize the appearance of your `Stats Card` or `Repo Card` however you
 -   `border_radius` - Corner rounding on the card. Default: `4.5`.
 
 > **Warning**
-> We use caching to decrease the load on our servers (see https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
+> We use caching to decrease the load on our servers (see <https://github.com/anuraghazra/github-readme-stats/issues/1471#issuecomment-1271551425>). Our cards have a default cache of 4 hours (14400 seconds). Also, note that the cache is clamped to a minimum of 4 hours and a maximum of 24 hours.
 
 ##### Gradient in bg_color
 
@@ -354,7 +354,7 @@ Use [show_owner](#customization) variable to include the repo's owner username
 The top languages card shows a GitHub user's most frequently used top language.
 
 > **Note**
-> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats._
+> Top Languages does not indicate my skill level or anything like that; it's a GitHub metric to determine which languages have the most code on GitHub. It is a new feature of github-readme-stats.
 
 ### Usage
 
@@ -498,7 +498,7 @@ By default, GitHub does not lay out the cards side by side. To do that, you can
 
 ## Deploy on your own Vercel instance
 
-#### [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
+#### :film_projector: [Check Out Step By Step Video Tutorial By @codeSTACKr](https://youtu.be/n6d4KHSKqGk?t=107)
 
 > **Warning**
 > If you are on the [hobby (i.e. free)](https://vercel.com/pricing) Vercel plan, please make sure you change the `maxDuration` parameter in the [vercel.json](https://github.com/anuraghazra/github-readme-stats/blob/master/vercel.json) file from `30` to `10` (see [#1416](https://github.com/anuraghazra/github-readme-stats/issues/1416#issuecomment-950275476) for more information).
diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js
index 7f6cb9e..a7df1e5 100644
--- a/src/fetchers/stats-fetcher.js
+++ b/src/fetchers/stats-fetcher.js
@@ -1,5 +1,6 @@
 // @ts-check
 import axios from "axios";
+import * as dotenv from "dotenv";
 import githubUsernameRegex from "github-username-regex";
 import { calculateRank } from "../calculateRank.js";
 import { retryer } from "../common/retryer.js";
@@ -11,46 +12,74 @@ import {
   wrapTextMultiline,
 } from "../common/utils.js";
 
+dotenv.config();
+
+// GraphQL queries.
+const GRAPHQL_REPOS_FIELD = `
+  repositories(first: 100, ownerAffiliations: OWNER, orderBy: {direction: DESC, field: STARGAZERS}, after: $after) {
+    totalCount
+    nodes {
+      name
+      stargazers {
+        totalCount
+      }
+    }
+    pageInfo {
+      hasNextPage
+      endCursor
+    }
+  }
+`;
+
+const GRAPHQL_REPOS_QUERY = `
+  query userInfo($login: String!, $after: String) {
+    user(login: $login) {
+      ${GRAPHQL_REPOS_FIELD}
+    }
+  }
+`;
+
+const GRAPHQL_STATS_QUERY = `
+  query userInfo($login: String!, $after: String) {
+    user(login: $login) {
+      name
+      login
+      contributionsCollection {
+        totalCommitContributions
+        restrictedContributionsCount
+      }
+      repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
+        totalCount
+      }
+      pullRequests(first: 1) {
+        totalCount
+      }
+      openIssues: issues(states: OPEN) {
+        totalCount
+      }
+      closedIssues: issues(states: CLOSED) {
+        totalCount
+      }
+      followers {
+        totalCount
+      }
+      ${GRAPHQL_REPOS_FIELD}
+    }
+  }
+`;
+
 /**
  * Stats fetcher object.
  *
  * @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
  * @param {string} token GitHub token.
- * @returns {Promise<import('../common/types').StatsFetcherResponse>} Stats fetcher response.
+ * @returns {Promise<import('../common/types').Fetcher>} Stats fetcher response.
  */
 const fetcher = (variables, token) => {
+  const query = !variables.after ? GRAPHQL_STATS_QUERY : GRAPHQL_REPOS_QUERY;
   return request(
     {
-      query: `
-      query userInfo($login: String!) {
-        user(login: $login) {
-          name
-          login
-          contributionsCollection {
-            totalCommitContributions
-            restrictedContributionsCount
-          }
-          repositoriesContributedTo(contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
-            totalCount
-          }
-          pullRequests {
-            totalCount
-          }
-          openIssues: issues(states: OPEN) {
-            totalCount
-          }
-          closedIssues: issues(states: CLOSED) {
-            totalCount
-          }
-          followers {
-            totalCount
-          }
-          repositories(ownerAffiliations: OWNER) {
-            totalCount
-          }
-        }
-      }
-      `,
+      query,
       variables,
     },
     {
@@ -60,39 +89,42 @@ const fetcher = (variables, token) => {
 };
 
 /**
- * Fetch first 100 repositories for a given username.
+ * Fetch stats information for a given username.
  *
- * @param {import('axios').AxiosRequestHeaders} variables Fetcher variables.
- * @param {string} token GitHub token.
- * @returns {Promise<import('../common/types').StatsFetcherResponse>} Repositories fetcher response.
+ * @param {string} username Github username.
+ * @returns {Promise<import('../common/types').StatsFetcher>} GraphQL Stats object.
+ *
+ * @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true.
  */
-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
-            }
-          }
-        }
-      }
-      `,
-      variables,
-    },
-    {
-      Authorization: `bearer ${token}`,
-    },
-  );
+const statsFetcher = async (username) => {
+  let stats;
+  let hasNextPage = true;
+  let endCursor = null;
+  while (hasNextPage) {
+    const variables = { login: username, first: 100, after: endCursor };
+    let res = await retryer(fetcher, variables);
+    if (res.data.errors) return res;
+
+    // Store stats data.
+    const repoNodes = res.data.data.user.repositories.nodes;
+    if (!stats) {
+      stats = res;
+    } else {
+      stats.data.data.user.repositories.nodes.push(...repoNodes);
+    }
+
+    // Disable multi page fetching on public Vercel instance due to rate limits.
+    const repoNodesWithStars = repoNodes.filter(
+      (node) => node.stargazers.totalCount !== 0,
+    );
+    hasNextPage =
+      process.env.FETCH_MULTI_PAGE_STARS === "true" &&
+      repoNodes.length === repoNodesWithStars.length &&
+      res.data.data.user.repositories.pageInfo.hasNextPage;
+    endCursor = res.data.data.user.repositories.pageInfo.endCursor;
+  }
+
+  return stats;
 };
 
 /**
@@ -137,46 +169,6 @@ const totalCommitsFetcher = async (username) => {
   return 0;
 };
 
-/**
- * Fetch all the stars for all the repositories of a given username.
- *
- * @param {string} username GitHub username.
- * @param {array} repoToHide Repositories to hide.
- * @returns {Promise<number>} Total stars.
- */
-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;
-    hasNextPage = false; // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-    endCursor = res.data.data.user.repositories.pageInfo.endCursor;
-  }
-
-  return nodes
-    .filter((data) => !repoToHide[data.name])
-    .reduce((prev, curr) => prev + curr.stargazers.totalCount, 0);
-};
-
 /**
  * Fetch stats for a given username.
  *
@@ -203,7 +195,7 @@ const fetchStats = async (
     rank: { level: "C", score: 0 },
   };
 
-  let res = await retryer(fetcher, { login: username });
+  let res = await statsFetcher(username);
 
   // Catch GraphQL errors.
   if (res.data.errors) {
@@ -259,8 +251,15 @@ const fetchStats = async (
   stats.contributedTo = user.repositoriesContributedTo.totalCount;
 
   // Retrieve stars while filtering out repositories to be hidden
-  stats.totalStars = await totalStarsFetcher(username, repoToHide);
+  stats.totalStars = user.repositories.nodes
+    .filter((data) => {
+      return !repoToHide[data.name];
+    })
+    .reduce((prev, curr) => {
+      return prev + curr.stargazers.totalCount;
+    }, 0);
 
+  // @ts-ignore // TODO: Fix this.
   stats.rank = calculateRank({
     totalCommits: stats.totalCommits,
     totalRepos: user.repositories.totalCount,
diff --git a/vercel.json b/vercel.json
index aee61ce..ddf82eb 100644
--- a/vercel.json
+++ b/vercel.json
@@ -2,7 +2,7 @@
   "functions": {
     "api/*.js": {
       "memory": 128,
-      "maxDuration": 30
+      "maxDuration": 10
     }
   },
   "redirects": [


Test patch:
diff --git a/tests/api.test.js b/tests/api.test.js
index 0037bcd..461f3e1 100644
--- a/tests/api.test.js
+++ b/tests/api.test.js
@@ -25,7 +25,7 @@ stats.rank = calculateRank({
   issues: stats.totalIssues,
 });
 
-const data = {
+const data_stats = {
   data: {
     user: {
       name: stats.name,
@@ -40,15 +40,6 @@ const data = {
       followers: { totalCount: 0 },
       repositories: {
         totalCount: 1,
-      },
-    },
-  },
-};
-
-const repositoriesData = {
-  data: {
-    user: {
-      repositories: {
         nodes: [{ stargazers: { totalCount: 100 } }],
         pageInfo: {
           hasNextPage: false,
@@ -83,11 +74,7 @@ const faker = (query, data) => {
     setHeader: jest.fn(),
     send: jest.fn(),
   };
-  mock
-    .onPost("https://api.github.com/graphql")
-    .replyOnce(200, data)
-    .onPost("https://api.github.com/graphql")
-    .replyOnce(200, repositoriesData);
+  mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
 
   return { req, res };
 };
@@ -98,7 +85,7 @@ afterEach(() => {
 
 describe("Test /api/", () => {
   it("should test the request", async () => {
-    const { req, res } = faker({}, data);
+    const { req, res } = faker({}, data_stats);
 
     await api(req, res);
 
@@ -133,7 +120,7 @@ describe("Test /api/", () => {
         text_color: "fff",
         bg_color: "fff",
       },
-      data,
+      data_stats,
     );
 
     await api(req, res);
@@ -154,7 +141,7 @@ describe("Test /api/", () => {
   });
 
   it("should have proper cache", async () => {
-    const { req, res } = faker({}, data);
+    const { req, res } = faker({}, data_stats);
 
     await api(req, res);
 
@@ -170,7 +157,7 @@ describe("Test /api/", () => {
   });
 
   it("should set proper cache", async () => {
-    const { req, res } = faker({ cache_seconds: 15000 }, data);
+    const { req, res } = faker({ cache_seconds: 15000 }, data_stats);
     await api(req, res);
 
     expect(res.setHeader.mock.calls).toEqual([
@@ -196,7 +183,7 @@ describe("Test /api/", () => {
 
   it("should set proper cache with clamped values", async () => {
     {
-      let { req, res } = faker({ cache_seconds: 200000 }, data);
+      let { req, res } = faker({ cache_seconds: 200000 }, data_stats);
       await api(req, res);
 
       expect(res.setHeader.mock.calls).toEqual([
@@ -212,7 +199,7 @@ describe("Test /api/", () => {
 
     // note i'm using block scoped vars
     {
-      let { req, res } = faker({ cache_seconds: 0 }, data);
+      let { req, res } = faker({ cache_seconds: 0 }, data_stats);
       await api(req, res);
 
       expect(res.setHeader.mock.calls).toEqual([
@@ -227,7 +214,7 @@ describe("Test /api/", () => {
     }
 
     {
-      let { req, res } = faker({ cache_seconds: -10000 }, data);
+      let { req, res } = faker({ cache_seconds: -10000 }, data_stats);
       await api(req, res);
 
       expect(res.setHeader.mock.calls).toEqual([
@@ -248,7 +235,7 @@ describe("Test /api/", () => {
         username: "anuraghazra",
         count_private: true,
       },
-      data,
+      data_stats,
     );
 
     await api(req, res);
@@ -288,7 +275,7 @@ describe("Test /api/", () => {
         text_color: "fff",
         bg_color: "fff",
       },
-      data,
+      data_stats,
     );
 
     await api(req, res);
diff --git a/tests/e2e/e2e.test.js b/tests/e2e/e2e.test.js
index 4d00646..402e210 100644
--- a/tests/e2e/e2e.test.js
+++ b/tests/e2e/e2e.test.js
@@ -62,7 +62,7 @@ const WAKATIME_DATA = {
 };
 
 const REPOSITORY_DATA = {
-  name: "cra-test",
+  name: REPO,
   nameWithOwner: `${USER}/cra-test`,
   isPrivate: false,
   isArchived: false,
diff --git a/tests/fetchStats.test.js b/tests/fetchStats.test.js
index 192146e..04e943a 100644
--- a/tests/fetchStats.test.js
+++ b/tests/fetchStats.test.js
@@ -4,7 +4,8 @@ import MockAdapter from "axios-mock-adapter";
 import { calculateRank } from "../src/calculateRank.js";
 import { fetchStats } from "../src/fetchers/stats-fetcher.js";
 
-const data = {
+// Test parameters.
+const data_stats = {
   data: {
     user: {
       name: "Anurag Hazra",
@@ -19,15 +20,6 @@ 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 } },
@@ -42,7 +34,7 @@ const firstRepositoriesData = {
   },
 };
 
-const secondRepositoriesData = {
+const data_repo = {
   data: {
     user: {
       repositories: {
@@ -59,7 +51,7 @@ const secondRepositoriesData = {
   },
 };
 
-const repositoriesWithZeroStarsData = {
+const data_repo_zero_stars = {
   data: {
     user: {
       repositories: {
@@ -93,13 +85,12 @@ const error = {
 const mock = new MockAdapter(axios);
 
 beforeEach(() => {
+  process.env.FETCH_MULTI_PAGE_STARS = "false"; // Set to `false` to fetch only one page of stars.
   mock
     .onPost("https://api.github.com/graphql")
-    .replyOnce(200, data)
+    .replyOnce(200, data_stats)
     .onPost("https://api.github.com/graphql")
-    .replyOnce(200, firstRepositoriesData);
-  // .onPost("https://api.github.com/graphql") // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-  // .replyOnce(200, secondRepositoriesData); // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+    .replyOnce(200, data_repo);
 });
 
 afterEach(() => {
@@ -114,8 +105,7 @@ describe("Test fetchStats", () => {
       totalRepos: 5,
       followers: 100,
       contributions: 61,
-      // stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      stargazers: 300,
       prs: 300,
       issues: 200,
     });
@@ -126,8 +116,7 @@ describe("Test fetchStats", () => {
       totalCommits: 100,
       totalIssues: 200,
       totalPRs: 300,
-      // totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      totalStars: 300,
       rank,
     });
   });
@@ -136,9 +125,9 @@ describe("Test fetchStats", () => {
     mock.reset();
     mock
       .onPost("https://api.github.com/graphql")
-      .replyOnce(200, data)
+      .replyOnce(200, data_stats)
       .onPost("https://api.github.com/graphql")
-      .replyOnce(200, repositoriesWithZeroStarsData);
+      .replyOnce(200, data_repo_zero_stars);
 
     let stats = await fetchStats("anuraghazra");
     const rank = calculateRank({
@@ -178,8 +167,7 @@ describe("Test fetchStats", () => {
       totalRepos: 5,
       followers: 100,
       contributions: 61,
-      // stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      stargazers: 300,
       prs: 300,
       issues: 200,
     });
@@ -190,8 +178,7 @@ describe("Test fetchStats", () => {
       totalCommits: 150,
       totalIssues: 200,
       totalPRs: 300,
-      // totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      totalStars: 300,
       rank,
     });
   });
@@ -207,8 +194,7 @@ describe("Test fetchStats", () => {
       totalRepos: 5,
       followers: 100,
       contributions: 61,
-      // stargazers: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      stargazers: 300,
       prs: 300,
       issues: 200,
     });
@@ -219,8 +205,7 @@ describe("Test fetchStats", () => {
       totalCommits: 1050,
       totalIssues: 200,
       totalPRs: 300,
-      // totalStars: 400, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      totalStars: 300,
       rank,
     });
   });
@@ -236,8 +221,7 @@ describe("Test fetchStats", () => {
       totalRepos: 5,
       followers: 100,
       contributions: 61,
-      // stargazers: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      stargazers: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      stargazers: 200,
       prs: 300,
       issues: 200,
     });
@@ -248,8 +232,82 @@ describe("Test fetchStats", () => {
       totalCommits: 1050,
       totalIssues: 200,
       totalPRs: 300,
-      // totalStars: 300, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
-      totalStars: 200, // NOTE: Temporarily disable fetching of multiple pages. Done because of #2130.
+      totalStars: 200,
+      rank,
+    });
+  });
+
+  it("should fetch two pages of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `true`", async () => {
+    process.env.FETCH_MULTI_PAGE_STARS = true;
+
+    let stats = await fetchStats("anuraghazra");
+    const rank = calculateRank({
+      totalCommits: 100,
+      totalRepos: 5,
+      followers: 100,
+      contributions: 61,
+      stargazers: 400,
+      prs: 300,
+      issues: 200,
+    });
+
+    expect(stats).toStrictEqual({
+      contributedTo: 61,
+      name: "Anurag Hazra",
+      totalCommits: 100,
+      totalIssues: 200,
+      totalPRs: 300,
+      totalStars: 400,
+      rank,
+    });
+  });
+
+  it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `false`", async () => {
+    process.env.FETCH_MULTI_PAGE_STARS = "false";
+
+    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 fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is not set", async () => {
+    process.env.FETCH_MULTI_PAGE_STARS = undefined;
+
+    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,
     });
   });