From 6e109e50d1492b01658f9187f6c5d435f59b8568 Mon Sep 17 00:00:00 2001 From: Martin <2026226+martin-mfg@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:38:16 +0100 Subject: [PATCH] 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" --- apps/backend/express.js | 1 - apps/backend/jest.bench.config.js | 9 +- apps/backend/jest.config.js | 21 +--- apps/backend/jest.e2e.config.js | 2 +- apps/backend/jest.private-instance.config.js | 13 ++ apps/backend/jest.public-instance.config.js | 10 ++ apps/backend/package.json | 1 - apps/backend/src/fetchers/stats.js | 3 - .../tests/__snapshots__/wakatime.test.js.snap | 115 ++++++++++++++++++ apps/backend/tests/api.test.js | 65 +--------- apps/backend/tests/e2e/e2e.test.js | 3 - apps/backend/tests/fetchGist.test.js | 2 +- apps/backend/tests/gist.test.js | 49 +------- apps/backend/tests/pat-info.test.js | 5 +- apps/backend/tests/pin.test.js | 72 +---------- .../tests/private-instance/api.test.js | 41 +++++++ .../tests/private-instance/gist.test.js | 41 +++++++ .../tests/private-instance/pin.test.js | 64 ++++++++++ .../tests/private-instance/top-langs.test.js | 41 +++++++ .../backend/tests/public-instance/api.test.js | 41 +++++++ .../tests/public-instance/gist.test.js | 39 ++++++ .../backend/tests/public-instance/pin.test.js | 65 ++++++++++ .../tests/public-instance/top-langs.test.js | 41 +++++++ apps/backend/tests/setup.jest.js | 4 +- .../tests/setup.private-instance.jest.js | 2 + apps/backend/tests/test-data/api-data.js | 51 ++++++++ apps/backend/tests/test-data/gist-data.js | 28 +++++ apps/backend/tests/test-data/langs-data.js | 36 ++++++ apps/backend/tests/test-data/pin-data.js | 26 ++++ apps/backend/tests/top-langs.test.js | 59 +-------- apps/backend/tests/wakatime.test.js | 47 +++++++ apps/frontend/src/dotenv-browser-stub.ts | 4 - .../stages/Login/LoginAccountDeleteModal.tsx | 3 +- apps/frontend/vite.config.ts | 7 -- knip.jsonc | 14 ++- pnpm-lock.yaml | 9 -- 36 files changed, 727 insertions(+), 307 deletions(-) create mode 100644 apps/backend/jest.private-instance.config.js create mode 100644 apps/backend/jest.public-instance.config.js create mode 100644 apps/backend/tests/__snapshots__/wakatime.test.js.snap create mode 100644 apps/backend/tests/private-instance/api.test.js create mode 100644 apps/backend/tests/private-instance/gist.test.js create mode 100644 apps/backend/tests/private-instance/pin.test.js create mode 100644 apps/backend/tests/private-instance/top-langs.test.js create mode 100644 apps/backend/tests/public-instance/api.test.js create mode 100644 apps/backend/tests/public-instance/gist.test.js create mode 100644 apps/backend/tests/public-instance/pin.test.js create mode 100644 apps/backend/tests/public-instance/top-langs.test.js create mode 100644 apps/backend/tests/setup.private-instance.jest.js create mode 100644 apps/backend/tests/test-data/api-data.js create mode 100644 apps/backend/tests/test-data/gist-data.js create mode 100644 apps/backend/tests/test-data/langs-data.js create mode 100644 apps/backend/tests/test-data/pin-data.js delete mode 100644 apps/frontend/src/dotenv-browser-stub.ts diff --git a/apps/backend/express.js b/apps/backend/express.js index 0e020275..35a24a36 100644 --- a/apps/backend/express.js +++ b/apps/backend/express.js @@ -1,4 +1,3 @@ -import "dotenv/config"; import express from "express"; import gistCard from "./api-renamed/gist.js"; diff --git a/apps/backend/jest.bench.config.js b/apps/backend/jest.bench.config.js index 880f918d..ad77a217 100644 --- a/apps/backend/jest.bench.config.js +++ b/apps/backend/jest.bench.config.js @@ -3,11 +3,6 @@ export default { transform: {}, testEnvironment: "jsdom", coverageProvider: "v8", - testPathIgnorePatterns: ["/node_modules/", "/tests/e2e/"], - modulePathIgnorePatterns: ["/node_modules/", "/tests/e2e/"], - coveragePathIgnorePatterns: [ - "/node_modules/", - "/tests/e2e/", - ], - testRegex: "(\\.bench)\\.(ts|tsx|js)$", + testMatch: ["/tests/bench/*.bench.{ts,js}"], + setupFiles: ["/tests/setup.jest.js"], }; diff --git a/apps/backend/jest.config.js b/apps/backend/jest.config.js index 4977ac60..64980e06 100644 --- a/apps/backend/jest.config.js +++ b/apps/backend/jest.config.js @@ -1,22 +1,7 @@ export default { - clearMocks: true, - transform: {}, - testEnvironment: "jsdom", coverageProvider: "v8", - setupFiles: ["/tests/setup.jest.js"], - testPathIgnorePatterns: [ - "/node_modules/", - "/tests/e2e/", - "/.vercel/", - ], - modulePathIgnorePatterns: [ - "/node_modules/", - "/tests/e2e/", - "/.vercel/", - ], - coveragePathIgnorePatterns: [ - "/node_modules/", - "/tests/E2E/", - "/.vercel/", + projects: [ + "/jest.public-instance.config.js", + "/jest.private-instance.config.js", ], }; diff --git a/apps/backend/jest.e2e.config.js b/apps/backend/jest.e2e.config.js index 656ab61f..8ac68f9b 100644 --- a/apps/backend/jest.e2e.config.js +++ b/apps/backend/jest.e2e.config.js @@ -3,5 +3,5 @@ export default { transform: {}, testEnvironment: "node", coverageProvider: "v8", - testMatch: ["/tests/e2e/**/*.test.js"], + testMatch: ["/tests/e2e/*.test.{ts,js}"], }; diff --git a/apps/backend/jest.private-instance.config.js b/apps/backend/jest.private-instance.config.js new file mode 100644 index 00000000..cd7e421a --- /dev/null +++ b/apps/backend/jest.private-instance.config.js @@ -0,0 +1,13 @@ +export default { + clearMocks: true, + transform: {}, + testEnvironment: "jsdom", + testMatch: [ + "/tests/*.test.{ts,js}", + "/tests/private-instance/*.test.{ts,js}", + ], + setupFiles: [ + "/tests/setup.jest.js", + "/tests/setup.private-instance.jest.js", + ], +}; diff --git a/apps/backend/jest.public-instance.config.js b/apps/backend/jest.public-instance.config.js new file mode 100644 index 00000000..11bd6944 --- /dev/null +++ b/apps/backend/jest.public-instance.config.js @@ -0,0 +1,10 @@ +export default { + clearMocks: true, + transform: {}, + testEnvironment: "jsdom", + testMatch: [ + "/tests/*.test.{ts,js}", + "/tests/public-instance/*.test.{ts,js}", + ], + setupFiles: ["/tests/setup.jest.js"], +}; diff --git a/apps/backend/package.json b/apps/backend/package.json index 487fa5ec..f0d29791 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -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", diff --git a/apps/backend/src/fetchers/stats.js b/apps/backend/src/fetchers/stats.js index be7c1224..f4a6be86 100644 --- a/apps/backend/src/fetchers/stats.js +++ b/apps/backend/src/fetchers/stats.js @@ -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}) { diff --git a/apps/backend/tests/__snapshots__/wakatime.test.js.snap b/apps/backend/tests/__snapshots__/wakatime.test.js.snap new file mode 100644 index 00000000..d12072f9 --- /dev/null +++ b/apps/backend/tests/__snapshots__/wakatime.test.js.snap @@ -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`] = ` +" + + + + + + + + + + + + + WakaTime Stats + + + + + + + + + WakaTime user profile not public + + + + + + " +`; diff --git a/apps/backend/tests/api.test.js b/apps/backend/tests/api.test.js index 8ec1f28b..7d0247f0 100644 --- a/apps/backend/tests/api.test.js +++ b/apps/backend/tests/api.test.js @@ -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); diff --git a/apps/backend/tests/e2e/e2e.test.js b/apps/backend/tests/e2e/e2e.test.js index cfc8b7d3..01ceb220 100644 --- a/apps/backend/tests/e2e/e2e.test.js +++ b/apps/backend/tests/e2e/e2e.test.js @@ -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"; diff --git a/apps/backend/tests/fetchGist.test.js b/apps/backend/tests/fetchGist.test.js index 6953921b..8f56214b 100644 --- a/apps/backend/tests/fetchGist.test.js +++ b/apps/backend/tests/fetchGist.test.js @@ -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( diff --git a/apps/backend/tests/gist.test.js b/apps/backend/tests/gist.test.js index c401a6bf..80e62c34 100644 --- a/apps/backend/tests/gist.test.js +++ b/apps/backend/tests/gist.test.js @@ -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); diff --git a/apps/backend/tests/pat-info.test.js b/apps/backend/tests/pat-info.test.js index 16d86757..a79c3991 100644 --- a/apps/backend/tests/pat-info.test.js +++ b/apps/backend/tests/pat-info.test.js @@ -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"; diff --git a/apps/backend/tests/pin.test.js b/apps/backend/tests/pin.test.js index 70b00182..0405dd23 100644 --- a/apps/backend/tests/pin.test.js +++ b/apps/backend/tests/pin.test.js @@ -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&repo=REPO_NAME", - renderOptions: { show_repo_link: false }, - }), - ); - }); - it("should have proper cache", async () => { const req = { query: { diff --git a/apps/backend/tests/private-instance/api.test.js b/apps/backend/tests/private-instance/api.test.js new file mode 100644 index 00000000..55230188 --- /dev/null +++ b/apps/backend/tests/private-instance/api.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/private-instance/gist.test.js b/apps/backend/tests/private-instance/gist.test.js new file mode 100644 index 00000000..e4bc1c22 --- /dev/null +++ b/apps/backend/tests/private-instance/gist.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/private-instance/pin.test.js b/apps/backend/tests/private-instance/pin.test.js new file mode 100644 index 00000000..d29554e4 --- /dev/null +++ b/apps/backend/tests/private-instance/pin.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/private-instance/top-langs.test.js b/apps/backend/tests/private-instance/top-langs.test.js new file mode 100644 index 00000000..b03e7e8f --- /dev/null +++ b/apps/backend/tests/private-instance/top-langs.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/public-instance/api.test.js b/apps/backend/tests/public-instance/api.test.js new file mode 100644 index 00000000..924035b3 --- /dev/null +++ b/apps/backend/tests/public-instance/api.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/public-instance/gist.test.js b/apps/backend/tests/public-instance/gist.test.js new file mode 100644 index 00000000..7ced1400 --- /dev/null +++ b/apps/backend/tests/public-instance/gist.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/public-instance/pin.test.js b/apps/backend/tests/public-instance/pin.test.js new file mode 100644 index 00000000..3a1ecf98 --- /dev/null +++ b/apps/backend/tests/public-instance/pin.test.js @@ -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&repo=REPO_NAME", + renderOptions: { show_repo_link: false }, + }), + ); + }); +}); diff --git a/apps/backend/tests/public-instance/top-langs.test.js b/apps/backend/tests/public-instance/top-langs.test.js new file mode 100644 index 00000000..3bfc6f4b --- /dev/null +++ b/apps/backend/tests/public-instance/top-langs.test.js @@ -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 }, + }), + ); + }); +}); diff --git a/apps/backend/tests/setup.jest.js b/apps/backend/tests/setup.jest.js index 0a77a124..5921bae1 100644 --- a/apps/backend/tests/setup.jest.js +++ b/apps/backend/tests/setup.jest.js @@ -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"; diff --git a/apps/backend/tests/setup.private-instance.jest.js b/apps/backend/tests/setup.private-instance.jest.js new file mode 100644 index 00000000..f4f094e5 --- /dev/null +++ b/apps/backend/tests/setup.private-instance.jest.js @@ -0,0 +1,2 @@ +process.env.GIST_WHITELIST = "bbfce31e0217a3689c8d961a356cb10d"; +process.env.WHITELIST = "anuraghazra"; diff --git a/apps/backend/tests/test-data/api-data.js b/apps/backend/tests/test-data/api-data.js new file mode 100644 index 00000000..cd0c1e64 --- /dev/null +++ b/apps/backend/tests/test-data/api-data.js @@ -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", + }, + }, + }, + }, +}; diff --git a/apps/backend/tests/test-data/gist-data.js b/apps/backend/tests/test-data/gist-data.js new file mode 100644 index 00000000..8bab8d2a --- /dev/null +++ b/apps/backend/tests/test-data/gist-data.js @@ -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, + }, + ], + }, + }, + }, +}; diff --git a/apps/backend/tests/test-data/langs-data.js b/apps/backend/tests/test-data/langs-data.js new file mode 100644 index 00000000..febec1c7 --- /dev/null +++ b/apps/backend/tests/test-data/langs-data.js @@ -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" } }, + ], + }, + }, + ], + }, + }, + }, +}; diff --git a/apps/backend/tests/test-data/pin-data.js b/apps/backend/tests/test-data/pin-data.js new file mode 100644 index 00000000..549f2c61 --- /dev/null +++ b/apps/backend/tests/test-data/pin-data.js @@ -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, + }, +}; diff --git a/apps/backend/tests/top-langs.test.js b/apps/backend/tests/top-langs.test.js index 53983cc6..ee1bc721 100644 --- a/apps/backend/tests/top-langs.test.js +++ b/apps/backend/tests/top-langs.test.js @@ -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: { diff --git a/apps/backend/tests/wakatime.test.js b/apps/backend/tests/wakatime.test.js index 58983a66..4e71f778 100644 --- a/apps/backend/tests/wakatime.test.js +++ b/apps/backend/tests/wakatime.test.js @@ -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 } }; diff --git a/apps/frontend/src/dotenv-browser-stub.ts b/apps/frontend/src/dotenv-browser-stub.ts deleted file mode 100644 index 5abc676a..00000000 --- a/apps/frontend/src/dotenv-browser-stub.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Safe browser stub for dotenv -export function config(): { parsed: Record } { - return { parsed: {} }; -} diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx b/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx index ba2c0f96..066b1d7b 100644 --- a/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx +++ b/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx @@ -52,7 +52,8 @@ export function LoginAccountDeleteModal(

- 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?


diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index cfdf96e8..358cb481 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -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( diff --git a/knip.jsonc b/knip.jsonc index 85bbcccb..3ce5dbf5 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bca48490..435cee94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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