Repository: anuraghazra/github-readme-stats
Pull request #2480 (2023-01-28 12:30:50)

add rose theme

vercel[bot]: @Kminzzi 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%22909fb8dea632de3ea5a1ac779c2bfa20eb840d58%22%7D%2C%22id%22%3A%22QmRZuLkNpAfaF61Ywm3dCJm3TViR1tQgdpvL83iFtarKUp%22%2C%22org%22%3A%22anuraghazra%22%2C%22prId%22%3A2480%2C%22repo%22%3A%22github-readme-stats%22%7D).


github-actions[bot]: 
      # Automated Theme Preview
      
  Hi, thanks for the theme contribution. Please read our theme [contribution guidelines](https://github.com/anuraghazra/github-readme-stats/blob/master/CONTRIBUTING.md#themes-contribution).
  We are currently only accepting color combinations from any VSCode theme or themes with good colour combinations to minimize bloating the themes collection.

  > Also, note that if this theme is exclusively for your personal use, then instead of adding it to our theme collection, you can use card [customization options](https://github.com/anuraghazra/github-readme-stats#customization).

    
      :heavy_check_mark: Theme PR does adhere to our guidelines.
      ## Test results
      - :heavy_check_mark: rose

      **Result:** :heavy_check_mark: All themes are valid.
      
      ## Details
      
        ### Rose theme preview
        
        

        
title_color: <code>#8d192b</code> | icon_color: <code>#B71F36</code> | text_color: <code>#862931</code> | bg_color: <code>#e9d8d4</code> | border_color: <code>#e9d8d4</code>

        [Preview Link](https://github-readme-stats.vercel.app/api?username=anuraghazra&title_color=8d192b&text_color=862931&icon_color=B71F36&border_color=e9d8d4&bg_color=e9d8d4&show_icons=true)

        [![](https://github-readme-stats.vercel.app/api?username=anuraghazra&title_color=8d192b&text_color=862931&icon_color=B71F36&border_color=e9d8d4&bg_color=e9d8d4&show_icons=true)](https://github-readme-stats.vercel.app/api?username=anuraghazra&title_color=8d192b&text_color=862931&icon_color=B71F36&border_color=e9d8d4&bg_color=e9d8d4&show_icons=true)
      
    

Patch:
diff --git a/.github/workflows/update-langs.yaml b/.github/workflows/update-langs.yaml
new file mode 100644
index 0000000..ad6bfb6
--- /dev/null
+++ b/.github/workflows/update-langs.yaml
@@ -0,0 +1,44 @@
+name: Update supported languages
+on:
+  schedule:
+    - cron: "0 0 */30 * *"
+
+jobs:
+  updateLanguages:
+    if: github.repository == 'anuraghazra/github-readme-stats'
+    name: Update supported languages
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [16.x]
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: npm
+
+      - name: Install dependencies
+        run: npm ci
+        env:
+          CI: true
+
+      - name: Run update-languages-json.js script
+        run: npm run generate-langs-json
+
+      - name: Create Pull Request if upstream language file is changed
+        uses: peter-evans/create-pull-request@v4
+        with:
+          commit-message: "refactor: update languages JSON"
+          branch: "update_langs/patch"
+          delete-branch: true
+          title: Update languages JSON
+          body:
+            "The
+            [update-langs](https://github.com/anuraghazra/github-readme-stats/actions/workflows/update-langs.yaml)
+            action found new/updated languages in the [upstream languages JSON
+            file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
+          labels: "ci, lang-card"
diff --git a/api/status/pat-info.js b/api/status/pat-info.js
new file mode 100644
index 0000000..775e068
--- /dev/null
+++ b/api/status/pat-info.js
@@ -0,0 +1,139 @@
+/**
+ * @file Contains a simple cloud function that can be used to check which PATs are no
+ * longer working. It returns a list of valid PATs, expired PATs and PATs with errors.
+ *
+ * @description This function is currently rate limited to 1 request per 10 minutes.
+ */
+
+import { logger, request, dateDiff } from "../../src/common/utils.js";
+export const RATE_LIMIT_SECONDS = 60 * 5; // 1 request per 10 minutes
+
+/**
+ * Simple uptime check fetcher for the PATs.
+ *
+ * @param {import('axios').AxiosRequestHeaders} variables
+ * @param {string} token
+ */
+const uptimeFetcher = (variables, token) => {
+  return request(
+    {
+      query: `
+        query {
+          rateLimit {
+            remaining
+            resetAt
+          },
+        }`,
+      variables,
+    },
+    {
+      Authorization: `bearer ${token}`,
+    },
+  );
+};
+
+const getAllPATs = () => {
+  return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
+};
+
+/**
+ * Check whether any of the PATs is expired.
+ */
+const getPATInfo = async (fetcher, variables) => {
+  const details = {};
+  const PATs = getAllPATs();
+
+  for (const pat of PATs) {
+    try {
+      const response = await fetcher(variables, process.env[pat]);
+      const errors = response.data.errors;
+      const hasErrors = Boolean(errors);
+      const errorType = errors?.[0]?.type;
+      const isRateLimited =
+        (hasErrors && errorType === "RATE_LIMITED") ||
+        response.data.data?.rateLimit?.remaining === 0;
+
+      // Store PATs with errors.
+      if (hasErrors && errorType !== "RATE_LIMITED") {
+        details[pat] = {
+          status: "error",
+          error: {
+            type: errors[0].type,
+            message: errors[0].message,
+          },
+        };
+        continue;
+      } else if (isRateLimited) {
+        const date1 = new Date();
+        const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
+        details[pat] = {
+          status: "exhausted",
+          remaining: 0,
+          resetIn: dateDiff(date2, date1) + " minutes",
+        };
+      } else {
+        details[pat] = {
+          status: "valid",
+          remaining: response.data.data.rateLimit.remaining,
+        };
+      }
+    } catch (err) {
+      // Store the PAT if it is expired.
+      const errorMessage = err.response?.data?.message?.toLowerCase();
+      if (errorMessage === "bad credentials") {
+        details[pat] = {
+          status: "expired",
+        };
+      } else if (errorMessage === "sorry. your account was suspended.") {
+        details[pat] = {
+          status: "suspended",
+        };
+      } else {
+        throw err;
+      }
+    }
+  }
+
+  const filterPATsByStatus = (status) => {
+    return Object.keys(details).filter((pat) => details[pat].status === status);
+  };
+
+  const sortedDetails = Object.keys(details)
+    .sort()
+    .reduce((obj, key) => {
+      obj[key] = details[key];
+      return obj;
+    }, {});
+
+  return {
+    validPATs: filterPATsByStatus("valid"),
+    expiredPATs: filterPATsByStatus("expired"),
+    exhaustedPATs: filterPATsByStatus("exhausted"),
+    suspendedPATs: filterPATsByStatus("suspended"),
+    errorPATs: filterPATsByStatus("error"),
+    details: sortedDetails,
+  };
+};
+
+/**
+ * Cloud function that returns information about the used PATs.
+ */
+export default async (_, res) => {
+  res.setHeader("Content-Type", "application/json");
+  try {
+    // Add header to prevent abuse.
+    const PATsInfo = await getPATInfo(uptimeFetcher, {});
+    if (PATsInfo) {
+      res.setHeader(
+        "Cache-Control",
+        `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
+      );
+    }
+    res.send(JSON.stringify(PATsInfo, null, 2));
+  } catch (err) {
+    // Throw error if something went wrong.
+    logger.error(err);
+    res.setHeader("Cache-Control", "no-store");
+    res.send("Something went wrong: " + err.message);
+  }
+};
diff --git a/api/status/up.js b/api/status/up.js
new file mode 100644
index 0000000..33fe8f9
--- /dev/null
+++ b/api/status/up.js
@@ -0,0 +1,103 @@
+/**
+ * @file Contains a simple cloud function that can be used to check if the PATs are still
+ * functional.
+ *
+ * @description This function is currently rate limited to 1 request per 10 minutes.
+ */
+
+import retryer from "../../src/common/retryer.js";
+import { logger, request } from "../../src/common/utils.js";
+
+export const RATE_LIMIT_SECONDS = 60 * 10; // 1 request per 10 minutes
+
+/**
+ * Simple uptime check fetcher for the PATs.
+ *
+ * @param {import('axios').AxiosRequestHeaders} variables
+ * @param {string} token
+ */
+const uptimeFetcher = (variables, token) => {
+  return request(
+    {
+      query: `
+        query {
+          rateLimit {
+              remaining
+          }
+        }
+        `,
+      variables,
+    },
+    {
+      Authorization: `bearer ${token}`,
+    },
+  );
+};
+
+/**
+ * Creates Json response that can be used for shields.io dynamic card generation.
+ *
+ * @param {*} up Whether the PATs are up or not.
+ * @returns Dynamic shields.io JSON response object.
+ *
+ * @see https://shields.io/endpoint.
+ */
+const shieldsUptimeBadge = (up) => {
+  const schemaVersion = 1;
+  const isError = true;
+  const label = "Public Instance";
+  const message = up ? "up" : "down";
+  const color = up ? "brightgreen" : "red";
+  return {
+    schemaVersion,
+    label,
+    message,
+    color,
+    isError,
+  };
+};
+
+/**
+ * Cloud function that returns whether the PATs are still functional.
+ */
+export default async (req, res) => {
+  let { type } = req.query;
+  type = type ? type.toLowerCase() : "boolean";
+
+  res.setHeader("Content-Type", "application/json");
+
+  try {
+    let PATsValid = true;
+    try {
+      await retryer(uptimeFetcher, {});
+    } catch (err) {
+      PATsValid = false;
+    }
+
+    if (PATsValid) {
+      res.setHeader(
+        "Cache-Control",
+        `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`,
+      );
+    } else {
+      res.setHeader("Cache-Control", "no-store");
+    }
+
+    switch (type) {
+      case "shields":
+        res.send(shieldsUptimeBadge(PATsValid));
+        break;
+      case "json":
+        res.send({ up: PATsValid });
+        break;
+      default:
+        res.send(PATsValid);
+        break;
+    }
+  } catch (err) {
+    // Return fail boolean if something went wrong.
+    logger.error(err);
+    res.setHeader("Cache-Control", "no-store");
+    res.send("Something went wrong: " + err.message);
+  }
+};
diff --git a/package-lock.json b/package-lock.json
index 048c316..ebc7570 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
         "@testing-library/dom": "^8.17.1",
         "@testing-library/jest-dom": "^5.16.5",
         "@uppercod/css-to-object": "^1.1.1",
-        "axios-mock-adapter": "^1.18.1",
+        "axios-mock-adapter": "^1.21.2",
         "color-contrast-checker": "^2.1.0",
         "hjson": "^3.2.2",
         "husky": "^8.0.0",
diff --git a/package.json b/package.json
index 95b1a11..606d4f5 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
     "@testing-library/dom": "^8.17.1",
     "@testing-library/jest-dom": "^5.16.5",
     "@uppercod/css-to-object": "^1.1.1",
-    "axios-mock-adapter": "^1.18.1",
+    "axios-mock-adapter": "^1.21.2",
     "color-contrast-checker": "^2.1.0",
     "hjson": "^3.2.2",
     "husky": "^8.0.0",
diff --git a/src/common/languageColors.json b/src/common/languageColors.json
index 7e8cd55..47bfb1c 100644
--- a/src/common/languageColors.json
+++ b/src/common/languageColors.json
@@ -102,6 +102,7 @@
     "Csound Score": "#1a1a1a",
     "Cuda": "#3A4E3A",
     "Curry": "#531242",
+    "Cypher": "#34c0eb",
     "Cython": "#fedf5b",
     "D": "#ba595e",
     "DM": "#447265",
@@ -124,6 +125,7 @@
     "Earthly": "#2af0ff",
     "Easybuild": "#069406",
     "Ecere Projects": "#913960",
+    "Ecmarkup": "#eb8131",
     "EditorConfig": "#fff1f2",
     "Eiffel": "#4d6977",
     "Elixir": "#6e4a7e",
@@ -215,6 +217,7 @@
     "Idris": "#b30000",
     "Ignore List": "#000000",
     "ImageJ Macro": "#99AAFF",
+    "Imba": "#16cec6",
     "Inno Setup": "#264b99",
     "Io": "#a9188d",
     "Ioke": "#078193",
@@ -286,6 +289,7 @@
     "Mathematica": "#dd1100",
     "Max": "#c4a79c",
     "Mercury": "#ff2b2b",
+    "Mermaid": "#ff3670",
     "Meson": "#007800",
     "Metal": "#8f14e9",
     "MiniYAML": "#ff1111",
@@ -318,6 +322,10 @@
     "Nu": "#c9df40",
     "NumPy": "#9C8AF9",
     "Nunjucks": "#3d8137",
+    "OASv2-json": "#85ea2d",
+    "OASv2-yaml": "#85ea2d",
+    "OASv3-json": "#85ea2d",
+    "OASv3-yaml": "#85ea2d",
     "OCaml": "#3be133",
     "ObjectScript": "#424893",
     "Objective-C": "#438eff",
@@ -327,14 +335,18 @@
     "Omgrofl": "#cabbff",
     "Opal": "#f7ede0",
     "Open Policy Agent": "#7d9199",
+    "OpenAPI Specification v2": "#85ea2d",
+    "OpenAPI Specification v3": "#85ea2d",
     "OpenCL": "#ed2e2d",
     "OpenEdge ABL": "#5ce600",
     "OpenQASM": "#AA70FF",
     "OpenSCAD": "#e5cd45",
+    "Option List": "#476732",
     "Org": "#77aa99",
     "Oxygene": "#cdd0e3",
     "Oz": "#fab738",
     "P4": "#7055b5",
+    "PDDL": "#0d00ff",
     "PEG.js": "#234d6b",
     "PHP": "#4F5D95",
     "PLSQL": "#dad8d8",
@@ -351,6 +363,7 @@
     "PigLatin": "#fcd7de",
     "Pike": "#005390",
     "PogoScript": "#d80074",
+    "Polar": "#ae81ff",
     "Portugol": "#f8bd00",
     "PostCSS": "#dc3a0c",
     "PostScript": "#da291c",
@@ -414,6 +427,7 @@
     "Sass": "#a53b70",
     "Scala": "#c22d40",
     "Scaml": "#bd181a",
+    "Scenic": "#fdc700",
     "Scheme": "#1e4aec",
     "Scilab": "#ca0f21",
     "Self": "#0579aa",
@@ -421,6 +435,7 @@
     "Shell": "#89e051",
     "ShellCheck Config": "#cecfcb",
     "Shen": "#120F14",
+    "Simple File Verification": "#C9BFED",
     "Singularity": "#64E6AD",
     "Slash": "#007eff",
     "Slice": "#003fa2",
@@ -428,6 +443,7 @@
     "SmPL": "#c94949",
     "Smalltalk": "#596706",
     "Smarty": "#f0c040",
+    "Smithy": "#c44536",
     "Solidity": "#AA6746",
     "SourcePawn": "#f69e1d",
     "Squirrel": "#800000",
@@ -478,6 +494,7 @@
     "Vim Script": "#199f4b",
     "Vim Snippet": "#199f4b",
     "Visual Basic .NET": "#945db7",
+    "Visual Basic 6.0": "#2c6353",
     "Volt": "#1F1F1F",
     "Vue": "#41b883",
     "Vyper": "#2980b9",
@@ -514,6 +531,7 @@
     "fish": "#4aae47",
     "hoon": "#00b171",
     "jq": "#c7254e",
+    "just": "#384d54",
     "kvlang": "#1da6e0",
     "mIRC Script": "#3d57c3",
     "mcfunction": "#E22837",
diff --git a/src/common/utils.js b/src/common/utils.js
index 1215fc9..c600c71 100644
--- a/src/common/utils.js
+++ b/src/common/utils.js
@@ -424,6 +424,19 @@ const parseEmojis = (str) => {
   });
 };
 
+/**
+ * Get diff in minutes
+ * @param {Date} d1
+ * @param {Date} d2
+ * @returns {number}
+ */
+const dateDiff = (d1, d2) => {
+  const date1 = new Date(d1);
+  const date2 = new Date(d2);
+  const diff = date1.getTime() - date2.getTime();
+  return Math.round(diff / (1000 * 60));
+};
+
 export {
   ERROR_CARD_LENGTH,
   renderError,
@@ -447,4 +460,5 @@ export {
   lowercaseTrim,
   chunkArray,
   parseEmojis,
+  dateDiff,
 };
diff --git a/themes/index.js b/themes/index.js
index a5d3aba..60825d1 100644
--- a/themes/index.js
+++ b/themes/index.js
@@ -374,6 +374,13 @@ export const themes = {
     border_color: "170F0C",
     bg_color: "170F0C",
   },
+  rose: {
+    title_color: "8d192b",
+    text_color: "862931",
+    icon_color: "B71F36",
+    border_color: "e9d8d4",
+    bg_color: "e9d8d4",
+  },
 };
 
 export default themes;


Test patch:
diff --git a/tests/pat-info.test.js b/tests/pat-info.test.js
new file mode 100644
index 0000000..23aca8c
--- /dev/null
+++ b/tests/pat-info.test.js
@@ -0,0 +1,244 @@
+/**
+ * @file Tests for the status/pat-info cloud function.
+ */
+import dotenv from "dotenv";
+dotenv.config();
+
+import { jest } from "@jest/globals";
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import patInfo, { RATE_LIMIT_SECONDS } from "../api/status/pat-info.js";
+
+const mock = new MockAdapter(axios);
+
+const successData = {
+  data: {
+    rateLimit: {
+      remaining: 4986,
+    },
+  },
+};
+
+const faker = (query) => {
+  const req = {
+    query: { ...query },
+  };
+  const res = {
+    setHeader: jest.fn(),
+    send: jest.fn(),
+  };
+
+  return { req, res };
+};
+
+const rate_limit_error = {
+  errors: [
+    {
+      type: "RATE_LIMITED",
+      message: "API rate limit exceeded for user ID.",
+    },
+  ],
+  data: {
+    rateLimit: {
+      resetAt: Date.now(),
+    },
+  },
+};
+
+const other_error = {
+  errors: [
+    {
+      type: "SOME_ERROR",
+      message: "This is a error",
+    },
+  ],
+};
+
+const bad_credentials_error = {
+  message: "Bad credentials",
+};
+
+afterEach(() => {
+  mock.reset();
+});
+
+describe("Test /api/status/pat-info", () => {
+  beforeAll(() => {
+    // reset patenv first so that dotenv doesn't populate them with local envs
+    process.env = {};
+    process.env.PAT_1 = "testPAT1";
+    process.env.PAT_2 = "testPAT2";
+    process.env.PAT_3 = "testPAT3";
+    process.env.PAT_4 = "testPAT4";
+  });
+
+  it("should return only 'validPATs' if all PATs are valid", async () => {
+    mock
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(200, rate_limit_error)
+      .onPost("https://api.github.com/graphql")
+      .reply(200, successData);
+
+    const { req, res } = faker({}, {});
+    await patInfo(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(
+      JSON.stringify(
+        {
+          validPATs: ["PAT_2", "PAT_3", "PAT_4"],
+          expiredPATs: [],
+          exhaustedPATs: ["PAT_1"],
+          suspendedPATs: [],
+          errorPATs: [],
+          details: {
+            PAT_1: {
+              status: "exhausted",
+              remaining: 0,
+              resetIn: "0 minutes",
+            },
+            PAT_2: {
+              status: "valid",
+              remaining: 4986,
+            },
+            PAT_3: {
+              status: "valid",
+              remaining: 4986,
+            },
+            PAT_4: {
+              status: "valid",
+              remaining: 4986,
+            },
+          },
+        },
+        null,
+        2,
+      ),
+    );
+  });
+
+  it("should return `errorPATs` if a PAT causes an error to be thrown", async () => {
+    mock
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(200, other_error)
+      .onPost("https://api.github.com/graphql")
+      .reply(200, successData);
+
+    const { req, res } = faker({}, {});
+    await patInfo(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(
+      JSON.stringify(
+        {
+          validPATs: ["PAT_2", "PAT_3", "PAT_4"],
+          expiredPATs: [],
+          exhaustedPATs: [],
+          suspendedPATs: [],
+          errorPATs: ["PAT_1"],
+          details: {
+            PAT_1: {
+              status: "error",
+              error: {
+                type: "SOME_ERROR",
+                message: "This is a error",
+              },
+            },
+            PAT_2: {
+              status: "valid",
+              remaining: 4986,
+            },
+            PAT_3: {
+              status: "valid",
+              remaining: 4986,
+            },
+            PAT_4: {
+              status: "valid",
+              remaining: 4986,
+            },
+          },
+        },
+        null,
+        2,
+      ),
+    );
+  });
+
+  it("should return `expiredPaths` if a PAT returns a 'Bad credentials' error", async () => {
+    mock
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(404, bad_credentials_error)
+      .onPost("https://api.github.com/graphql")
+      .reply(200, successData);
+
+    const { req, res } = faker({}, {});
+    await patInfo(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(
+      JSON.stringify(
+        {
+          validPATs: ["PAT_2", "PAT_3", "PAT_4"],
+          expiredPATs: ["PAT_1"],
+          exhaustedPATs: [],
+          suspendedPATs: [],
+          errorPATs: [],
+          details: {
+            PAT_1: {
+              status: "expired",
+            },
+            PAT_2: {
+              status: "valid",
+              remaining: 4986,
+            },
+            PAT_3: {
+              status: "valid",
+              remaining: 4986,
+            },
+            PAT_4: {
+              status: "valid",
+              remaining: 4986,
+            },
+          },
+        },
+        null,
+        2,
+      ),
+    );
+  });
+
+  it("should throw an error if something goes wrong", async () => {
+    mock.onPost("https://api.github.com/graphql").networkError();
+
+    const { req, res } = faker({}, {});
+    await patInfo(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith("Something went wrong: Network Error");
+  });
+
+  it("should have proper cache when no error is thrown", async () => {
+    mock.onPost("https://api.github.com/graphql").reply(200, successData);
+
+    const { req, res } = faker({}, {});
+    await patInfo(req, res);
+
+    expect(res.setHeader.mock.calls).toEqual([
+      ["Content-Type", "application/json"],
+      ["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`],
+    ]);
+  });
+
+  it("should have proper cache when error is thrown", async () => {
+    mock.reset();
+    mock.onPost("https://api.github.com/graphql").networkError();
+
+    const { req, res } = faker({}, {});
+    await patInfo(req, res);
+
+    expect(res.setHeader.mock.calls).toEqual([
+      ["Content-Type", "application/json"],
+      ["Cache-Control", "no-store"],
+    ]);
+  });
+});
diff --git a/tests/status.up.test.js b/tests/status.up.test.js
new file mode 100644
index 0000000..7cf0144
--- /dev/null
+++ b/tests/status.up.test.js
@@ -0,0 +1,194 @@
+/**
+ * @file Tests for the status/up cloud function.
+ */
+import { jest } from "@jest/globals";
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import up, { RATE_LIMIT_SECONDS } from "../api/status/up.js";
+
+const mock = new MockAdapter(axios);
+
+const successData = {
+  rateLimit: {
+    remaining: 4986,
+  },
+};
+
+const faker = (query) => {
+  const req = {
+    query: { ...query },
+  };
+  const res = {
+    setHeader: jest.fn(),
+    send: jest.fn(),
+  };
+
+  return { req, res };
+};
+
+const rate_limit_error = {
+  errors: [
+    {
+      type: "RATE_LIMITED",
+    },
+  ],
+};
+
+const bad_credentials_error = {
+  message: "Bad credentials",
+};
+
+const shields_up = {
+  schemaVersion: 1,
+  label: "Public Instance",
+  isError: true,
+  message: "up",
+  color: "brightgreen",
+};
+const shields_down = {
+  schemaVersion: 1,
+  label: "Public Instance",
+  isError: true,
+  message: "down",
+  color: "red",
+};
+
+afterEach(() => {
+  mock.reset();
+});
+
+describe("Test /api/status/up", () => {
+  it("should return `true` if request was successful", async () => {
+    mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(true);
+  });
+
+  it("should return `false` if all PATs are rate limited", async () => {
+    mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(false);
+  });
+
+  it("should return JSON `true` if request was successful and type='json'", async () => {
+    mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
+
+    const { req, res } = faker({ type: "json" }, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith({ up: true });
+  });
+
+  it("should return JSON `false` if all PATs are rate limited and type='json'", async () => {
+    mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
+
+    const { req, res } = faker({ type: "json" }, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith({ up: false });
+  });
+
+  it("should return UP shields.io config if request was successful and type='shields'", async () => {
+    mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
+
+    const { req, res } = faker({ type: "shields" }, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(shields_up);
+  });
+
+  it("should return DOWN shields.io config if all PATs are rate limited and type='shields'", async () => {
+    mock.onPost("https://api.github.com/graphql").reply(200, rate_limit_error);
+
+    const { req, res } = faker({ type: "shields" }, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(shields_down);
+  });
+
+  it("should return `true` if the first PAT is rate limited but the second PATs works", async () => {
+    mock
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(200, rate_limit_error)
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(200, successData);
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(true);
+  });
+
+  it("should return `true` if the first PAT has 'Bad credentials' but the second PAT works", async () => {
+    mock
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(404, bad_credentials_error)
+      .onPost("https://api.github.com/graphql")
+      .replyOnce(200, successData);
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(true);
+  });
+
+  it("should return `false` if all pats have 'Bad credentials'", async () => {
+    mock
+      .onPost("https://api.github.com/graphql")
+      .reply(404, bad_credentials_error);
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(false);
+  });
+
+  it("should throw an error if the request fails", async () => {
+    mock.onPost("https://api.github.com/graphql").networkError();
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader).toBeCalledWith("Content-Type", "application/json");
+    expect(res.send).toBeCalledWith(false);
+  });
+
+  it("should have proper cache when no error is thrown", async () => {
+    mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader.mock.calls).toEqual([
+      ["Content-Type", "application/json"],
+      ["Cache-Control", `max-age=0, s-maxage=${RATE_LIMIT_SECONDS}`],
+    ]);
+  });
+
+  it("should have proper cache when error is thrown", async () => {
+    mock.onPost("https://api.github.com/graphql").networkError();
+
+    const { req, res } = faker({}, {});
+    await up(req, res);
+
+    expect(res.setHeader.mock.calls).toEqual([
+      ["Content-Type", "application/json"],
+      ["Cache-Control", "no-store"],
+    ]);
+  });
+});