- Same as #143 - Resolves https://github.com/stats-organization/github-stats-extended/pull/143#discussion_r3105155487 - Closes #141
150 lines
4.8 KiB
JavaScript
150 lines
4.8 KiB
JavaScript
// @ts-check
|
|
|
|
import { getConfig, gist } from "@stats-organization/github-readme-stats-core";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import router from "../router.js";
|
|
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
|
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
|
|
|
|
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
|
|
const { mockCore } = await import("./utils.js");
|
|
return mockCore();
|
|
});
|
|
|
|
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
|
|
...(await importOriginal()),
|
|
storeRequest: vi.fn(),
|
|
getUserAccessByName: vi.fn(),
|
|
}));
|
|
|
|
const gistMock = vi.mocked(gist);
|
|
const getConfigMock = vi.mocked(getConfig);
|
|
const storeRequestMock = vi.mocked(storeRequest);
|
|
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
|
|
|
|
const createRequest = (search) => ({
|
|
headers: {},
|
|
url: `/api/gist?${search}`,
|
|
});
|
|
|
|
const createResponse = () => ({
|
|
end: vi.fn(),
|
|
setHeader: vi.fn(),
|
|
});
|
|
|
|
const defaultCacheHeader =
|
|
`max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
|
|
`s-maxage=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
|
|
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
|
|
|
|
const errorCacheHeader =
|
|
`max-age=${CACHE_TTL.ERROR}, ` +
|
|
`s-maxage=${CACHE_TTL.ERROR}, ` +
|
|
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
|
|
|
|
beforeEach(() => {
|
|
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 () => {
|
|
gistMock.mockResolvedValue({
|
|
status: "success",
|
|
content: "mock-gist-svg",
|
|
});
|
|
|
|
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d&theme=dark");
|
|
const res = createResponse();
|
|
|
|
await router(req, res);
|
|
|
|
expect(gistMock).toHaveBeenCalledWith({
|
|
id: "bbfce31e0217a3689c8d961a356cb10d",
|
|
theme: "dark",
|
|
});
|
|
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
|
|
expect(req.query).toEqual({
|
|
id: "bbfce31e0217a3689c8d961a356cb10d",
|
|
theme: "dark",
|
|
});
|
|
expect(res.setHeader.mock.calls).toEqual([
|
|
["Cache-Control", defaultCacheHeader],
|
|
["Content-Type", "image/svg+xml"],
|
|
]);
|
|
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-gist-svg");
|
|
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
|
});
|
|
|
|
it("should use the shorter error cache for temporary gist errors", async () => {
|
|
gistMock.mockResolvedValue({
|
|
status: "error - temporary",
|
|
content: "temporary-error-svg",
|
|
});
|
|
|
|
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d");
|
|
const res = createResponse();
|
|
|
|
await router(req, res);
|
|
|
|
expect(gistMock).toHaveBeenCalledWith({
|
|
id: "bbfce31e0217a3689c8d961a356cb10d",
|
|
});
|
|
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(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
|
});
|
|
|
|
it("should not persist permanent gist errors returned by core", async () => {
|
|
gistMock.mockResolvedValue({
|
|
status: "error - permanent",
|
|
content: "permanent-error-svg",
|
|
});
|
|
|
|
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d");
|
|
const res = createResponse();
|
|
|
|
await router(req, res);
|
|
|
|
expect(gistMock).toHaveBeenCalledWith({
|
|
id: "bbfce31e0217a3689c8d961a356cb10d",
|
|
});
|
|
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(storeRequestMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should reject non-whitelisted gist ids before calling core logic", async () => {
|
|
getConfigMock.mockReturnValue({ gistWhitelist: ["allowed-gist-id"] });
|
|
|
|
const req = createRequest("id=blocked-gist-id");
|
|
const res = createResponse();
|
|
|
|
await router(req, res);
|
|
|
|
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(storeRequestMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|