Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: pnpm run build:frontend

- name: Run tests
run: pnpm run test --silent
run: pnpm run test

frontend-test-e2e:
name: Frontend E2E test
Expand Down
77 changes: 38 additions & 39 deletions apps/backend/tests/api.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
// @ts-check

import { api, getConfig } from "@stats-organization/github-readme-stats-core";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
api: vi.fn(),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
config: {},
}));
import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { getUserAccessByName, storeRequest } from "../src/common/database.js";

vi.mock("@stats-organization/github-readme-stats-core", async () => {
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
const { mockCore } = await import("./utils.js");
return mockCore({ api: mocks.api, getConfig: () => mocks.config });
return mockCore();
});

vi.mock("../src/common/database.js", () => ({
storeRequest: mocks.storeRequest,
getUserAccessByName: mocks.getUserAccessByName,
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
...(await importOriginal()),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
}));

import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
const apiMock = vi.mocked(api);
const getConfigMock = vi.mocked(getConfig);
const storeRequestMock = vi.mocked(storeRequest);
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);

const createRequest = (search = "") => ({
headers: {},
Expand All @@ -43,18 +44,18 @@ const errorCacheHeader =
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;

beforeEach(() => {
mocks.api.mockReset();
mocks.storeRequest.mockReset().mockResolvedValue(undefined);
mocks.getUserAccessByName.mockReset().mockResolvedValue(null);
mocks.config = {};
apiMock.mockReset();
getConfigMock.mockReset().mockReturnValue({});
storeRequestMock.mockReset().mockResolvedValue(undefined);
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
delete process.env.CACHE_SECONDS;
});

describe("Test /api backend routing", () => {
it("happy path should pass query params and user PAT, respond with stats content and persist request", async () => {
mocks.getUserAccessByName.mockResolvedValue({ token: "user-pat" });
mocks.api.mockResolvedValue({
getUserAccessByNameMock.mockResolvedValue({ token: "user-pat" });
apiMock.mockResolvedValue({
status: "success",
content: "mock-stats-svg",
});
Expand All @@ -66,8 +67,8 @@ describe("Test /api backend routing", () => {

await router(req, res);

expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra");
expect(mocks.api).toHaveBeenCalledWith(
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(apiMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
theme: "dark",
Expand All @@ -85,11 +86,11 @@ describe("Test /api backend routing", () => {
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-stats-svg");
expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req);
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});

it("should use the shorter error cache for temporary stats errors", async () => {
mocks.api.mockResolvedValue({
apiMock.mockResolvedValue({
status: "error - temporary",
content: "temporary-error-svg",
});
Expand All @@ -99,8 +100,8 @@ describe("Test /api backend routing", () => {

await router(req, res);

expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra");
expect(mocks.api).toHaveBeenCalledWith(
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(apiMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
},
Expand All @@ -111,11 +112,11 @@ describe("Test /api backend routing", () => {
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg");
expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req);
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});

it("should not persist permanent stats errors returned by core", async () => {
mocks.api.mockResolvedValue({
apiMock.mockResolvedValue({
status: "error - permanent",
content: "permanent-error-svg",
});
Expand All @@ -125,8 +126,8 @@ describe("Test /api backend routing", () => {

await router(req, res);

expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra");
expect(mocks.api).toHaveBeenCalledWith(
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(apiMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
},
Expand All @@ -137,7 +138,7 @@ describe("Test /api backend routing", () => {
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg");
expect(mocks.storeRequest).not.toHaveBeenCalled();
expect(storeRequestMock).not.toHaveBeenCalled();
});

it("should reject blacklisted usernames before calling core logic", async () => {
Expand All @@ -146,37 +147,35 @@ describe("Test /api backend routing", () => {

await router(req, res);

expect(mocks.api).not.toHaveBeenCalled();
expect(mocks.getUserAccessByName).not.toHaveBeenCalled();
expect(apiMock).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith(
"render-error:This username is blacklisted",
);
expect(mocks.storeRequest).not.toHaveBeenCalled();
expect(storeRequestMock).not.toHaveBeenCalled();
});

it("should reject non-whitelisted usernames before calling core logic", async () => {
mocks.config = {
whitelist: ["allowed-user"],
};
getConfigMock.mockReturnValue({ whitelist: ["allowed-user"] });

const req = createRequest("username=blocked-user");
const res = createResponse();

await router(req, res);

expect(mocks.api).not.toHaveBeenCalled();
expect(mocks.getUserAccessByName).not.toHaveBeenCalled();
expect(apiMock).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith(
"render-error:This username is not whitelisted",
);
expect(mocks.storeRequest).not.toHaveBeenCalled();
expect(storeRequestMock).not.toHaveBeenCalled();
});
});
69 changes: 34 additions & 35 deletions apps/backend/tests/gist.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
// @ts-check

import { getConfig, gist } from "@stats-organization/github-readme-stats-core";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
gist: vi.fn(),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
config: {},
}));
import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { getUserAccessByName, storeRequest } from "../src/common/database.js";

vi.mock("@stats-organization/github-readme-stats-core", async () => {
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
const { mockCore } = await import("./utils.js");
return mockCore({ gist: mocks.gist, getConfig: () => mocks.config });
return mockCore();
});

vi.mock("../src/common/database.js", () => ({
storeRequest: mocks.storeRequest,
getUserAccessByName: mocks.getUserAccessByName,
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
...(await importOriginal()),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
}));

import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
const gistMock = vi.mocked(gist);
const getConfigMock = vi.mocked(getConfig);
const storeRequestMock = vi.mocked(storeRequest);
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);

const createRequest = (search) => ({
headers: {},
Expand All @@ -43,17 +44,17 @@ const errorCacheHeader =
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;

beforeEach(() => {
mocks.gist.mockReset();
mocks.storeRequest.mockReset().mockResolvedValue(undefined);
mocks.getUserAccessByName.mockReset().mockResolvedValue(null);
mocks.config = {};
gistMock.mockReset();
getConfigMock.mockReset().mockReturnValue({});
storeRequestMock.mockReset().mockResolvedValue(undefined);
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
delete process.env.CACHE_SECONDS;
});

describe("Test /api/gist backend routing", () => {
it("happy path should pass query params, respond with gist content and persist request", async () => {
mocks.gist.mockResolvedValue({
gistMock.mockResolvedValue({
status: "success",
content: "mock-gist-svg",
});
Expand All @@ -63,11 +64,11 @@ describe("Test /api/gist backend routing", () => {

await router(req, res);

expect(mocks.gist).toHaveBeenCalledWith({
expect(gistMock).toHaveBeenCalledWith({
id: "bbfce31e0217a3689c8d961a356cb10d",
theme: "dark",
});
expect(mocks.getUserAccessByName).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(req.query).toEqual({
id: "bbfce31e0217a3689c8d961a356cb10d",
theme: "dark",
Expand All @@ -77,11 +78,11 @@ describe("Test /api/gist backend routing", () => {
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-gist-svg");
expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req);
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});

it("should use the shorter error cache for temporary gist errors", async () => {
mocks.gist.mockResolvedValue({
gistMock.mockResolvedValue({
status: "error - temporary",
content: "temporary-error-svg",
});
Expand All @@ -91,20 +92,20 @@ describe("Test /api/gist backend routing", () => {

await router(req, res);

expect(mocks.gist).toHaveBeenCalledWith({
expect(gistMock).toHaveBeenCalledWith({
id: "bbfce31e0217a3689c8d961a356cb10d",
});
expect(mocks.getUserAccessByName).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", errorCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg");
expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req);
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});

it("should not persist permanent gist errors returned by core", async () => {
mocks.gist.mockResolvedValue({
gistMock.mockResolvedValue({
status: "error - permanent",
content: "permanent-error-svg",
});
Expand All @@ -114,37 +115,35 @@ describe("Test /api/gist backend routing", () => {

await router(req, res);

expect(mocks.gist).toHaveBeenCalledWith({
expect(gistMock).toHaveBeenCalledWith({
id: "bbfce31e0217a3689c8d961a356cb10d",
});
expect(mocks.getUserAccessByName).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg");
expect(mocks.storeRequest).not.toHaveBeenCalled();
expect(storeRequestMock).not.toHaveBeenCalled();
});

it("should reject non-whitelisted gist ids before calling core logic", async () => {
mocks.config = {
gistWhitelist: ["allowed-gist-id"],
};
getConfigMock.mockReturnValue({ gistWhitelist: ["allowed-gist-id"] });

const req = createRequest("id=blocked-gist-id");
const res = createResponse();

await router(req, res);

expect(mocks.gist).not.toHaveBeenCalled();
expect(mocks.getUserAccessByName).not.toHaveBeenCalled();
expect(gistMock).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith(
"render-error:This gist ID is not whitelisted",
);
expect(mocks.storeRequest).not.toHaveBeenCalled();
expect(storeRequestMock).not.toHaveBeenCalled();
});
});
19 changes: 18 additions & 1 deletion apps/backend/tests/pat-info.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from "vitest";

const mock = new MockAdapter(axios);

Expand Down Expand Up @@ -63,6 +71,11 @@ beforeAll(async () => {
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"));
});
Expand All @@ -74,6 +87,10 @@ afterEach(() => {
vi.resetModules();
});

afterAll(() => {
vi.restoreAllMocks();
});

describe("Test /api/status/pat-info", () => {
it("should return only 'validPATs' if all PATs are valid", async () => {
mock
Expand Down
Loading