/** * @file Tests for the status/pat-info cloud function. */ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi, } from "vitest"; const mock = new MockAdapter(axios); const successData = { data: { rateLimit: { remaining: 4986, }, }, }; const faker = (query) => { const req = { query: { ...query }, }; const res = { setHeader: vi.fn(), send: vi.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", }; let RATE_LIMIT_SECONDS, patInfo; beforeAll(async () => { vi.stubEnv("PAT_1", "testPAT1"); vi.stubEnv("PAT_2", "testPAT2"); vi.stubEnv("PAT_3", "testPAT3"); vi.stubEnv("PAT_4", "testPAT4"); const { logger } = await import("@stats-organization/github-readme-stats-core"); vi.spyOn(logger, "log").mockImplementation(() => {}); vi.spyOn(logger, "error").mockImplementation(() => {}); ({ RATE_LIMIT_SECONDS, default: patInfo } = await import("../api-renamed/status/pat-info.js")); }); afterEach(() => { mock.reset(); vi.unstubAllEnvs(); // modules may cache environment variables, so we need to reset them vi.resetModules(); }); afterAll(() => { vi.restoreAllMocks(); }); describe("Test /api/status/pat-info", () => { 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).toHaveBeenCalledWith( "Content-Type", "application/json", ); expect(res.send).toHaveBeenCalledWith( 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).toHaveBeenCalledWith( "Content-Type", "application/json", ); expect(res.send).toHaveBeenCalledWith( 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).toHaveBeenCalledWith( "Content-Type", "application/json", ); expect(res.send).toHaveBeenCalledWith( 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).toHaveBeenCalledWith( "Content-Type", "application/json", ); expect(res.send).toHaveBeenCalledWith( "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.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"], ]); }); });