• api.test.js
  • import { jest } from "@jest/globals";
    import axios from "axios";
    import MockAdapter from "axios-mock-adapter";
    import api from "../api/index.js";
    import { calculateRank } from "../src/calculateRank.js";
    import { renderStatsCard } from "../src/cards/stats-card.js";
    import { CONSTANTS, renderError } from "../src/common/utils.js";
    import { expect, it, describe, afterEach } from "@jest/globals";
    
    const stats = {
      name: "Anurag Hazra",
      totalStars: 100,
      totalCommits: 200,
      totalIssues: 300,
      totalPRs: 400,
      totalPRsMerged: 320,
      mergedPRsPercentage: 80,
      totalReviews: 50,
      totalDiscussionsStarted: 10,
      totalDiscussionsAnswered: 40,
      contributedTo: 50,
      rank: null,
    };
    
    stats.rank = calculateRank({
      all_commits: false,
      commits: stats.totalCommits,
      prs: stats.totalPRs,
      reviews: stats.totalReviews,
      issues: stats.totalIssues,
      repos: 1,
      stars: stats.totalStars,
      followers: 0,
    });
    
    const data_stats = {
      data: {
        user: {
          name: stats.name,
          repositoriesContributedTo: { totalCount: stats.contributedTo },
          contributionsCollection: {
            totalCommitContributions: stats.totalCommits,
            totalPullRequestReviewContributions: stats.totalReviews,
          },
          pullRequests: { totalCount: stats.totalPRs },
          mergedPullRequests: { totalCount: stats.totalPRsMerged },
          openIssues: { totalCount: stats.totalIssues },
          closedIssues: { totalCount: 0 },
          followers: { totalCount: 0 },
          repositoryDiscussions: { totalCount: stats.totalDiscussionsStarted },
          repositoryDiscussionComments: {
            totalCount: stats.totalDiscussionsAnswered,
          },
          repositories: {
            totalCount: 1,
            nodes: [{ stargazers: { totalCount: 100 } }],
            pageInfo: {
              hasNextPage: false,
              endCursor: "cursor",
            },
          },
        },
      },
    };
    
    const error = {
      errors: [
        {
          type: "NOT_FOUND",
          path: ["user"],
          locations: [],
          message: "Could not fetch user",
        },
      ],
    };
    
    const mock = new MockAdapter(axios);
    
    const faker = (query, data) => {
      const req = {
        query: {
          username: "anuraghazra",
          ...query,
        },
      };
      const res = {
        setHeader: jest.fn(),
        send: jest.fn(),
      };
      mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
    
      return { req, res };
    };
    
    afterEach(() => {
      mock.reset();
    });
    
    describe("Test /api/", () => {
      it("should test the request", async () => {
        const { req, res } = faker({}, data_stats);
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(renderStatsCard(stats, { ...req.query }));
      });
    
      it("should render error card on error", async () => {
        const { req, res } = faker({}, error);
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderError(
            error.errors[0].message,
            "Make sure the provided username is not an organization",
          ),
        );
      });
    
      it("should render error card in same theme as requested card", async () => {
        const { req, res } = faker({ theme: "merko" }, error);
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderError(
            error.errors[0].message,
            "Make sure the provided username is not an organization",
            { theme: "merko" },
          ),
        );
      });
    
      it("should get the query options", async () => {
        const { req, res } = faker(
          {
            username: "anuraghazra",
            hide: "issues,prs,contribs",
            show_icons: true,
            hide_border: true,
            line_height: 100,
            title_color: "fff",
            icon_color: "fff",
            text_color: "fff",
            bg_color: "fff",
          },
          data_stats,
        );
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderStatsCard(stats, {
            hide: ["issues", "prs", "contribs"],
            show_icons: true,
            hide_border: true,
            line_height: 100,
            title_color: "fff",
            icon_color: "fff",
            text_color: "fff",
            bg_color: "fff",
          }),
        );
      });
    
      it("should set shorter cache when error", async () => {
        const { req, res } = faker({}, error);
        await api(req, res);
    
        expect(res.setHeader.mock.calls).toEqual([
          ["Content-Type", "image/svg+xml"],
          [
            "Cache-Control",
            `max-age=${CONSTANTS.ERROR_CACHE_SECONDS / 2}, s-maxage=${
              CONSTANTS.ERROR_CACHE_SECONDS
            }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`,
          ],
        ]);
      });
    
      it("should allow changing ring_color", async () => {
        const { req, res } = faker(
          {
            username: "anuraghazra",
            hide: "issues,prs,contribs",
            show_icons: true,
            hide_border: true,
            line_height: 100,
            title_color: "fff",
            ring_color: "0000ff",
            icon_color: "fff",
            text_color: "fff",
            bg_color: "fff",
          },
          data_stats,
        );
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderStatsCard(stats, {
            hide: ["issues", "prs", "contribs"],
            show_icons: true,
            hide_border: true,
            line_height: 100,
            title_color: "fff",
            ring_color: "0000ff",
            icon_color: "fff",
            text_color: "fff",
            bg_color: "fff",
          }),
        );
      });
    
      it("should render error card if username in blacklist", async () => {
        const { req, res } = faker({ username: "renovate-bot" }, data_stats);
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderError("Something went wrong", "This username is blacklisted"),
        );
      });
    
      it("should render error card when wrong locale is provided", async () => {
        const { req, res } = faker({ locale: "asdf" }, data_stats);
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderError("Something went wrong", "Language not found"),
        );
      });
    
      it("should render error card when include_all_commits true and upstream API fails", async () => {
        mock
          .onGet("https://api.github.com/search/commits?q=author:anuraghazra")
          .reply(200, { error: "Some test error message" });
    
        const { req, res } = faker(
          { username: "anuraghazra", include_all_commits: true },
          data_stats,
        );
    
        await api(req, res);
    
        expect(res.setHeader).toBeCalledWith("Content-Type", "image/svg+xml");
        expect(res.send).toBeCalledWith(
          renderError("Could not fetch total commits.", "Please try again later"),
        );
        // Received SVG output should not contain string "https://tiny.one/readme-stats"
        expect(res.send.mock.calls[0][0]).not.toContain(
          "https://tiny.one/readme-stats",
        );
      });
    });