restructure tests, add tests, remove dotenv (#83)

- add a few tests
- re-organize tests to allow testing with different env vars and
creating a combined coverage report
- extract test data into separate files, because importing data from
testA in testB leads to weird jest errors
- remove `dotenv` because it's not being used
- "GitHub Trends" -> "GitHub Stats Extended"
This commit is contained in:
Martin
2026-02-20 19:38:16 +01:00
committed by GitHub
parent 53d0553d3f
commit 6e109e50d1
36 changed files with 727 additions and 307 deletions
-1
View File
@@ -1,4 +1,3 @@
import "dotenv/config";
import express from "express";
import gistCard from "./api-renamed/gist.js";
+2 -7
View File
@@ -3,11 +3,6 @@ export default {
transform: {},
testEnvironment: "jsdom",
coverageProvider: "v8",
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/tests/e2e/"],
modulePathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/tests/e2e/"],
coveragePathIgnorePatterns: [
"<rootDir>/node_modules/",
"<rootDir>/tests/e2e/",
],
testRegex: "(\\.bench)\\.(ts|tsx|js)$",
testMatch: ["<rootDir>/tests/bench/*.bench.{ts,js}"],
setupFiles: ["<rootDir>/tests/setup.jest.js"],
};
+3 -18
View File
@@ -1,22 +1,7 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "jsdom",
coverageProvider: "v8",
setupFiles: ["<rootDir>/tests/setup.jest.js"],
testPathIgnorePatterns: [
"<rootDir>/node_modules/",
"<rootDir>/tests/e2e/",
"<rootDir>/.vercel/",
],
modulePathIgnorePatterns: [
"<rootDir>/node_modules/",
"<rootDir>/tests/e2e/",
"<rootDir>/.vercel/",
],
coveragePathIgnorePatterns: [
"<rootDir>/node_modules/",
"<rootDir>/tests/E2E/",
"<rootDir>/.vercel/",
projects: [
"<rootDir>/jest.public-instance.config.js",
"<rootDir>/jest.private-instance.config.js",
],
};
+1 -1
View File
@@ -3,5 +3,5 @@ export default {
transform: {},
testEnvironment: "node",
coverageProvider: "v8",
testMatch: ["<rootDir>/tests/e2e/**/*.test.js"],
testMatch: ["<rootDir>/tests/e2e/*.test.{ts,js}"],
};
@@ -0,0 +1,13 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "jsdom",
testMatch: [
"<rootDir>/tests/*.test.{ts,js}",
"<rootDir>/tests/private-instance/*.test.{ts,js}",
],
setupFiles: [
"<rootDir>/tests/setup.jest.js",
"<rootDir>/tests/setup.private-instance.jest.js",
],
};
@@ -0,0 +1,10 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "jsdom",
testMatch: [
"<rootDir>/tests/*.test.{ts,js}",
"<rootDir>/tests/public-instance/*.test.{ts,js}",
],
setupFiles: ["<rootDir>/tests/setup.jest.js"],
};
-1
View File
@@ -43,7 +43,6 @@
},
"dependencies": {
"axios": "^1.13.5",
"dotenv": "^17.2.3",
"emoji-name-map": "^2.0.3",
"github-username-regex": "^1.0.0",
"pg": "^8.16.2",
-3
View File
@@ -1,7 +1,6 @@
// @ts-check
import axios from "axios";
import * as dotenv from "dotenv";
import githubUsernameRegex from "github-username-regex";
import { calculateRank } from "../calculateRank.js";
@@ -13,8 +12,6 @@ import { logger } from "../common/log.js";
import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js";
import { retryer } from "../common/retryer.js";
dotenv.config();
// GraphQL queries.
const GRAPHQL_REPOS_FIELD = `
repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {
@@ -0,0 +1,115 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Test /api/wakatime should render error if user data is not accessible 1`] = `
"
<svg
width="495"
height="150"
viewBox="0 0 495 150"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId"></title>
<desc id="descId"></desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="494"
fill="#fffefe"
stroke-opacity="1"
/>
<g
data-testid="card-title"
transform="translate(25, 35)"
>
<g transform="translate(0, 0)">
<text
x="0"
y="0"
class="header"
data-testid="header"
>WakaTime Stats</text>
</g>
</g>
<g
data-testid="main-card-body"
transform="translate(0, 55)"
>
<svg x="0" y="0" width="100%">
<g transform="translate(0, 0)">
<text x="25" y="11" class="stat bold" fill="#434d58">WakaTime user profile not public</text>
</g>
</svg>
</g>
</svg>
"
`;
+1 -64
View File
@@ -17,23 +17,7 @@ import { renderStatsCard } from "../src/cards/stats.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
/**
* @type {import("../src/fetchers/stats").StatsData}
*/
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: { level: "DEV", percentile: 0 },
};
import { data_stats, stats } from "./test-data/api-data.js";
stats.rank = calculateRank({
all_commits: false,
@@ -46,38 +30,6 @@ stats.rank = calculateRank({
followers: 0,
});
const data_stats = {
data: {
user: {
name: stats.name,
repositoriesContributedTo: { totalCount: stats.contributedTo },
commits: {
totalCommitContributions: stats.totalCommits,
},
reviews: {
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: [
{
@@ -357,21 +309,6 @@ describe("Test /api/", () => {
);
});
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).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error card when wrong locale is provided", async () => {
const { req, res } = faker({ locale: "asdf" }, data_stats);
-3
View File
@@ -4,7 +4,6 @@
import { beforeAll, describe, expect, test } from "@jest/globals";
import axios from "axios";
import * as dotenv from "dotenv";
import { renderGistCard } from "../../src/cards/gist.js";
import { renderRepoCard } from "../../src/cards/repo.js";
@@ -12,8 +11,6 @@ import { renderStatsCard } from "../../src/cards/stats.js";
import { renderTopLanguages } from "../../src/cards/top-languages.js";
import { renderWakatimeCard } from "../../src/cards/wakatime.js";
dotenv.config();
const REPO = "curly-fiesta";
const USER = "catelinemnemosyne";
const STATS_CARD_USER = "e2eninja";
+1 -1
View File
@@ -102,7 +102,7 @@ describe("Test fetchGist", () => {
);
});
it("should throw error if reaponse contains them", async () => {
it("should throw error if response contains them", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, gist_errors_data);
await expect(fetchGist("bbfce31e0217a3689c8d961a356cb10d")).rejects.toThrow(
+2 -47
View File
@@ -10,32 +10,7 @@ import { renderGistCard } from "../src/cards/gist.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
const gist_data = {
data: {
viewer: {
gist: {
description:
"List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023",
owner: {
login: "Yizack",
},
stargazerCount: 33,
forks: {
totalCount: 11,
},
files: [
{
name: "countries.json",
language: {
name: "JSON",
},
size: 85858,
},
],
},
},
},
};
import { gist_data } from "./test-data/gist-data.js";
const gist_not_found_data = {
data: {
@@ -114,27 +89,6 @@ describe("Test /api/gist", () => {
);
});
it("should render error if id is not provided", async () => {
const req = {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
await gist(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: 'Missing params "id" make sure you pass the parameters in URL',
secondaryMessage: "/api/gist?id=GIST_ID",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error if gist is not found", async () => {
const req = {
query: {
@@ -168,6 +122,7 @@ describe("Test /api/gist", () => {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
await gist(req, res);
+1 -4
View File
@@ -12,12 +12,9 @@ import {
} from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import * as dotenv from "dotenv";
import patInfo, { RATE_LIMIT_SECONDS } from "../api-renamed/status/pat-info.js";
dotenv.config();
const mock = new MockAdapter(axios);
const successData = {
@@ -73,7 +70,7 @@ afterEach(() => {
describe("Test /api/status/pat-info", () => {
beforeAll(() => {
// reset patenv first so that dotenv doesn't populate them with local envs
// reset patenv first so that they are not populated with local envs
process.env = {};
process.env.PAT_1 = "testPAT1";
process.env.PAT_2 = "testPAT2";
+1 -71
View File
@@ -10,30 +10,7 @@ import { renderRepoCard } from "../src/cards/repo.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
const data_repo = {
repository: {
username: "anuraghazra",
name: "convoychat",
stargazers: {
totalCount: 38000,
},
description: "Help us take over the world! React + TS + GraphQL Chat App",
primaryLanguage: {
color: "#2b7489",
id: "MDg6TGFuZ3VhZ2UyODc=",
name: "TypeScript",
},
forkCount: 100,
isTemplate: false,
},
};
const data_user = {
data: {
user: { repository: data_repo.repository },
organization: null,
},
};
import { data_repo, data_user } from "./test-data/pin-data.js";
const mock = new MockAdapter(axios);
@@ -146,31 +123,6 @@ describe("Test /api/pin", () => {
);
});
it("should render error card if username in blacklist", async () => {
const req = {
query: {
username: "renovate-bot",
repo: "convoychat",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error card if wrong locale provided", async () => {
const req = {
query: {
@@ -196,28 +148,6 @@ describe("Test /api/pin", () => {
);
});
it("should render error card if missing required parameters", async () => {
const req = {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message:
'Missing params "username", "repo" make sure you pass the parameters in URL',
secondaryMessage: "/api/pin?username=USERNAME&amp;repo=REPO_NAME",
renderOptions: { show_repo_link: false },
}),
);
});
it("should have proper cache", async () => {
const req = {
query: {
@@ -0,0 +1,41 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import api from "../../api-renamed/index.js";
import { renderError } from "../../src/common/render.js";
import { data_stats } from "../test-data/api-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/", () => {
it("should render error card if username not in whitelist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,41 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import gist from "../../api-renamed/gist.js";
import { renderError } from "../../src/common/render.js";
import { gist_data } from "../test-data/gist-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/gist with gist whitelist", () => {
it("should render error card if id not in whitelist", async () => {
const req = {
query: {
id: "9bae0392ee3a26bac5cc388a6c8b1469",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
await gist(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This gist ID is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,64 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import pin from "../../api-renamed/pin.js";
import { renderError } from "../../src/common/render.js";
import { data_user } from "../test-data/pin-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/pin", () => {
it("should render error card if username not in whitelist", async () => {
const req = {
query: {
username: "renovate-bot",
repo: "convoychat",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error card if missing required parameters", async () => {
const req = {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,41 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import topLangs from "../../api-renamed/top-langs.js";
import { renderError } from "../../src/common/render.js";
import { data_langs } from "../test-data/langs-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/top-langs", () => {
it("should render error card if username not in whitelist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
await topLangs(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,41 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import api from "../../api-renamed/index.js";
import { renderError } from "../../src/common/render.js";
import { data_stats } from "../test-data/api-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/", () => {
it("should render error card if username in blacklist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,39 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import gist from "../../api-renamed/gist.js";
import { renderError } from "../../src/common/render.js";
import { gist_data } from "../test-data/gist-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/gist", () => {
it("should render error if id is not provided", async () => {
const req = {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
await gist(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: 'Missing params "id" make sure you pass the parameters in URL',
secondaryMessage: "/api/gist?id=GIST_ID",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,65 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import pin from "../../api-renamed/pin.js";
import { renderError } from "../../src/common/render.js";
import { data_user } from "../test-data/pin-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/pin", () => {
it("should render error card if username in blacklist", async () => {
const req = {
query: {
username: "renovate-bot",
repo: "convoychat",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error card if missing required parameters", async () => {
const req = {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message:
'Missing params "username", "repo" make sure you pass the parameters in URL',
secondaryMessage: "/api/pin?username=USERNAME&amp;repo=REPO_NAME",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,41 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import topLangs from "../../api-renamed/top-langs.js";
import { renderError } from "../../src/common/render.js";
import { data_langs } from "../test-data/langs-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/top-langs", () => {
it("should render error card if username in blacklist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
await topLangs(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
+2 -2
View File
@@ -1,7 +1,7 @@
//https://stackoverflow.com/a/68468204/1643179
import { TextDecoder, TextEncoder } from "util";
//https://stackoverflow.com/a/68468204/1643179
Object.assign(global, { TextDecoder, TextEncoder });
process.env.PAT_1 = "dummyPAT1";
process.env.PAT_2 = "dummyPAT2";
@@ -0,0 +1,2 @@
process.env.GIST_WHITELIST = "bbfce31e0217a3689c8d961a356cb10d";
process.env.WHITELIST = "anuraghazra";
+51
View File
@@ -0,0 +1,51 @@
// @ts-check
/**
* @type {import("../../src/fetchers/stats").StatsData}
*/
export 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: { level: "DEV", percentile: 0 },
};
export const data_stats = {
data: {
user: {
name: stats.name,
repositoriesContributedTo: { totalCount: stats.contributedTo },
commits: {
totalCommitContributions: stats.totalCommits,
},
reviews: {
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",
},
},
},
},
};
+28
View File
@@ -0,0 +1,28 @@
// @ts-check
export const gist_data = {
data: {
viewer: {
gist: {
description:
"List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023",
owner: {
login: "Yizack",
},
stargazerCount: 33,
forks: {
totalCount: 11,
},
files: [
{
name: "countries.json",
language: {
name: "JSON",
},
size: 85858,
},
],
},
},
},
};
@@ -0,0 +1,36 @@
// @ts-check
export const data_langs = {
data: {
user: {
repositories: {
nodes: [
{
languages: {
edges: [{ size: 150, node: { color: "#0f0", name: "HTML" } }],
},
},
{
languages: {
edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
},
},
{
languages: {
edges: [
{ size: 100, node: { color: "#0ff", name: "javascript" } },
],
},
},
{
languages: {
edges: [
{ size: 100, node: { color: "#0ff", name: "javascript" } },
],
},
},
],
},
},
},
};
+26
View File
@@ -0,0 +1,26 @@
// @ts-check
export const data_repo = {
repository: {
username: "anuraghazra",
name: "convoychat",
stargazers: {
totalCount: 38000,
},
description: "Help us take over the world! React + TS + GraphQL Chat App",
primaryLanguage: {
color: "#2b7489",
id: "MDg6TGFuZ3VhZ2UyODc=",
name: "TypeScript",
},
forkCount: 100,
isTemplate: false,
},
};
export const data_user = {
data: {
user: { repository: data_repo.repository },
organization: null,
},
};
+1 -58
View File
@@ -10,40 +10,7 @@ import { renderTopLanguages } from "../src/cards/top-languages.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
const data_langs = {
data: {
user: {
repositories: {
nodes: [
{
languages: {
edges: [{ size: 150, node: { color: "#0f0", name: "HTML" } }],
},
},
{
languages: {
edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }],
},
},
{
languages: {
edges: [
{ size: 100, node: { color: "#0ff", name: "javascript" } },
],
},
},
{
languages: {
edges: [
{ size: 100, node: { color: "#0ff", name: "javascript" } },
],
},
},
],
},
},
},
};
import { data_langs } from "./test-data/langs-data.js";
const error = {
errors: [
@@ -175,30 +142,6 @@ describe("Test /api/top-langs", () => {
);
});
it("should render error card if username in blacklist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
await topLangs(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error card if wrong locale provided", async () => {
const req = {
query: {
+47
View File
@@ -6,6 +6,7 @@ import MockAdapter from "axios-mock-adapter";
import wakatime from "../api-renamed/wakatime.js";
import { renderWakatimeCard } from "../src/cards/wakatime.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/index.js";
const wakaTimeData = {
data: {
@@ -99,6 +100,14 @@ const wakaTimeData = {
},
};
const wakaTimeNotFoundData = {
data: {
viewer: {
gist: null,
},
},
};
const mock = new MockAdapter(axios);
afterEach(() => {
@@ -124,6 +133,44 @@ describe("Test /api/wakatime", () => {
);
});
it("should render error if wrong locale is provided", async () => {
const username = "anuraghazra";
const req = { query: { username, locale: "asdf" } };
const res = { setHeader: jest.fn(), send: jest.fn() };
mock
.onGet(
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
)
.reply(200, wakaTimeData);
await wakatime(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
}),
);
});
it("should render error if user data is not accessible", async () => {
const username = "anuraghazra";
const req = { query: { username } };
const res = { setHeader: jest.fn(), send: jest.fn() };
mock
.onGet(
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
)
.reply(200, wakaTimeNotFoundData);
await wakatime(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledTimes(1);
expect(res.send.mock.calls[0][0]).toMatchSnapshot();
});
it("should have proper cache", async () => {
const username = "anuraghazra";
const req = { query: { username } };
-4
View File
@@ -1,4 +0,0 @@
// Safe browser stub for dotenv
export function config(): { parsed: Record<string, never> } {
return { parsed: {} };
}
@@ -52,7 +52,8 @@ export function LoginAccountDeleteModal(
<hr />
<br />
<p>
Are you sure you want to delete your account from GitHub Trends?
Are you sure you want to delete your account from GitHub Stats
Extended?
</p>
<br />
<div className="flex flex-wrap">
-7
View File
@@ -61,13 +61,6 @@ export default defineConfig({
},
resolve: {
alias: [
{
find: "dotenv",
replacement: path.resolve(
import.meta.dirname,
"src/dotenv-browser-stub.ts",
),
},
{
find: "../src/fetchers/wakatime.js",
replacement: path.resolve(
+9 -5
View File
@@ -2,6 +2,14 @@
"$schema": "./node_modules/knip/schema.json",
"workspaces": {
"apps/backend": {
"jest": {
"config": [
"jest.config.js",
"jest.*.config.js",
"package.json" // included by default, see https://knip.dev/reference/plugins/jest
]
},
"entry": [
"api-renamed/*.js",
"express.js",
@@ -17,11 +25,7 @@
]
},
"apps/frontend": {
"entry": [
"src/index.tsx",
"src/dotenv-browser-stub.ts", // referenced by vite.config.ts
"src/wakatime-override.ts"
],
"entry": ["src/index.tsx", "src/wakatime-override.ts"],
"ignoreDependencies": [
// below dependencies are added because backend folder is copied inside frontend folder,
-9
View File
@@ -74,9 +74,6 @@ importers:
axios:
specifier: ^1.13.5
version: 1.13.5
dotenv:
specifier: ^17.2.3
version: 17.2.3
emoji-name-map:
specifier: ^2.0.3
version: 2.0.3
@@ -2096,10 +2093,6 @@ packages:
resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
engines: {node: '>=10'}
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -6406,8 +6399,6 @@ snapshots:
domain-browser@4.22.0: {}
dotenv@17.2.3: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2