Back to Repositories

Testing PAT Validation and Status Reporting in github-readme-stats

This test suite validates the functionality of Personal Access Token (PAT) information handling in the github-readme-stats repository. It focuses on testing various PAT states including valid, expired, exhausted, and error conditions through the pat-info cloud function.

Test Coverage Overview

The test suite provides comprehensive coverage of PAT validation scenarios:
  • Valid PAT verification and rate limit checking
  • Handling of expired and exhausted tokens
  • Error condition management for bad credentials
  • Network error handling and caching behavior
  • Multiple PAT status reporting and detail verification

Implementation Analysis

The testing approach utilizes Jest’s mocking capabilities with axios-mock-adapter to simulate GitHub API responses. The implementation follows a structured pattern of setting up test environments, mocking HTTP responses, and validating response formats and headers.

Key patterns include environment variable management, response structure validation, and comprehensive error scenario coverage.

Technical Details

Testing tools and configuration:
  • Jest as the primary testing framework
  • axios-mock-adapter for API mocking
  • dotenv for environment variable management
  • Mock implementations for request/response objects
  • Structured test data for different API response scenarios

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test environments with beforeAll and afterEach hooks
  • Comprehensive error scenario coverage
  • Detailed assertion checking for response structure
  • Proper header and cache control validation
  • Clean test data setup and environment management

anuraghazra/github-readme-stats

tests/pat-info.test.js

            
/**
 * @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";
import { expect, it, describe, afterEach, beforeAll } from "@jest/globals";

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"],
    ]);
  });
});