Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73189400ee | ||
|
|
16ea9afebe | ||
|
|
8a239313d3 | ||
|
|
c707e6b7b1 | ||
|
|
a3dbb37dd2 | ||
|
|
9c4e631bc7 | ||
|
|
9dc4910d4b | ||
|
|
5d1978011e | ||
|
|
6ebd9a082f | ||
|
|
d84cc27b9c | ||
|
|
e43e55c2b7 | ||
|
|
f884df6312 | ||
|
|
9236acd99d | ||
|
|
12b4e979a8 | ||
|
|
f67dd0f381 | ||
|
|
1da425c0bb | ||
|
|
46df37adb7 | ||
|
|
f766c3b789 | ||
|
|
8305746754 | ||
|
|
b863992994 | ||
|
|
fe5ef06049 | ||
|
|
134b738151 | ||
|
|
3263dd9c78 | ||
|
|
b4ae648153 | ||
|
|
c401be4d6c | ||
|
|
f0c250a954 | ||
|
|
a963caaf5b | ||
|
|
eab14c8932 | ||
|
|
457c457cab | ||
|
|
609578da0c | ||
|
|
c3f338780b | ||
|
|
c4a4220448 | ||
|
|
9d8a7b7f0c | ||
|
|
0898dbfb00 | ||
|
|
6d3145c3d8 | ||
|
|
5156a811cb | ||
|
|
e263e5dfc4 | ||
|
|
f8094ab5d3 | ||
|
|
9c0f4a8390 | ||
|
|
1779475e1b | ||
|
|
b474572598 | ||
|
|
4cabac4bc1 | ||
|
|
d2a2274eeb | ||
|
|
30e2233ae4 | ||
|
|
4c2628fa67 | ||
|
|
9dcfce415d | ||
|
|
ae341596f3 | ||
|
|
c63bf09478 | ||
|
|
c772f6ae56 | ||
|
|
d5e7217b4f | ||
|
|
672777a7e1 | ||
|
|
3380ee97cc | ||
|
|
baf45b7e21 | ||
|
|
fc4dc9d010 | ||
|
|
805e668715 | ||
|
|
0e45db3041 | ||
|
|
b29d7989eb | ||
|
|
b2360c535a | ||
|
|
a552a04942 | ||
|
|
775abe704c | ||
|
|
fca39d25a8 | ||
|
|
dbe6aa0033 | ||
|
|
b40c4b7915 | ||
|
|
f7b1b9f382 | ||
|
|
5ccb799198 | ||
|
|
00f5ac900e | ||
|
|
3b67bd6bcb | ||
|
|
e58bee0bfd | ||
|
|
f346dcaa73 | ||
|
|
fc4ee7507b | ||
|
|
d476f8cbe1 | ||
|
|
19c39f02af |
@@ -5,9 +5,9 @@
|
|||||||
To set up the project GitHub-Stats-Extended locally, run the following commands:
|
To set up the project GitHub-Stats-Extended locally, run the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./vercel-preparation.sh
|
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm --filter frontend run build
|
pnpm run build:packages
|
||||||
|
pnpm run dev:frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
The easiest way to run and test the project is to deploy it to Vercel as described in the [deployment guide](../docs/deploy.md).
|
The easiest way to run and test the project is to deploy it to Vercel as described in the [deployment guide](../docs/deploy.md).
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ runs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||||
|
|
||||||
- name: Setup Node.js (via input)
|
- name: Setup Node.js (via input)
|
||||||
if: ${{ inputs.node-version }}
|
if: ${{ inputs.node-version }}
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@@ -26,7 +26,7 @@ runs:
|
|||||||
|
|
||||||
- name: Setup Node.js (via .nvmrc)
|
- name: Setup Node.js (via .nvmrc)
|
||||||
if: ${{ !inputs.node-version }}
|
if: ${{ !inputs.node-version }}
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|||||||
+49
-9
@@ -4,9 +4,8 @@ updates:
|
|||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: cron
|
interval: daily
|
||||||
cronjob: every day at 5am
|
open-pull-requests-limit: 20
|
||||||
open-pull-requests-limit: 10
|
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "build(deps)"
|
prefix: "build(deps)"
|
||||||
prefix-development: "build(deps-dev)"
|
prefix-development: "build(deps-dev)"
|
||||||
@@ -15,14 +14,56 @@ updates:
|
|||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "@types/node"
|
- dependency-name: "@types/node"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types: ["version-update:semver-major"]
|
||||||
|
groups:
|
||||||
|
react:
|
||||||
|
patterns:
|
||||||
|
- "react"
|
||||||
|
- "react-dom"
|
||||||
|
- "react-*"
|
||||||
|
- "@types/react"
|
||||||
|
- "@types/react-dom"
|
||||||
|
redux:
|
||||||
|
patterns:
|
||||||
|
- "redux"
|
||||||
|
- "@reduxjs/*"
|
||||||
|
- "react-redux"
|
||||||
|
vite:
|
||||||
|
patterns:
|
||||||
|
- "vite"
|
||||||
|
- "@vitejs/*"
|
||||||
|
tailwind:
|
||||||
|
patterns:
|
||||||
|
- "tailwindcss"
|
||||||
|
- "@tailwindcss/*"
|
||||||
|
- "daisyui"
|
||||||
|
vitest:
|
||||||
|
patterns:
|
||||||
|
- "vitest"
|
||||||
|
- "@vitest/*"
|
||||||
|
eslint:
|
||||||
|
patterns:
|
||||||
|
- "eslint"
|
||||||
|
- "@eslint/*"
|
||||||
|
- "eslint-*"
|
||||||
|
- "typescript-eslint"
|
||||||
|
typescript:
|
||||||
|
patterns:
|
||||||
|
- "typescript"
|
||||||
|
- "@types/*"
|
||||||
|
playwright:
|
||||||
|
patterns:
|
||||||
|
- "@playwright/*"
|
||||||
|
axios:
|
||||||
|
patterns:
|
||||||
|
- "axios"
|
||||||
|
- "axios-*"
|
||||||
|
|
||||||
# Maintain dependencies for GitHub Actions
|
# Maintain dependencies for GitHub Actions
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: cron
|
interval: daily
|
||||||
cronjob: every day at 5am
|
open-pull-requests-limit: 20
|
||||||
open-pull-requests-limit: 10
|
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "ci(deps)"
|
prefix: "ci(deps)"
|
||||||
prefix-development: "ci(deps-dev)"
|
prefix-development: "ci(deps-dev)"
|
||||||
@@ -33,9 +74,8 @@ updates:
|
|||||||
- package-ecosystem: devcontainers
|
- package-ecosystem: devcontainers
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: cron
|
interval: daily
|
||||||
cronjob: every day at 5am
|
open-pull-requests-limit: 20
|
||||||
open-pull-requests-limit: 10
|
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "build(deps)"
|
prefix: "build(deps)"
|
||||||
prefix-development: "build(deps-dev)"
|
prefix-development: "build(deps-dev)"
|
||||||
|
|||||||
+35
-29
@@ -31,26 +31,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Run vercel-preparation.sh
|
|
||||||
run: |
|
|
||||||
chmod +x ./vercel-preparation.sh
|
|
||||||
./vercel-preparation.sh
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm run build:packages
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: pnpm --filter frontend run build
|
run: pnpm run build:frontend
|
||||||
|
|
||||||
- name: Run frontend tests
|
- name: Run tests
|
||||||
run: pnpm --filter frontend run test
|
run: pnpm run test
|
||||||
|
|
||||||
- name: Run backend tests
|
|
||||||
run: pnpm --filter github-readme-stats run test
|
|
||||||
|
|
||||||
frontend-test-e2e:
|
frontend-test-e2e:
|
||||||
name: Frontend E2E test
|
name: Frontend E2E test
|
||||||
@@ -64,21 +59,42 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|
||||||
- name: Run vercel-preparation.sh
|
- name: Build packages
|
||||||
run: |
|
run: pnpm run build:packages
|
||||||
chmod +x ./vercel-preparation.sh
|
|
||||||
./vercel-preparation.sh
|
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: pnpm exec playwright install --with-deps
|
run: pnpm exec playwright install --with-deps
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: pnpm --filter frontend run test:e2e
|
run: pnpm --filter ./apps/frontend/ run test:e2e
|
||||||
|
|
||||||
|
backend-test-e2e:
|
||||||
|
name: Backend E2E test
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm run build:packages
|
||||||
|
|
||||||
|
- name: Run backend end-to-end tests
|
||||||
|
run: pnpm --filter ./apps/backend/ run test:e2e
|
||||||
|
|
||||||
code-checks:
|
code-checks:
|
||||||
name: Code checks
|
name: Code checks
|
||||||
@@ -90,17 +106,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
# Heads up!
|
|
||||||
#
|
|
||||||
# 1. Execution of this script is needed to resolve `.vercel` folder from `apps/frontend/src/components/Card/SVG.js`
|
|
||||||
# 2. This scripts removes `apps/backend/node_modules` breaking ESLint’s module resolution.
|
|
||||||
# Dependency installation must occur after running ./vercel-preparation.sh.
|
|
||||||
- name: Run vercel-preparation.sh
|
|
||||||
run: |
|
|
||||||
chmod +x ./vercel-preparation.sh
|
|
||||||
./vercel-preparation.sh
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- "apps/backend/themes/index.js"
|
- "packages/core/src/themes/index.js"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -30,17 +30,17 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|
||||||
- name: Generate readme
|
- name: Generate readme
|
||||||
run: |
|
run: |
|
||||||
pnpm --filter github-readme-stats run theme-readme-gen
|
pnpm --filter ./packages/core/ run theme-readme-gen
|
||||||
|
|
||||||
- name: Create Pull Request if themes README has changed
|
- name: Create Pull Request if themes README has changed
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||||
with:
|
with:
|
||||||
commit-message: "feat(backend): update themes README"
|
commit-message: "feat(backend): update themes README"
|
||||||
branch: "update_themes_readme/patch"
|
branch: "update_themes_readme/patch"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Publish core package to npm
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|
||||||
|
- name: Verify core package
|
||||||
|
run: |
|
||||||
|
pnpm --filter @stats-organization/github-readme-stats-core run typecheck
|
||||||
|
pnpm test
|
||||||
|
pnpm build:packages
|
||||||
|
pnpm --filter @stats-organization/github-readme-stats-core pack --dry-run
|
||||||
|
|
||||||
|
- name: Publish core package to npm
|
||||||
|
run: pnpm --filter @stats-organization/github-readme-stats-core publish --provenance --access public --no-git-checks
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Make Request
|
- name: Make Request
|
||||||
id: myRequest
|
id: myRequest
|
||||||
uses: fjogeleit/http-request-action@v2
|
uses: fjogeleit/http-request-action@551353b829c3646756b2ec2b3694f819d7957495 # v2
|
||||||
with:
|
with:
|
||||||
url: "https://github-stats-extended.vercel.app/api/repeat-recent"
|
url: "https://github-stats-extended.vercel.app/api/repeat-recent"
|
||||||
method: "POST"
|
method: "POST"
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
|
||||||
- name: Run update-languages-json.js script
|
- name: Run update-languages-json.js script
|
||||||
run: pnpm --filter github-readme-stats run generate-langs-json
|
run: pnpm --filter ./packages/core/ run generate-langs-json
|
||||||
|
|
||||||
- name: Create Pull Request if upstream language file is changed
|
- name: Create Pull Request if upstream language file is changed
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
|
||||||
with:
|
with:
|
||||||
commit-message: "feat(backend): update languages JSON"
|
commit-message: "feat(backend): update languages JSON"
|
||||||
branch: "update_langs/patch"
|
branch: "update_langs/patch"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Update version tags
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- "**"
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-semver:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
|
||||||
+4
-13
@@ -1,18 +1,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
@@ -26,11 +16,12 @@ apps/backend/vercel_token
|
|||||||
apps/backend-copy
|
apps/backend-copy
|
||||||
|
|
||||||
apps/frontend/.env
|
apps/frontend/.env
|
||||||
apps/frontend/src/backend
|
|
||||||
apps/frontend/build
|
|
||||||
|
|
||||||
|
|
||||||
|
.turbo
|
||||||
|
build
|
||||||
build-ts
|
build-ts
|
||||||
tsconfig.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
pnpm lint-staged
|
pnpm lint-staged
|
||||||
pnpm run lint
|
pnpm run lint
|
||||||
# TODO enable
|
pnpm test
|
||||||
# npm test
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "GitHub Stats Extended Dev",
|
"name": "GitHub Stats Extended Dev",
|
||||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": { "version": "22" }
|
"ghcr.io/devcontainers/features/node:1": { "version": "24" }
|
||||||
},
|
},
|
||||||
"forwardPorts": [3000],
|
"forwardPorts": [3000],
|
||||||
"portsAttributes": {
|
"portsAttributes": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"runtime": "nodejs22.x",
|
"runtime": "nodejs24.x",
|
||||||
"handler": "router.js",
|
"handler": "router.js",
|
||||||
"launcherType": "Nodejs"
|
"launcherType": "Nodejs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
/* eslint-disable import-x/no-unresolved */
|
|
||||||
import { default as authenticate } from "./api-renamed/authenticate.js";
|
|
||||||
import { default as deleteUser } from "./api-renamed/delete-user.js";
|
|
||||||
import { default as downgrade } from "./api-renamed/downgrade.js";
|
|
||||||
import { default as gist } from "./api-renamed/gist.js";
|
|
||||||
import { default as api } from "./api-renamed/index.js";
|
|
||||||
import { default as pin } from "./api-renamed/pin.js";
|
|
||||||
import { default as repeatRecent } from "./api-renamed/repeat-recent.js";
|
|
||||||
import { default as patInfo } from "./api-renamed/status/pat-info.js";
|
|
||||||
import { default as statusUp } from "./api-renamed/status/up.js";
|
|
||||||
import { default as topLangs } from "./api-renamed/top-langs.js";
|
|
||||||
import { default as userAccess } from "./api-renamed/user-access.js";
|
|
||||||
import { default as wakatimeProxy } from "./api-renamed/wakatime-proxy.js";
|
|
||||||
import { default as wakatime } from "./api-renamed/wakatime.js";
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
// remaining code expects express.js-like request and response objects
|
|
||||||
res.send = function (data) {
|
|
||||||
if (typeof data === "object") {
|
|
||||||
res.setHeader("Content-Type", "application/json");
|
|
||||||
res.end(JSON.stringify(data));
|
|
||||||
} else if (typeof data === "string") {
|
|
||||||
res.end(data);
|
|
||||||
} else {
|
|
||||||
res.end(String(data));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const url = new URL(req.url, "https://localhost");
|
|
||||||
req.query = Object.fromEntries(url.searchParams.entries());
|
|
||||||
|
|
||||||
switch (url.pathname) {
|
|
||||||
case "/api":
|
|
||||||
await api(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/gist":
|
|
||||||
await gist(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/pin":
|
|
||||||
await pin(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/top-langs":
|
|
||||||
await topLangs(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/wakatime":
|
|
||||||
await wakatime(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/wakatime-proxy":
|
|
||||||
await wakatimeProxy(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/repeat-recent":
|
|
||||||
await repeatRecent(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/status/pat-info":
|
|
||||||
await patInfo(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/status/up":
|
|
||||||
await statusUp(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/authenticate":
|
|
||||||
await authenticate(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/delete-user":
|
|
||||||
await deleteUser(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/user-access":
|
|
||||||
await userAccess(req, res);
|
|
||||||
break;
|
|
||||||
case "/api/downgrade":
|
|
||||||
await downgrade(req, res);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end("Not Found");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { logger } from "../src/common/log.js";
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
import { authenticate } from "../src/users.js";
|
import { authenticate } from "../src/users.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
import { deleteUser } from "../src/common/database.js";
|
import { deleteUser } from "../src/common/database.js";
|
||||||
import { logger } from "../src/common/log.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} req The request.
|
* @param {any} req The request.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { deleteUser, getUserAccessByKey } from "../src/common/database.js";
|
import { deleteUser, getUserAccessByKey } from "../src/common/database.js";
|
||||||
import { logger } from "../src/common/log.js";
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
// We could optimize this method by doing both database operations in one statement, using "DELETE ... RETURNING ..."
|
// We could optimize this method by doing both database operations in one statement, using "DELETE ... RETURNING ..."
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderGistCard } from "../src/cards/gist.js";
|
|
||||||
import { guardAccess } from "../src/common/access.js";
|
|
||||||
import {
|
|
||||||
CACHE_TTL,
|
|
||||||
resolveCacheSeconds,
|
|
||||||
setCacheHeaders,
|
|
||||||
setErrorCacheHeaders,
|
|
||||||
} from "../src/common/cache.js";
|
|
||||||
import { storeRequest } from "../src/common/database.js";
|
|
||||||
import {
|
|
||||||
MissingParamError,
|
|
||||||
retrieveSecondaryMessage,
|
|
||||||
} from "../src/common/error.js";
|
|
||||||
import { parseBoolean } from "../src/common/ops.js";
|
|
||||||
import { renderError } from "../src/common/render.js";
|
|
||||||
import { fetchGist } from "../src/fetchers/gist.js";
|
|
||||||
import { isLocaleAvailable } from "../src/translations.js";
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
export default async (req, res) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
title_color,
|
|
||||||
icon_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
theme,
|
|
||||||
cache_seconds,
|
|
||||||
locale,
|
|
||||||
border_radius,
|
|
||||||
border_color,
|
|
||||||
show_owner,
|
|
||||||
hide_border,
|
|
||||||
} = req.query;
|
|
||||||
|
|
||||||
res.setHeader("Content-Type", "image/svg+xml");
|
|
||||||
|
|
||||||
const access = guardAccess({
|
|
||||||
res,
|
|
||||||
id,
|
|
||||||
type: "gist",
|
|
||||||
colors: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!access.isPassed) {
|
|
||||||
return access.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale && !isLocaleAvailable(locale)) {
|
|
||||||
return res.send(
|
|
||||||
renderError({
|
|
||||||
message: "Something went wrong",
|
|
||||||
secondaryMessage: "Language not found",
|
|
||||||
renderOptions: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await storeRequest(req);
|
|
||||||
const gistData = await fetchGist(id);
|
|
||||||
const cacheSeconds = resolveCacheSeconds({
|
|
||||||
requested: parseInt(cache_seconds, 10),
|
|
||||||
def: CACHE_TTL.GIST_CARD.DEFAULT,
|
|
||||||
min: CACHE_TTL.GIST_CARD.MIN,
|
|
||||||
max: CACHE_TTL.GIST_CARD.MAX,
|
|
||||||
});
|
|
||||||
|
|
||||||
setCacheHeaders(res, cacheSeconds);
|
|
||||||
|
|
||||||
return res.send(
|
|
||||||
renderGistCard(gistData, {
|
|
||||||
title_color,
|
|
||||||
icon_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
theme,
|
|
||||||
border_radius,
|
|
||||||
border_color,
|
|
||||||
locale: locale ? locale.toLowerCase() : null,
|
|
||||||
show_owner: parseBoolean(show_owner),
|
|
||||||
hide_border: parseBoolean(hide_border),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
setErrorCacheHeaders(res);
|
|
||||||
if (err instanceof Error) {
|
|
||||||
return res.send(
|
|
||||||
renderError({
|
|
||||||
message: err.message,
|
|
||||||
secondaryMessage: retrieveSecondaryMessage(err),
|
|
||||||
renderOptions: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
show_repo_link: !(err instanceof MissingParamError),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res.send(
|
|
||||||
renderError({
|
|
||||||
message: "An unknown error occurred",
|
|
||||||
renderOptions: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -6,10 +6,12 @@
|
|||||||
*
|
*
|
||||||
* @description This function is currently rate limited to 1 request per 3 minutes.
|
* @description This function is currently rate limited to 1 request per 3 minutes.
|
||||||
*/
|
*/
|
||||||
|
import {
|
||||||
import { request } from "../../src/common/http.js";
|
dateDiff,
|
||||||
import { logger } from "../../src/common/log.js";
|
getConfig,
|
||||||
import { dateDiff } from "../../src/common/ops.js";
|
logger,
|
||||||
|
request,
|
||||||
|
} from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes
|
export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ const uptimeFetcher = (variables, token) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getAllPATs = () => {
|
const getAllPATs = () => {
|
||||||
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
|
return getConfig().pats;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +63,7 @@ const getPATInfo = async (fetcher, variables) => {
|
|||||||
|
|
||||||
for (const pat of PATs) {
|
for (const pat of PATs) {
|
||||||
try {
|
try {
|
||||||
const response = await fetcher(variables, process.env[pat]);
|
const response = await fetcher(variables, pat.value);
|
||||||
const errors = response.data.errors;
|
const errors = response.data.errors;
|
||||||
const hasErrors = Boolean(errors);
|
const hasErrors = Boolean(errors);
|
||||||
const errorType = errors?.[0]?.type;
|
const errorType = errors?.[0]?.type;
|
||||||
@@ -71,7 +73,7 @@ const getPATInfo = async (fetcher, variables) => {
|
|||||||
|
|
||||||
// Store PATs with errors.
|
// Store PATs with errors.
|
||||||
if (hasErrors && errorType !== "RATE_LIMITED") {
|
if (hasErrors && errorType !== "RATE_LIMITED") {
|
||||||
details[pat] = {
|
details[pat.name] = {
|
||||||
status: "error",
|
status: "error",
|
||||||
error: {
|
error: {
|
||||||
type: errors[0].type,
|
type: errors[0].type,
|
||||||
@@ -82,13 +84,13 @@ const getPATInfo = async (fetcher, variables) => {
|
|||||||
} else if (isRateLimited) {
|
} else if (isRateLimited) {
|
||||||
const date1 = new Date();
|
const date1 = new Date();
|
||||||
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
|
||||||
details[pat] = {
|
details[pat.name] = {
|
||||||
status: "exhausted",
|
status: "exhausted",
|
||||||
remaining: 0,
|
remaining: 0,
|
||||||
resetIn: dateDiff(date2, date1) + " minutes",
|
resetIn: dateDiff(date2, date1) + " minutes",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
details[pat] = {
|
details[pat.name] = {
|
||||||
status: "valid",
|
status: "valid",
|
||||||
remaining: response.data.data.rateLimit.remaining,
|
remaining: response.data.data.rateLimit.remaining,
|
||||||
};
|
};
|
||||||
@@ -97,11 +99,11 @@ const getPATInfo = async (fetcher, variables) => {
|
|||||||
// Store the PAT if it is expired.
|
// Store the PAT if it is expired.
|
||||||
const errorMessage = err.response?.data?.message?.toLowerCase();
|
const errorMessage = err.response?.data?.message?.toLowerCase();
|
||||||
if (errorMessage === "bad credentials") {
|
if (errorMessage === "bad credentials") {
|
||||||
details[pat] = {
|
details[pat.name] = {
|
||||||
status: "expired",
|
status: "expired",
|
||||||
};
|
};
|
||||||
} else if (errorMessage === "sorry. your account was suspended.") {
|
} else if (errorMessage === "sorry. your account was suspended.") {
|
||||||
details[pat] = {
|
details[pat.name] = {
|
||||||
status: "suspended",
|
status: "suspended",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
* @description This function is currently rate limited to 1 request per 3 minutes.
|
* @description This function is currently rate limited to 1 request per 3 minutes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { request } from "../../src/common/http.js";
|
import {
|
||||||
import { logger } from "../../src/common/log.js";
|
logger,
|
||||||
import { default as retryer } from "../../src/common/retryer.js";
|
request,
|
||||||
|
retryer,
|
||||||
|
} from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes
|
export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ export default async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
let PATsValid = true;
|
let PATsValid = true;
|
||||||
try {
|
try {
|
||||||
await retryer(uptimeFetcher, null, {});
|
await retryer(uptimeFetcher, {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Resolve eslint no-unused-vars
|
// Resolve eslint no-unused-vars
|
||||||
err;
|
err;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
import { getUserAccessByKey } from "../src/common/database.js";
|
import { getUserAccessByKey } from "../src/common/database.js";
|
||||||
import { logger } from "../src/common/log.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} req The request.
|
* @param {any} req The request.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { logger } from "../src/common/log.js";
|
import {
|
||||||
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
|
fetchWakatimeStats,
|
||||||
|
logger,
|
||||||
|
} from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} req The request.
|
* @param {any} req The request.
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { renderWakatimeCard } from "../src/cards/wakatime.js";
|
|
||||||
import { guardAccess } from "../src/common/access.js";
|
|
||||||
import {
|
|
||||||
CACHE_TTL,
|
|
||||||
resolveCacheSeconds,
|
|
||||||
setCacheHeaders,
|
|
||||||
setErrorCacheHeaders,
|
|
||||||
} from "../src/common/cache.js";
|
|
||||||
import { storeRequest } from "../src/common/database.js";
|
|
||||||
import {
|
|
||||||
MissingParamError,
|
|
||||||
retrieveSecondaryMessage,
|
|
||||||
} from "../src/common/error.js";
|
|
||||||
import { parseArray, parseBoolean } from "../src/common/ops.js";
|
|
||||||
import { renderError } from "../src/common/render.js";
|
|
||||||
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
|
|
||||||
import { isLocaleAvailable } from "../src/translations.js";
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
export default async (req, res) => {
|
|
||||||
const {
|
|
||||||
username,
|
|
||||||
title_color,
|
|
||||||
icon_color,
|
|
||||||
hide_border,
|
|
||||||
card_width,
|
|
||||||
line_height,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
theme,
|
|
||||||
cache_seconds,
|
|
||||||
hide_title,
|
|
||||||
hide_progress,
|
|
||||||
custom_title,
|
|
||||||
locale,
|
|
||||||
layout,
|
|
||||||
langs_count,
|
|
||||||
hide,
|
|
||||||
api_domain,
|
|
||||||
border_radius,
|
|
||||||
border_color,
|
|
||||||
display_format,
|
|
||||||
disable_animations,
|
|
||||||
} = req.query;
|
|
||||||
|
|
||||||
res.setHeader("Content-Type", "image/svg+xml");
|
|
||||||
|
|
||||||
const access = guardAccess({
|
|
||||||
res,
|
|
||||||
id: username,
|
|
||||||
type: "wakatime",
|
|
||||||
colors: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!access.isPassed) {
|
|
||||||
return access.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale && !isLocaleAvailable(locale)) {
|
|
||||||
return res.send(
|
|
||||||
renderError({
|
|
||||||
message: "Something went wrong",
|
|
||||||
secondaryMessage: "Language not found",
|
|
||||||
renderOptions: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await storeRequest(req);
|
|
||||||
const stats = await fetchWakatimeStats({ username, api_domain });
|
|
||||||
const cacheSeconds = resolveCacheSeconds({
|
|
||||||
requested: parseInt(cache_seconds, 10),
|
|
||||||
def: CACHE_TTL.WAKATIME_CARD.DEFAULT,
|
|
||||||
min: CACHE_TTL.WAKATIME_CARD.MIN,
|
|
||||||
max: CACHE_TTL.WAKATIME_CARD.MAX,
|
|
||||||
});
|
|
||||||
|
|
||||||
setCacheHeaders(res, cacheSeconds);
|
|
||||||
|
|
||||||
return res.send(
|
|
||||||
renderWakatimeCard(stats, {
|
|
||||||
custom_title,
|
|
||||||
hide_title: parseBoolean(hide_title),
|
|
||||||
hide_border: parseBoolean(hide_border),
|
|
||||||
card_width: parseInt(card_width, 10),
|
|
||||||
hide: parseArray(hide),
|
|
||||||
line_height,
|
|
||||||
title_color,
|
|
||||||
icon_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
theme,
|
|
||||||
hide_progress,
|
|
||||||
border_radius,
|
|
||||||
border_color,
|
|
||||||
locale: locale ? locale.toLowerCase() : null,
|
|
||||||
layout,
|
|
||||||
langs_count,
|
|
||||||
display_format,
|
|
||||||
disable_animations: parseBoolean(disable_animations),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
setErrorCacheHeaders(res);
|
|
||||||
if (err instanceof Error) {
|
|
||||||
return res.send(
|
|
||||||
renderError({
|
|
||||||
message: err.message,
|
|
||||||
secondaryMessage: retrieveSecondaryMessage(err),
|
|
||||||
renderOptions: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
show_repo_link: !(err instanceof MissingParamError),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res.send(
|
|
||||||
renderError({
|
|
||||||
message: "An unknown error occurred",
|
|
||||||
renderOptions: {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+2
-13
@@ -1,21 +1,10 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
|
|
||||||
import gistCard from "./api-renamed/gist.js";
|
import router from "./router.js";
|
||||||
import statsCard from "./api-renamed/index.js";
|
|
||||||
import repoCard from "./api-renamed/pin.js";
|
|
||||||
import langCard from "./api-renamed/top-langs.js";
|
|
||||||
import wakatimeCard from "./api-renamed/wakatime.js";
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/", statsCard);
|
app.use(router);
|
||||||
router.get("/pin", repoCard);
|
|
||||||
router.get("/top-langs", langCard);
|
|
||||||
router.get("/wakatime", wakatimeCard);
|
|
||||||
router.get("/gist", gistCard);
|
|
||||||
|
|
||||||
app.use("/api", router);
|
|
||||||
|
|
||||||
const port = process.env.PORT || process.env.port || 9000;
|
const port = process.env.PORT || process.env.port || 9000;
|
||||||
app.listen(port, "0.0.0.0", () => {
|
app.listen(port, "0.0.0.0", () => {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as router } from "./router.js";
|
||||||
+15
-38
@@ -1,52 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "github-readme-stats",
|
"name": "@stats-organization/github-readme-stats-backend",
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Dynamically generate stats for your GitHub readme",
|
|
||||||
"keywords": [
|
|
||||||
"github-readme-stats",
|
|
||||||
"readme-stats",
|
|
||||||
"cards",
|
|
||||||
"card-generator"
|
|
||||||
],
|
|
||||||
"main": "src/index.js",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://github-stats-extended.vercel.app/frontend",
|
"license": "MIT",
|
||||||
"bugs": {
|
"engines": {
|
||||||
"url": "https://github.com/stats-organization/github-stats-extended/issues"
|
"node": "24.x"
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/stats-organization/github-stats-extended.git"
|
|
||||||
},
|
},
|
||||||
|
"private": true,
|
||||||
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:update:snapshot": "vitest -u",
|
"test:update:snapshot": "vitest -u",
|
||||||
"test:e2e": "vitest --config vitest.config.e2e.ts",
|
"test:e2e": "vitest --config vitest.config.e2e.ts",
|
||||||
"theme-readme-gen": "node scripts/generate-theme-doc",
|
"bench": "vitest bench --run --config vitest.config.bench.ts",
|
||||||
"generate-langs-json": "node scripts/generate-langs-json",
|
"lint": "eslint",
|
||||||
"bench": "vitest bench --run --config vitest.config.bench.ts"
|
"typecheck": "tsc -p tsconfig.typecheck.json"
|
||||||
},
|
},
|
||||||
"author": "Anurag Hazra",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"axios-mock-adapter": "2.1.0",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"express": "5.2.1",
|
||||||
"@uppercod/css-to-object": "^1.1.1",
|
"jsdom": "29.0.2",
|
||||||
"@vitest/coverage-v8": "catalog:default",
|
|
||||||
"axios-mock-adapter": "^2.1.0",
|
|
||||||
"express": "^5.2.1",
|
|
||||||
"js-yaml": "^4.1.1",
|
|
||||||
"jsdom": "28.1.0",
|
|
||||||
"vitest": "catalog:default"
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.15.2",
|
||||||
"emoji-name-map": "^2.0.3",
|
"@stats-organization/github-readme-stats-core": "workspace:^",
|
||||||
"github-username-regex": "^1.0.0",
|
"pg": "^8.20.0"
|
||||||
"pg": "^8.18.0",
|
|
||||||
"word-wrap": "^1.2.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "24.x"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,225 @@
|
|||||||
|
import {
|
||||||
|
api,
|
||||||
|
gist,
|
||||||
|
pin,
|
||||||
|
topLangs,
|
||||||
|
wakatime,
|
||||||
|
} from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
|
import { default as authenticate } from "./api-renamed/authenticate.js";
|
||||||
|
import { default as deleteUser } from "./api-renamed/delete-user.js";
|
||||||
|
import { default as downgrade } from "./api-renamed/downgrade.js";
|
||||||
|
import { default as repeatRecent } from "./api-renamed/repeat-recent.js";
|
||||||
|
import { default as patInfo } from "./api-renamed/status/pat-info.js";
|
||||||
|
import { default as statusUp } from "./api-renamed/status/up.js";
|
||||||
|
import { default as userAccess } from "./api-renamed/user-access.js";
|
||||||
|
import { default as wakatimeProxy } from "./api-renamed/wakatime-proxy.js";
|
||||||
|
import { guardAccess } from "./src/common/access.js";
|
||||||
|
import {
|
||||||
|
CACHE_TTL,
|
||||||
|
resolveCacheSeconds,
|
||||||
|
setCacheHeaders,
|
||||||
|
setErrorCacheHeaders,
|
||||||
|
} from "./src/common/cache.js";
|
||||||
|
import { getUserAccessByName, storeRequest } from "./src/common/database.js";
|
||||||
|
|
||||||
|
const getGuardResult = (query, type, id) => {
|
||||||
|
const access = guardAccess({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
colors: {
|
||||||
|
title_color: query.title_color,
|
||||||
|
text_color: query.text_color,
|
||||||
|
bg_color: query.bg_color,
|
||||||
|
border_color: query.border_color,
|
||||||
|
theme: query.theme,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (access.isPassed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "error - permanent",
|
||||||
|
content: access.result,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserPat = async (username) => {
|
||||||
|
if (!username) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAccess = await getUserAccessByName(username);
|
||||||
|
if (!userAccess?.token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userAccess.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const url = new URL(req.url, "https://localhost");
|
||||||
|
if (res.send === undefined) {
|
||||||
|
// remaining code expects express.js-like request and response objects
|
||||||
|
res.send = function (data) {
|
||||||
|
if (typeof data === "object") {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
res.end(JSON.stringify(data));
|
||||||
|
} else if (typeof data === "string") {
|
||||||
|
res.end(data);
|
||||||
|
} else {
|
||||||
|
res.end(String(data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.query = Object.fromEntries(url.searchParams.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (url.pathname) {
|
||||||
|
case "/api": {
|
||||||
|
result = getGuardResult(req.query, "username", req.query.username);
|
||||||
|
if (!result) {
|
||||||
|
const userPat = await getUserPat(req.query.username);
|
||||||
|
result = await api(req.query, userPat);
|
||||||
|
}
|
||||||
|
if (result.status === "error - temporary") {
|
||||||
|
setErrorCacheHeaders(res);
|
||||||
|
} else {
|
||||||
|
const cacheSeconds = resolveCacheSeconds({
|
||||||
|
requested: parseInt(req.query.cache_seconds, 10),
|
||||||
|
def: CACHE_TTL.STATS_CARD.DEFAULT,
|
||||||
|
min: CACHE_TTL.STATS_CARD.MIN,
|
||||||
|
max: CACHE_TTL.STATS_CARD.MAX,
|
||||||
|
});
|
||||||
|
setCacheHeaders(res, cacheSeconds);
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
res.end(result.content);
|
||||||
|
if (result.status !== "error - permanent") {
|
||||||
|
await storeRequest(req);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "/api/gist":
|
||||||
|
result =
|
||||||
|
getGuardResult(req.query, "gist", req.query.id) ??
|
||||||
|
(await gist(req.query));
|
||||||
|
if (result.status === "error - temporary") {
|
||||||
|
setErrorCacheHeaders(res);
|
||||||
|
} else {
|
||||||
|
const cacheSeconds = resolveCacheSeconds({
|
||||||
|
requested: parseInt(req.query.cache_seconds, 10),
|
||||||
|
def: CACHE_TTL.GIST_CARD.DEFAULT,
|
||||||
|
min: CACHE_TTL.GIST_CARD.MIN,
|
||||||
|
max: CACHE_TTL.GIST_CARD.MAX,
|
||||||
|
});
|
||||||
|
setCacheHeaders(res, cacheSeconds);
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
res.end(result.content);
|
||||||
|
if (result.status !== "error - permanent") {
|
||||||
|
await storeRequest(req);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "/api/pin": {
|
||||||
|
result = getGuardResult(req.query, "username", req.query.username);
|
||||||
|
if (!result) {
|
||||||
|
const userPat = await getUserPat(req.query.username);
|
||||||
|
result = await pin(req.query, userPat);
|
||||||
|
}
|
||||||
|
if (result.status === "error - temporary") {
|
||||||
|
setErrorCacheHeaders(res);
|
||||||
|
} else {
|
||||||
|
const cacheSeconds = resolveCacheSeconds({
|
||||||
|
requested: parseInt(req.query.cache_seconds, 10),
|
||||||
|
def: CACHE_TTL.PIN_CARD.DEFAULT,
|
||||||
|
min: CACHE_TTL.PIN_CARD.MIN,
|
||||||
|
max: CACHE_TTL.PIN_CARD.MAX,
|
||||||
|
});
|
||||||
|
setCacheHeaders(res, cacheSeconds);
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
res.end(result.content);
|
||||||
|
if (result.status !== "error - permanent") {
|
||||||
|
await storeRequest(req);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "/api/top-langs": {
|
||||||
|
result = getGuardResult(req.query, "username", req.query.username);
|
||||||
|
if (!result) {
|
||||||
|
const userPat = await getUserPat(req.query.username);
|
||||||
|
result = await topLangs(req.query, userPat);
|
||||||
|
}
|
||||||
|
if (result.status === "error - temporary") {
|
||||||
|
setErrorCacheHeaders(res);
|
||||||
|
} else {
|
||||||
|
const cacheSeconds = resolveCacheSeconds({
|
||||||
|
requested: parseInt(req.query.cache_seconds, 10),
|
||||||
|
def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
|
||||||
|
min: CACHE_TTL.TOP_LANGS_CARD.MIN,
|
||||||
|
max: CACHE_TTL.TOP_LANGS_CARD.MAX,
|
||||||
|
});
|
||||||
|
setCacheHeaders(res, cacheSeconds);
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
res.end(result.content);
|
||||||
|
if (result.status !== "error - permanent") {
|
||||||
|
await storeRequest(req);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "/api/wakatime":
|
||||||
|
result =
|
||||||
|
getGuardResult(req.query, "wakatime", req.query.username) ??
|
||||||
|
(await wakatime(req.query));
|
||||||
|
if (result.status === "error - temporary") {
|
||||||
|
setErrorCacheHeaders(res);
|
||||||
|
} else {
|
||||||
|
const cacheSeconds = resolveCacheSeconds({
|
||||||
|
requested: parseInt(req.query.cache_seconds, 10),
|
||||||
|
def: CACHE_TTL.WAKATIME_CARD.DEFAULT,
|
||||||
|
min: CACHE_TTL.WAKATIME_CARD.MIN,
|
||||||
|
max: CACHE_TTL.WAKATIME_CARD.MAX,
|
||||||
|
});
|
||||||
|
setCacheHeaders(res, cacheSeconds);
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "image/svg+xml");
|
||||||
|
res.end(result.content);
|
||||||
|
if (result.status !== "error - permanent") {
|
||||||
|
await storeRequest(req);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "/api/wakatime-proxy":
|
||||||
|
await wakatimeProxy(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/repeat-recent":
|
||||||
|
await repeatRecent(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/status/pat-info":
|
||||||
|
await patInfo(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/status/up":
|
||||||
|
await statusUp(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/authenticate":
|
||||||
|
await authenticate(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/delete-user":
|
||||||
|
await deleteUser(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/user-access":
|
||||||
|
await userAccess(req, res);
|
||||||
|
break;
|
||||||
|
case "/api/downgrade":
|
||||||
|
await downgrade(req, res);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end("Not Found");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export { renderRepoCard } from "./repo.js";
|
|
||||||
export { renderStatsCard } from "./stats.js";
|
|
||||||
export { renderTopLanguages } from "./top-languages.js";
|
|
||||||
export { renderWakatimeCard } from "./wakatime.js";
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConfig,
|
||||||
|
renderError,
|
||||||
|
} from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
import { blacklist } from "./blacklist.js";
|
import { blacklist } from "./blacklist.js";
|
||||||
import { gistWhitelist, whitelist } from "./envs.js";
|
|
||||||
import { renderError } from "./render.js";
|
|
||||||
|
|
||||||
const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted";
|
const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted";
|
||||||
const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted";
|
const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted";
|
||||||
@@ -12,36 +15,35 @@ const BLACKLISTED_MESSAGE = "This username is blacklisted";
|
|||||||
* Guards access using whitelist/blacklist.
|
* Guards access using whitelist/blacklist.
|
||||||
*
|
*
|
||||||
* @param {Object} args The parameters object.
|
* @param {Object} args The parameters object.
|
||||||
* @param {any} args.res The response object.
|
|
||||||
* @param {string} args.id Resource identifier (username or gist id).
|
* @param {string} args.id Resource identifier (username or gist id).
|
||||||
* @param {"username"|"gist"|"wakatime"} args.type The type of identifier.
|
* @param {"username"|"gist"|"wakatime"} args.type The type of identifier.
|
||||||
* @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card.
|
* @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card.
|
||||||
* @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure.
|
* @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure.
|
||||||
*/
|
*/
|
||||||
const guardAccess = ({ res, id, type, colors }) => {
|
const guardAccess = ({ id, type, colors }) => {
|
||||||
if (!["username", "gist", "wakatime"].includes(type)) {
|
if (!["username", "gist", "wakatime"].includes(type)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid type. Expected "username", "gist", or "wakatime".',
|
'Invalid type. Expected "username", "gist", or "wakatime".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentWhitelist = type === "gist" ? gistWhitelist : whitelist;
|
const config = getConfig();
|
||||||
|
const currentWhitelist =
|
||||||
|
type === "gist" ? config.gistWhitelist : config.whitelist;
|
||||||
const notWhitelistedMsg =
|
const notWhitelistedMsg =
|
||||||
type === "gist"
|
type === "gist"
|
||||||
? NOT_WHITELISTED_GIST_MESSAGE
|
? NOT_WHITELISTED_GIST_MESSAGE
|
||||||
: NOT_WHITELISTED_USERNAME_MESSAGE;
|
: NOT_WHITELISTED_USERNAME_MESSAGE;
|
||||||
|
|
||||||
if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) {
|
if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) {
|
||||||
const result = res.send(
|
const result = renderError({
|
||||||
renderError({
|
message: notWhitelistedMsg,
|
||||||
message: notWhitelistedMsg,
|
secondaryMessage: "Please deploy your own instance",
|
||||||
secondaryMessage: "Please deploy your own instance",
|
renderOptions: {
|
||||||
renderOptions: {
|
...colors,
|
||||||
...colors,
|
show_repo_link: false,
|
||||||
show_repo_link: false,
|
},
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
return { isPassed: false, result };
|
return { isPassed: false, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,16 +52,14 @@ const guardAccess = ({ res, id, type, colors }) => {
|
|||||||
currentWhitelist === undefined &&
|
currentWhitelist === undefined &&
|
||||||
blacklist.includes(id)
|
blacklist.includes(id)
|
||||||
) {
|
) {
|
||||||
const result = res.send(
|
const result = renderError({
|
||||||
renderError({
|
message: BLACKLISTED_MESSAGE,
|
||||||
message: BLACKLISTED_MESSAGE,
|
secondaryMessage: "Please deploy your own instance",
|
||||||
secondaryMessage: "Please deploy your own instance",
|
renderOptions: {
|
||||||
renderOptions: {
|
...colors,
|
||||||
...colors,
|
show_repo_link: false,
|
||||||
show_repo_link: false,
|
},
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
return { isPassed: false, result };
|
return { isPassed: false, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import { clampValue } from "./ops.js";
|
import { clampValue } from "@stats-organization/github-readme-stats-core";
|
||||||
|
|
||||||
const MIN = 60;
|
const MIN = 60;
|
||||||
const HOUR = 60 * MIN;
|
const HOUR = 60 * MIN;
|
||||||
@@ -106,7 +106,7 @@ const disableCaching = (res) => {
|
|||||||
* @param {number} cacheSeconds The cache seconds to set in the headers.
|
* @param {number} cacheSeconds The cache seconds to set in the headers.
|
||||||
*/
|
*/
|
||||||
const setCacheHeaders = (res, cacheSeconds) => {
|
const setCacheHeaders = (res, cacheSeconds) => {
|
||||||
if (cacheSeconds < 1 || process.env.NODE_ENV === "development") {
|
if (cacheSeconds < 1) {
|
||||||
disableCaching(res);
|
disableCaching(res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -128,10 +128,7 @@ const setErrorCacheHeaders = (res) => {
|
|||||||
const envCacheSeconds = process.env.CACHE_SECONDS
|
const envCacheSeconds = process.env.CACHE_SECONDS
|
||||||
? parseInt(process.env.CACHE_SECONDS, 10)
|
? parseInt(process.env.CACHE_SECONDS, 10)
|
||||||
: NaN;
|
: NaN;
|
||||||
if (
|
if (!isNaN(envCacheSeconds) && envCacheSeconds < 1) {
|
||||||
(!isNaN(envCacheSeconds) && envCacheSeconds < 1) ||
|
|
||||||
process.env.NODE_ENV === "development"
|
|
||||||
) {
|
|
||||||
disableCaching(res);
|
disableCaching(res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
/**
|
export let pool = null;
|
||||||
* In the browser this has to be mocked to avoid runtime errors
|
if (process.env.POSTGRES_URL) {
|
||||||
* @see apps/frontend/vite.config.ts
|
const { Pool } = await import("pg");
|
||||||
*/
|
pool = new Pool({
|
||||||
import { Pool } from "pg";
|
connectionString: process.env.POSTGRES_URL,
|
||||||
|
});
|
||||||
export const pool = process.env.POSTGRES_URL
|
}
|
||||||
? new Pool({
|
|
||||||
connectionString: process.env.POSTGRES_URL,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates all required tables if they do not exist.
|
* Creates all required tables if they do not exist.
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
const whitelist = process.env.WHITELIST
|
|
||||||
? process.env.WHITELIST.split(",")
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const gistWhitelist = process.env.GIST_WHITELIST
|
|
||||||
? process.env.GIST_WHITELIST.split(",")
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const excludeRepositories = process.env.EXCLUDE_REPO
|
|
||||||
? process.env.EXCLUDE_REPO.split(",")
|
|
||||||
: [];
|
|
||||||
|
|
||||||
export { whitelist, gistWhitelist, excludeRepositories };
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
export { blacklist } from "./blacklist.js";
|
|
||||||
export { Card } from "./Card.js";
|
|
||||||
export { I18n } from "./I18n.js";
|
|
||||||
export { icons } from "./icons.js";
|
|
||||||
export { retryer } from "./retryer.js";
|
|
||||||
export {
|
|
||||||
ERROR_CARD_LENGTH,
|
|
||||||
renderError,
|
|
||||||
flexLayout,
|
|
||||||
measureText,
|
|
||||||
} from "./render.js";
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return console instance based on the environment.
|
|
||||||
*
|
|
||||||
* @type {Console | {log: () => void, error: () => void}}
|
|
||||||
*/
|
|
||||||
const logger =
|
|
||||||
process.env.NODE_ENV === "test" ? { log: noop, error: noop } : console;
|
|
||||||
|
|
||||||
export { logger };
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { getCardColors } from "./color.js";
|
|
||||||
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
|
|
||||||
import { encodeHTML } from "./html.js";
|
|
||||||
import { clampValue } from "./ops.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto layout utility, allows us to layout things vertically or horizontally with
|
|
||||||
* proper gaping.
|
|
||||||
*
|
|
||||||
* @param {object} props Function properties.
|
|
||||||
* @param {string[]} props.items Array of items to layout.
|
|
||||||
* @param {number} props.gap Gap between items.
|
|
||||||
* @param {"column" | "row"=} props.direction Direction to layout items.
|
|
||||||
* @param {number[]=} props.sizes Array of sizes for each item.
|
|
||||||
* @returns {string[]} Array of items with proper layout.
|
|
||||||
*/
|
|
||||||
const flexLayout = ({ items, gap, direction, sizes = [] }) => {
|
|
||||||
let lastSize = 0;
|
|
||||||
// filter() for filtering out empty strings
|
|
||||||
return items.filter(Boolean).map((item, i) => {
|
|
||||||
const size = sizes[i] || 0;
|
|
||||||
let transform = `translate(${lastSize}, 0)`;
|
|
||||||
if (direction === "column") {
|
|
||||||
transform = `translate(0, ${lastSize})`;
|
|
||||||
}
|
|
||||||
lastSize += size + gap;
|
|
||||||
return `<g transform="${transform}">${item}</g>`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a node to display the primary programming language of the repository/gist.
|
|
||||||
*
|
|
||||||
* @param {string} langName Language name.
|
|
||||||
* @param {string} langColor Language color.
|
|
||||||
* @returns {string} Language display SVG object.
|
|
||||||
*/
|
|
||||||
const createLanguageNode = (langName, langColor) => {
|
|
||||||
return `
|
|
||||||
<g data-testid="primary-lang">
|
|
||||||
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
|
|
||||||
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
|
|
||||||
</g>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a node to indicate progress in percentage along a horizontal line.
|
|
||||||
*
|
|
||||||
* @param {Object} params Object that contains the createProgressNode parameters.
|
|
||||||
* @param {number} params.x X-axis position.
|
|
||||||
* @param {number} params.y Y-axis position.
|
|
||||||
* @param {number} params.width Width of progress bar.
|
|
||||||
* @param {string} params.color Progress color.
|
|
||||||
* @param {number} params.progress Progress value.
|
|
||||||
* @param {string} params.progressBarBackgroundColor Progress bar bg color.
|
|
||||||
* @param {number} params.delay Delay before animation starts.
|
|
||||||
* @returns {string} Progress node.
|
|
||||||
*/
|
|
||||||
const createProgressNode = ({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
color,
|
|
||||||
progress,
|
|
||||||
progressBarBackgroundColor,
|
|
||||||
delay,
|
|
||||||
}) => {
|
|
||||||
const progressPercentage = clampValue(progress, 2, 100);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<svg width="${width}" x="${x}" y="${y}">
|
|
||||||
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
|
|
||||||
<svg data-testid="lang-progress" width="${progressPercentage}%">
|
|
||||||
<rect
|
|
||||||
height="8"
|
|
||||||
fill="${color}"
|
|
||||||
rx="5" ry="5" x="0" y="0"
|
|
||||||
class="lang-progress"
|
|
||||||
style="animation-delay: ${delay}ms;"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an icon with label to display repository/gist stats like forks, stars, etc.
|
|
||||||
*
|
|
||||||
* @param {string} icon The icon to display.
|
|
||||||
* @param {number|string} label The label to display.
|
|
||||||
* @param {string} testid The testid to assign to the label.
|
|
||||||
* @param {number} iconSize The size of the icon.
|
|
||||||
* @returns {string} Icon with label SVG object.
|
|
||||||
*/
|
|
||||||
const iconWithLabel = (icon, label, testid, iconSize) => {
|
|
||||||
if (typeof label === "number" && label <= 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const iconSvg = `
|
|
||||||
<svg
|
|
||||||
class="icon"
|
|
||||||
y="-12"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
version="1.1"
|
|
||||||
width="${iconSize}"
|
|
||||||
height="${iconSize}"
|
|
||||||
>
|
|
||||||
${icon}
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
|
|
||||||
return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Script parameters.
|
|
||||||
const ERROR_CARD_LENGTH = 576.5;
|
|
||||||
|
|
||||||
const UPSTREAM_API_ERRORS = [
|
|
||||||
TRY_AGAIN_LATER,
|
|
||||||
SECONDARY_ERROR_MESSAGES.MAX_RETRY,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders error message on the card.
|
|
||||||
*
|
|
||||||
* @param {object} args Function arguments.
|
|
||||||
* @param {string} args.message Main error message.
|
|
||||||
* @param {string} [args.secondaryMessage=""] The secondary error message.
|
|
||||||
* @param {object} [args.renderOptions={}] Render options.
|
|
||||||
* @param {string=} args.renderOptions.title_color Card title color.
|
|
||||||
* @param {string=} args.renderOptions.text_color Card text color.
|
|
||||||
* @param {string=} args.renderOptions.bg_color Card background color.
|
|
||||||
* @param {string=} args.renderOptions.border_color Card border color.
|
|
||||||
* @param {Parameters<typeof getCardColors>[0]["theme"]=} args.renderOptions.theme Card theme.
|
|
||||||
* @param {boolean=} args.renderOptions.show_repo_link Whether to show repo link or not.
|
|
||||||
* @returns {string} The SVG markup.
|
|
||||||
*/
|
|
||||||
const renderError = ({
|
|
||||||
message,
|
|
||||||
secondaryMessage = "",
|
|
||||||
renderOptions = {},
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
theme = "default",
|
|
||||||
show_repo_link = true,
|
|
||||||
} = renderOptions;
|
|
||||||
|
|
||||||
// returns theme based colors with proper overrides and defaults
|
|
||||||
const { titleColor, textColor, bgColor, borderColor } = getCardColors({
|
|
||||||
title_color,
|
|
||||||
text_color,
|
|
||||||
icon_color: "",
|
|
||||||
bg_color,
|
|
||||||
border_color,
|
|
||||||
ring_color: "",
|
|
||||||
theme,
|
|
||||||
});
|
|
||||||
|
|
||||||
return `
|
|
||||||
<svg width="${ERROR_CARD_LENGTH}" height="120" viewBox="0 0 ${ERROR_CARD_LENGTH} 120" fill="${bgColor}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>
|
|
||||||
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
|
|
||||||
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
|
|
||||||
.gray { fill: #858585 }
|
|
||||||
</style>
|
|
||||||
<rect x="0.5" y="0.5" width="${
|
|
||||||
ERROR_CARD_LENGTH - 1
|
|
||||||
}" height="99%" rx="4.5" fill="${bgColor}" stroke="${borderColor}"/>
|
|
||||||
<text x="25" y="45" class="text">Something went wrong!${
|
|
||||||
UPSTREAM_API_ERRORS.includes(secondaryMessage) || !show_repo_link
|
|
||||||
? ""
|
|
||||||
: " file an issue at https://tinyurl.com/github-stats"
|
|
||||||
}</text>
|
|
||||||
<text data-testid="message" x="25" y="55" class="text small">
|
|
||||||
<tspan x="25" dy="18">${encodeHTML(message)}</tspan>
|
|
||||||
<tspan x="25" dy="18" class="gray">${secondaryMessage}</tspan>
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve text length.
|
|
||||||
*
|
|
||||||
* @see https://stackoverflow.com/a/48172630/10629172
|
|
||||||
* @param {string} str String to measure.
|
|
||||||
* @param {number} fontSize Font size.
|
|
||||||
* @returns {number} Text length.
|
|
||||||
*/
|
|
||||||
const measureText = (str, fontSize = 10) => {
|
|
||||||
// prettier-ignore
|
|
||||||
const widths = [
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0.2796875, 0.2765625,
|
|
||||||
0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625,
|
|
||||||
0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125,
|
|
||||||
0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
|
|
||||||
0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
|
|
||||||
0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875,
|
|
||||||
1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625,
|
|
||||||
0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625,
|
|
||||||
0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625,
|
|
||||||
0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375,
|
|
||||||
0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625,
|
|
||||||
0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5,
|
|
||||||
0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875,
|
|
||||||
0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875,
|
|
||||||
0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875,
|
|
||||||
0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625,
|
|
||||||
];
|
|
||||||
|
|
||||||
const avg = 0.5279276315789471;
|
|
||||||
return (
|
|
||||||
str
|
|
||||||
.split("")
|
|
||||||
.map((c) =>
|
|
||||||
c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
|
|
||||||
)
|
|
||||||
.reduce((cur, acc) => acc + cur) * fontSize
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
ERROR_CARD_LENGTH,
|
|
||||||
renderError,
|
|
||||||
createLanguageNode,
|
|
||||||
createProgressNode,
|
|
||||||
iconWithLabel,
|
|
||||||
flexLayout,
|
|
||||||
measureText,
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./common/index.js";
|
|
||||||
export * from "./cards/index.js";
|
|
||||||
@@ -80,6 +80,7 @@ async function githubAuthenticate(code, privateAccess) {
|
|||||||
return { userId, accessToken, needDowngrade };
|
return { userId, accessToken, needDowngrade };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
|
// eslint-disable-next-line preserve-caught-error
|
||||||
throw new Error(`OAuth Error: ${err.response.status}`);
|
throw new Error(`OAuth Error: ${err.response.status}`);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
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,2 +0,0 @@
|
|||||||
process.env.GIST_WHITELIST = "bbfce31e0217a3689c8d961a356cb10d";
|
|
||||||
process.env.WHITELIST = "anuraghazra";
|
|
||||||
+133
-297
@@ -1,345 +1,181 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import axios from "axios";
|
import { api, getConfig } from "@stats-organization/github-readme-stats-core";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import api from "../api-renamed/index.js";
|
import router from "../router.js";
|
||||||
import { calculateRank } from "../src/calculateRank.js";
|
|
||||||
import { renderStatsCard } from "../src/cards/stats.js";
|
|
||||||
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
||||||
import { renderError } from "../src/common/render.js";
|
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
|
||||||
|
|
||||||
import { data_stats, stats } from "./test-data/api-data.js";
|
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
|
||||||
|
const { mockCore } = await import("./utils.js");
|
||||||
stats.rank = calculateRank({
|
return mockCore();
|
||||||
all_commits: false,
|
|
||||||
commits: stats.totalCommits,
|
|
||||||
prs: stats.totalPRs,
|
|
||||||
reviews: stats.totalReviews,
|
|
||||||
issues: stats.totalIssues,
|
|
||||||
repos: 1,
|
|
||||||
stars: stats.totalStars,
|
|
||||||
followers: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const error = {
|
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
|
||||||
errors: [
|
...(await importOriginal()),
|
||||||
{
|
storeRequest: vi.fn(),
|
||||||
type: "NOT_FOUND",
|
getUserAccessByName: vi.fn(),
|
||||||
path: ["user"],
|
}));
|
||||||
locations: [],
|
|
||||||
message: "Could not fetch user",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const apiMock = vi.mocked(api);
|
||||||
|
const getConfigMock = vi.mocked(getConfig);
|
||||||
|
const storeRequestMock = vi.mocked(storeRequest);
|
||||||
|
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
|
||||||
|
|
||||||
// @ts-ignore
|
const createRequest = (search = "") => ({
|
||||||
const faker = (query, data) => {
|
headers: {},
|
||||||
const req = {
|
url: `/api?${search}`,
|
||||||
query: {
|
});
|
||||||
username: "anuraghazra",
|
|
||||||
...query,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
|
|
||||||
|
|
||||||
return { req, res };
|
const createResponse = () => ({
|
||||||
};
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultCacheHeader =
|
||||||
|
`max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
|
||||||
|
`s-maxage=${CACHE_TTL.STATS_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(() => {
|
beforeEach(() => {
|
||||||
process.env.CACHE_SECONDS = undefined;
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
describe("Test /api backend routing", () => {
|
||||||
mock.reset();
|
it("happy path should pass query params and user PAT, respond with stats content and persist request", async () => {
|
||||||
});
|
getUserAccessByNameMock.mockResolvedValue({ token: "user-pat" });
|
||||||
|
apiMock.mockResolvedValue({
|
||||||
|
status: "success",
|
||||||
|
content: "mock-stats-svg",
|
||||||
|
});
|
||||||
|
|
||||||
describe("Test /api/", () => {
|
const req = createRequest(
|
||||||
it("should test the request", async () => {
|
"username=anuraghazra&theme=dark&hide=issues,prs,contribs",
|
||||||
const { req, res } = faker({}, data_stats);
|
|
||||||
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderStatsCard(stats, { ...req.query }),
|
|
||||||
);
|
);
|
||||||
});
|
const res = createResponse();
|
||||||
|
|
||||||
it("should render error card on error", async () => {
|
await router(req, res);
|
||||||
const { req, res } = faker({}, error);
|
|
||||||
|
|
||||||
await api(req, res);
|
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
|
||||||
|
expect(apiMock).toHaveBeenCalledWith(
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
|
||||||
message: error.errors[0].message,
|
|
||||||
secondaryMessage:
|
|
||||||
"Make sure the provided username is not an organization",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error card in same theme as requested card", async () => {
|
|
||||||
const { req, res } = faker({ theme: "merko" }, error);
|
|
||||||
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
|
||||||
message: error.errors[0].message,
|
|
||||||
secondaryMessage:
|
|
||||||
"Make sure the provided username is not an organization",
|
|
||||||
renderOptions: { theme: "merko" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get the query options", async () => {
|
|
||||||
const { req, res } = faker(
|
|
||||||
{
|
{
|
||||||
username: "anuraghazra",
|
username: "anuraghazra",
|
||||||
|
theme: "dark",
|
||||||
hide: "issues,prs,contribs",
|
hide: "issues,prs,contribs",
|
||||||
show_icons: true,
|
|
||||||
hide_border: true,
|
|
||||||
line_height: 100,
|
|
||||||
title_color: "fff",
|
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
},
|
},
|
||||||
data_stats,
|
"user-pat",
|
||||||
);
|
);
|
||||||
|
expect(req.query).toEqual({
|
||||||
await api(req, res);
|
username: "anuraghazra",
|
||||||
|
theme: "dark",
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
hide: "issues,prs,contribs",
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
});
|
||||||
renderStatsCard(stats, {
|
|
||||||
hide: ["issues", "prs", "contribs"],
|
|
||||||
show_icons: true,
|
|
||||||
hide_border: true,
|
|
||||||
line_height: 100,
|
|
||||||
title_color: "fff",
|
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache", async () => {
|
|
||||||
const { req, res } = faker({}, data_stats);
|
|
||||||
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
|
["Cache-Control", defaultCacheHeader],
|
||||||
["Content-Type", "image/svg+xml"],
|
["Content-Type", "image/svg+xml"],
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-stats-svg");
|
||||||
|
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set proper cache", async () => {
|
it("should use the shorter error cache for temporary stats errors", async () => {
|
||||||
const cache_seconds = DURATIONS.TWELVE_HOURS;
|
apiMock.mockResolvedValue({
|
||||||
const { req, res } = faker({ cache_seconds }, data_stats);
|
status: "error - temporary",
|
||||||
await api(req, res);
|
content: "temporary-error-svg",
|
||||||
|
});
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
const req = createRequest("username=anuraghazra");
|
||||||
["Content-Type", "image/svg+xml"],
|
const res = createResponse();
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${cache_seconds}, ` +
|
|
||||||
`s-maxage=${cache_seconds}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set shorter cache when error", async () => {
|
await router(req, res);
|
||||||
const { req, res } = faker({}, error);
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
|
||||||
["Content-Type", "image/svg+xml"],
|
expect(apiMock).toHaveBeenCalledWith(
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.ERROR}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.ERROR}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should properly set cache using CACHE_SECONDS env variable", async () => {
|
|
||||||
const cacheSeconds = "10000";
|
|
||||||
process.env.CACHE_SECONDS = cacheSeconds;
|
|
||||||
|
|
||||||
const { req, res } = faker({}, data_stats);
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "image/svg+xml"],
|
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${cacheSeconds}, ` +
|
|
||||||
`s-maxage=${cacheSeconds}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should disable cache when CACHE_SECONDS is set to 0", async () => {
|
|
||||||
process.env.CACHE_SECONDS = "0";
|
|
||||||
|
|
||||||
const { req, res } = faker({}, data_stats);
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "image/svg+xml"],
|
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
"no-cache, no-store, must-revalidate, max-age=0, s-maxage=0",
|
|
||||||
],
|
|
||||||
["Pragma", "no-cache"],
|
|
||||||
["Expires", "0"],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set proper cache with clamped values", async () => {
|
|
||||||
{
|
|
||||||
let { req, res } = faker({ cache_seconds: 200_000 }, data_stats);
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "image/svg+xml"],
|
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.STATS_CARD.MAX}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.STATS_CARD.MAX}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// note i'm using block scoped vars
|
|
||||||
{
|
|
||||||
let { req, res } = faker({ cache_seconds: 0 }, data_stats);
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "image/svg+xml"],
|
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.STATS_CARD.MIN}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.STATS_CARD.MIN}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let { req, res } = faker({ cache_seconds: -10_000 }, data_stats);
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "image/svg+xml"],
|
|
||||||
[
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.STATS_CARD.MIN}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.STATS_CARD.MIN}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow changing ring_color", async () => {
|
|
||||||
const { req, res } = faker(
|
|
||||||
{
|
{
|
||||||
username: "anuraghazra",
|
username: "anuraghazra",
|
||||||
hide: "issues,prs,contribs",
|
|
||||||
show_icons: true,
|
|
||||||
hide_border: true,
|
|
||||||
line_height: 100,
|
|
||||||
title_color: "fff",
|
|
||||||
ring_color: "0000ff",
|
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
},
|
},
|
||||||
data_stats,
|
null,
|
||||||
);
|
|
||||||
|
|
||||||
await api(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderStatsCard(stats, {
|
|
||||||
hide: ["issues", "prs", "contribs"],
|
|
||||||
show_icons: true,
|
|
||||||
hide_border: true,
|
|
||||||
line_height: 100,
|
|
||||||
title_color: "fff",
|
|
||||||
ring_color: "0000ff",
|
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
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 render error card when wrong locale is provided", async () => {
|
it("should not persist permanent stats errors returned by core", async () => {
|
||||||
const { req, res } = faker({ locale: "asdf" }, data_stats);
|
apiMock.mockResolvedValue({
|
||||||
|
status: "error - permanent",
|
||||||
|
content: "permanent-error-svg",
|
||||||
|
});
|
||||||
|
|
||||||
await api(req, res);
|
const req = createRequest("username=anuraghazra");
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
await router(req, res);
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
|
||||||
message: "Something went wrong",
|
expect(apiMock).toHaveBeenCalledWith(
|
||||||
secondaryMessage: "Language not found",
|
{
|
||||||
}),
|
username: "anuraghazra",
|
||||||
|
},
|
||||||
|
null,
|
||||||
);
|
);
|
||||||
|
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 render error card when include_all_commits true and upstream API fails", async () => {
|
it("should reject blacklisted usernames before calling core logic", async () => {
|
||||||
mock
|
const req = createRequest("username=renovate-bot");
|
||||||
.onGet(
|
const res = createResponse();
|
||||||
"https://api.github.com/search/commits?per_page=1&q=author:anuraghazra",
|
|
||||||
)
|
|
||||||
.reply(200, { error: "Some test error message" });
|
|
||||||
|
|
||||||
const { req, res } = faker(
|
await router(req, res);
|
||||||
{ username: "anuraghazra", include_all_commits: true },
|
|
||||||
data_stats,
|
|
||||||
);
|
|
||||||
|
|
||||||
await api(req, res);
|
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(storeRequestMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
it("should reject non-whitelisted usernames before calling core logic", async () => {
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
getConfigMock.mockReturnValue({ whitelist: ["allowed-user"] });
|
||||||
renderError({
|
|
||||||
message: "Could not fetch data from GitHub REST API.",
|
const req = createRequest("username=blocked-user");
|
||||||
secondaryMessage: "Please try again later",
|
const res = createResponse();
|
||||||
}),
|
|
||||||
);
|
await router(req, res);
|
||||||
// Received SVG output should not contain string "https://tiny.one/readme-stats"
|
|
||||||
expect(res.send.mock.calls[0][0]).not.toContain(
|
expect(apiMock).not.toHaveBeenCalled();
|
||||||
"https://tiny.one/readme-stats",
|
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(storeRequestMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,81 +1,40 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { bench, describe, vi } from "vitest";
|
import { beforeAll, bench, describe, vi } from "vitest";
|
||||||
|
|
||||||
import api from "../../api-renamed/index.js";
|
import { data_stats } from "../utils.js";
|
||||||
|
|
||||||
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: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
const faker = (query, data) => {
|
const createResponse = () => ({
|
||||||
const req = {
|
end: vi.fn(),
|
||||||
query: {
|
setHeader: vi.fn(),
|
||||||
username: "anuraghazra",
|
});
|
||||||
...query,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
|
|
||||||
|
|
||||||
return { req, res };
|
let router;
|
||||||
};
|
|
||||||
|
|
||||||
describe("/api", () => {
|
beforeAll(async () => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
({ default: router } = await import("../../router.js"));
|
||||||
|
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply(200, data_stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bench /api", () => {
|
||||||
bench(
|
bench(
|
||||||
"base",
|
"base",
|
||||||
async () => {
|
async () => {
|
||||||
const { req, res } = faker({}, data_stats);
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api?username=anuraghazra",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
await api(req, res);
|
await router(req, res);
|
||||||
},
|
},
|
||||||
{ warmupIterations: 50 },
|
{ warmupIterations: 50 },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { bench, describe } from "vitest";
|
|
||||||
|
|
||||||
import { calculateRank } from "../../src/calculateRank.js";
|
|
||||||
|
|
||||||
describe("calculateRank", () => {
|
|
||||||
bench(
|
|
||||||
"base",
|
|
||||||
async () => {
|
|
||||||
calculateRank({
|
|
||||||
all_commits: false,
|
|
||||||
commits: 1300,
|
|
||||||
prs: 1500,
|
|
||||||
issues: 4500,
|
|
||||||
reviews: 1000,
|
|
||||||
repos: 0,
|
|
||||||
stars: 600000,
|
|
||||||
followers: 50000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ warmupIterations: 50 },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,54 +1,42 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { bench, describe, vi } from "vitest";
|
import { beforeAll, bench, describe, vi } from "vitest";
|
||||||
|
|
||||||
import gist from "../../api-renamed/gist.js";
|
import { happy_path_gist_data } from "../utils.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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
|
||||||
|
|
||||||
describe("test /api/gist", () => {
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
({ default: router } = await import("../../router.js"));
|
||||||
|
|
||||||
|
mock
|
||||||
|
.onPost("https://api.github.com/graphql")
|
||||||
|
.reply(200, happy_path_gist_data);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bench /api/gist", () => {
|
||||||
bench(
|
bench(
|
||||||
"base",
|
"base",
|
||||||
async () => {
|
async () => {
|
||||||
const req = {
|
const req = {
|
||||||
query: {
|
headers: {},
|
||||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
url: "/api/gist?id=happy-gist-id",
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
await gist(req, res);
|
await router(req, res);
|
||||||
},
|
},
|
||||||
{ warmupIterations: 50 },
|
{ warmupIterations: 50 },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,53 +1,40 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { bench, describe, vi } from "vitest";
|
import { beforeAll, bench, describe, vi } from "vitest";
|
||||||
|
|
||||||
import pin from "../../api-renamed/pin.js";
|
import { data_user } from "../utils.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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
|
||||||
|
|
||||||
describe("/api/pin", () => {
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
({ default: router } = await import("../../router.js"));
|
||||||
|
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bench /api/pin", () => {
|
||||||
bench(
|
bench(
|
||||||
"base",
|
"base",
|
||||||
async () => {
|
async () => {
|
||||||
const req = {
|
const req = {
|
||||||
query: {
|
headers: {},
|
||||||
username: "anuraghazra",
|
url: "/api/pin?username=anuraghazra&repo=convoychat",
|
||||||
repo: "convoychat",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
await pin(req, res);
|
await router(req, res);
|
||||||
},
|
},
|
||||||
{ warmupIterations: 50 },
|
{ warmupIterations: 50 },
|
||||||
);
|
);
|
||||||
|
|||||||
+244
-122
@@ -3,221 +3,343 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { beforeAll, describe, expect, test } from "vitest";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
|
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
||||||
import { renderGistCard } from "../../src/cards/gist.js";
|
|
||||||
import { renderRepoCard } from "../../src/cards/repo.js";
|
|
||||||
import { renderStatsCard } from "../../src/cards/stats.js";
|
|
||||||
import { renderTopLanguages } from "../../src/cards/top-languages.js";
|
|
||||||
import { renderWakatimeCard } from "../../src/cards/wakatime.js";
|
|
||||||
|
|
||||||
const REPO = "curly-fiesta";
|
const REPO = "curly-fiesta";
|
||||||
const USER = "catelinemnemosyne";
|
const USER = "catelinemnemosyne";
|
||||||
const STATS_CARD_USER = "e2eninja";
|
const STATS_CARD_USER = "e2eninja";
|
||||||
const GIST_ID = "372cef55fd897b31909fdeb3a7262758";
|
const GIST_ID = "372cef55fd897b31909fdeb3a7262758";
|
||||||
|
|
||||||
const STATS_DATA = {
|
const STATS_MOCK_RESPONSE = {
|
||||||
name: "CodeNinja",
|
data: {
|
||||||
totalPRs: 1,
|
user: {
|
||||||
totalReviews: 0,
|
name: "CodeNinja",
|
||||||
totalCommits: 3,
|
login: STATS_CARD_USER,
|
||||||
totalIssues: 1,
|
repositoriesContributedTo: { totalCount: 0 },
|
||||||
totalStars: 1,
|
commits: {
|
||||||
contributedTo: 0,
|
totalCommitContributions: 3,
|
||||||
rank: {
|
},
|
||||||
level: "C",
|
reviews: {
|
||||||
percentile: 98.73972605284538,
|
totalPullRequestReviewContributions: 0,
|
||||||
|
},
|
||||||
|
pullRequests: { totalCount: 1 },
|
||||||
|
openIssues: { totalCount: 1 },
|
||||||
|
closedIssues: { totalCount: 0 },
|
||||||
|
followers: { totalCount: 0 },
|
||||||
|
repositories: {
|
||||||
|
totalCount: 1,
|
||||||
|
nodes: [{ name: REPO, stargazers: { totalCount: 1 } }],
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
endCursor: "cursor",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const LANGS_DATA = {
|
const COMMITS_SEARCH_MOCK_RESPONSE = {
|
||||||
HTML: {
|
total_count: 3,
|
||||||
color: "#e34c26",
|
};
|
||||||
name: "HTML",
|
|
||||||
size: 1721,
|
const TOP_LANGS_MOCK_RESPONSE = {
|
||||||
},
|
data: {
|
||||||
CSS: {
|
user: {
|
||||||
color: "#663399",
|
repositories: {
|
||||||
name: "CSS",
|
nodes: [
|
||||||
size: 930,
|
{
|
||||||
},
|
name: REPO,
|
||||||
JavaScript: {
|
languages: {
|
||||||
color: "#f1e05a",
|
edges: [
|
||||||
name: "JavaScript",
|
{
|
||||||
size: 1912,
|
size: 1721,
|
||||||
|
node: {
|
||||||
|
color: "#e34c26",
|
||||||
|
name: "HTML",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 930,
|
||||||
|
node: {
|
||||||
|
color: "#663399",
|
||||||
|
name: "CSS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 1912,
|
||||||
|
node: {
|
||||||
|
color: "#f1e05a",
|
||||||
|
name: "JavaScript",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const WAKATIME_DATA = {
|
const WAKATIME_MOCK_RESPONSE = {
|
||||||
human_readable_range: "last week",
|
data: {
|
||||||
is_already_updating: false,
|
human_readable_range: "last week",
|
||||||
is_coding_activity_visible: true,
|
is_already_updating: false,
|
||||||
is_including_today: false,
|
is_coding_activity_visible: true,
|
||||||
is_other_usage_visible: false,
|
is_including_today: false,
|
||||||
is_stuck: false,
|
is_other_usage_visible: false,
|
||||||
is_up_to_date: false,
|
is_stuck: false,
|
||||||
is_up_to_date_pending_future: false,
|
is_up_to_date: false,
|
||||||
percent_calculated: 0,
|
is_up_to_date_pending_future: false,
|
||||||
range: "all_time",
|
percent_calculated: 0,
|
||||||
status: "pending_update",
|
range: "all_time",
|
||||||
timeout: 15,
|
status: "pending_update",
|
||||||
username: USER,
|
timeout: 15,
|
||||||
writes_only: false,
|
username: USER,
|
||||||
|
writes_only: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const REPOSITORY_DATA = {
|
const REPO_MOCK_RESPONSE = {
|
||||||
name: REPO,
|
data: {
|
||||||
nameWithOwner: `${USER}/cra-test`,
|
user: {
|
||||||
isPrivate: false,
|
repository: {
|
||||||
isArchived: false,
|
name: REPO,
|
||||||
isTemplate: false,
|
nameWithOwner: `${USER}/cra-test`,
|
||||||
stargazers: {
|
isPrivate: false,
|
||||||
totalCount: 1,
|
isArchived: false,
|
||||||
|
isTemplate: false,
|
||||||
|
stargazers: {
|
||||||
|
totalCount: 1,
|
||||||
|
},
|
||||||
|
description: "Simple cra test repo.",
|
||||||
|
primaryLanguage: {
|
||||||
|
color: "#f1e05a",
|
||||||
|
id: "MDg6TGFuZ3VhZ2UxNDA=",
|
||||||
|
name: "JavaScript",
|
||||||
|
},
|
||||||
|
forkCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
organization: null,
|
||||||
},
|
},
|
||||||
description: "Simple cra test repo.",
|
|
||||||
primaryLanguage: {
|
|
||||||
color: "#f1e05a",
|
|
||||||
id: "MDg6TGFuZ3VhZ2UxNDA=",
|
|
||||||
name: "JavaScript",
|
|
||||||
},
|
|
||||||
forkCount: 0,
|
|
||||||
starCount: 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const GIST_MOCK_RESPONSE = {
|
||||||
* @typedef {import("../../src/fetchers/types").GistData} GistData Gist data type.
|
data: {
|
||||||
*/
|
viewer: {
|
||||||
|
gist: {
|
||||||
/**
|
description:
|
||||||
* @type {GistData}
|
"Trying to access this path on Windows 10 ver. 1803+ will breaks NTFS",
|
||||||
*/
|
owner: {
|
||||||
const GIST_DATA = {
|
login: "qwerty541",
|
||||||
name: "link.txt",
|
},
|
||||||
nameWithOwner: "qwerty541/link.txt",
|
stargazerCount: 1,
|
||||||
description:
|
forks: {
|
||||||
"Trying to access this path on Windows 10 ver. 1803+ will breaks NTFS",
|
totalCount: 0,
|
||||||
language: "Text",
|
},
|
||||||
starsCount: 1,
|
files: [
|
||||||
forksCount: 0,
|
{
|
||||||
|
name: "link.txt",
|
||||||
|
language: {
|
||||||
|
name: "Text",
|
||||||
|
},
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const CACHE_BURST_STRING = `v=${new Date().getTime()}`;
|
const CACHE_BURST_STRING = `v=${new Date().getTime()}`;
|
||||||
|
|
||||||
describe("Fetch Cards", () => {
|
const mock = new MockAdapter(axios, { onNoMatch: "passthrough" });
|
||||||
let VERCEL_PREVIEW_URL = "https://github-stats-extended.vercel.app";
|
|
||||||
|
|
||||||
beforeAll(() => {
|
const createResponse = () => ({
|
||||||
process.env.NODE_ENV = "development";
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a card locally through the backend router.
|
||||||
|
* @param {string} url Card URL to render through the router.
|
||||||
|
* @returns {Promise<string>} Rendered SVG markup.
|
||||||
|
*/
|
||||||
|
async function getLocalSvg(url) {
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
return res.end.mock.calls[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
vi.stubEnv("PAT_1", "dummyPAT1");
|
||||||
|
vi.stubEnv("PAT_2", "dummyPAT2");
|
||||||
|
|
||||||
|
({ default: router } = await import("../../router.js"));
|
||||||
|
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply((config) => {
|
||||||
|
const { query, variables } = JSON.parse(config.data);
|
||||||
|
|
||||||
|
if (
|
||||||
|
query.includes("query userInfo") &&
|
||||||
|
variables?.login === STATS_CARD_USER
|
||||||
|
) {
|
||||||
|
return [200, STATS_MOCK_RESPONSE];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.includes("query userInfo") && variables?.login === USER) {
|
||||||
|
return [200, TOP_LANGS_MOCK_RESPONSE];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.includes("query getRepo")) {
|
||||||
|
return [200, REPO_MOCK_RESPONSE];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.includes("query gistInfo")) {
|
||||||
|
return [200, GIST_MOCK_RESPONSE];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [500, { error: "Unhandled GraphQL request in e2e test" }];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
`https://api.github.com/search/commits?per_page=1&q=author:${STATS_CARD_USER}`,
|
||||||
|
)
|
||||||
|
.reply(200, COMMITS_SEARCH_MOCK_RESPONSE);
|
||||||
|
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
`https://wakatime.com/api/v1/users/${USER}/stats?is_including_today=true`,
|
||||||
|
)
|
||||||
|
.reply(200, WAKATIME_MOCK_RESPONSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mock.restore();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Fetch Cards", () => {
|
||||||
|
const VERCEL_PREVIEW_URL = "https://github-stats-extended-preview.vercel.app";
|
||||||
|
|
||||||
test("retrieve stats card", async () => {
|
test("retrieve stats card", async () => {
|
||||||
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
||||||
|
|
||||||
|
const cardPath = `/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`;
|
||||||
|
|
||||||
// Check if the Vercel preview instance stats card function is up and running.
|
// Check if the Vercel preview instance stats card function is up and running.
|
||||||
await expect(
|
await expect(
|
||||||
axios.get(`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}`),
|
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
// Get local stats card.
|
// Get local stats card.
|
||||||
const localStatsCardSVG = renderStatsCard(STATS_DATA, {
|
const localStatsCardSVG = await getLocalSvg(cardPath);
|
||||||
include_all_commits: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the Vercel preview stats card response.
|
// Get the Vercel preview stats card response.
|
||||||
const serverStatsSvg = await axios.get(
|
const serverStatsSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`);
|
||||||
`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if stats card from deployment matches the stats card from local.
|
// Check if stats card from deployment matches the stats card from local.
|
||||||
expect(serverStatsSvg.data).toEqual(localStatsCardSVG);
|
expect(serverStatsSvg.data).toEqual(localStatsCardSVG);
|
||||||
}, 15000);
|
}, 20000);
|
||||||
|
|
||||||
test("retrieve language card", async () => {
|
test("retrieve language card", async () => {
|
||||||
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
||||||
|
|
||||||
|
const cardPath = `/api/top-langs?username=${USER}&${CACHE_BURST_STRING}`;
|
||||||
|
|
||||||
// Check if the Vercel preview instance language card function is up and running.
|
// Check if the Vercel preview instance language card function is up and running.
|
||||||
console.log(
|
|
||||||
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
|
|
||||||
);
|
|
||||||
await expect(
|
await expect(
|
||||||
axios.get(
|
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
|
||||||
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
|
|
||||||
),
|
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
// Get local language card.
|
// Get local language card.
|
||||||
const localLanguageCardSVG = renderTopLanguages(LANGS_DATA);
|
const localLanguageCardSVG = await getLocalSvg(cardPath);
|
||||||
|
|
||||||
// Get the Vercel preview language card response.
|
// Get the Vercel preview language card response.
|
||||||
const severLanguageSVG = await axios.get(
|
const serverLanguageSVG = await axios.get(
|
||||||
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
|
`${VERCEL_PREVIEW_URL}${cardPath}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if language card from deployment matches the local language card.
|
// Check if language card from deployment matches the local language card.
|
||||||
expect(severLanguageSVG.data).toEqual(localLanguageCardSVG);
|
expect(serverLanguageSVG.data).toEqual(localLanguageCardSVG);
|
||||||
}, 15000);
|
}, 20000);
|
||||||
|
|
||||||
test("retrieve WakaTime card", async () => {
|
test("retrieve WakaTime card", async () => {
|
||||||
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
||||||
|
|
||||||
|
const cardPath = `/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`;
|
||||||
|
|
||||||
// Check if the Vercel preview instance WakaTime function is up and running.
|
// Check if the Vercel preview instance WakaTime function is up and running.
|
||||||
await expect(
|
await expect(
|
||||||
axios.get(`${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}`),
|
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
// Get local WakaTime card.
|
// Get local WakaTime card.
|
||||||
const localWakaCardSVG = renderWakatimeCard(WAKATIME_DATA);
|
const localWakaCardSVG = await getLocalSvg(cardPath);
|
||||||
|
|
||||||
// Get the Vercel preview WakaTime card response.
|
// Get the Vercel preview WakaTime card response.
|
||||||
const serverWakaTimeSvg = await axios.get(
|
const serverWakaTimeSvg = await axios.get(
|
||||||
`${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`,
|
`${VERCEL_PREVIEW_URL}${cardPath}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if WakaTime card from deployment matches the local WakaTime card.
|
// Check if WakaTime card from deployment matches the local WakaTime card.
|
||||||
expect(serverWakaTimeSvg.data).toEqual(localWakaCardSVG);
|
expect(serverWakaTimeSvg.data).toEqual(localWakaCardSVG);
|
||||||
}, 15000);
|
}, 20000);
|
||||||
|
|
||||||
test("retrieve repo card", async () => {
|
test("retrieve repo card", async () => {
|
||||||
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
||||||
|
|
||||||
|
const cardPath = `/api/pin?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`;
|
||||||
|
|
||||||
// Check if the Vercel preview instance Repo function is up and running.
|
// Check if the Vercel preview instance Repo function is up and running.
|
||||||
await expect(
|
await expect(
|
||||||
axios.get(
|
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
|
||||||
`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`,
|
|
||||||
),
|
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
// Get local repo card.
|
// Get local repo card.
|
||||||
const localRepoCardSVG = renderRepoCard(REPOSITORY_DATA);
|
const localRepoCardSVG = await getLocalSvg(cardPath);
|
||||||
|
|
||||||
// Get the Vercel preview repo card response.
|
// Get the Vercel preview repo card response.
|
||||||
const serverRepoSvg = await axios.get(
|
const serverRepoSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`);
|
||||||
`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if Repo card from deployment matches the local Repo card.
|
// Check if Repo card from deployment matches the local Repo card.
|
||||||
expect(serverRepoSvg.data).toEqual(localRepoCardSVG);
|
expect(serverRepoSvg.data).toEqual(localRepoCardSVG);
|
||||||
}, 15000);
|
}, 20000);
|
||||||
|
|
||||||
test("retrieve gist card", async () => {
|
test("retrieve gist card", async () => {
|
||||||
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
expect(VERCEL_PREVIEW_URL).toBeDefined();
|
||||||
|
|
||||||
|
const cardPath = `/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`;
|
||||||
|
|
||||||
// Check if the Vercel preview instance Gist function is up and running.
|
// Check if the Vercel preview instance Gist function is up and running.
|
||||||
await expect(
|
await expect(
|
||||||
axios.get(
|
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
|
||||||
`${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`,
|
|
||||||
),
|
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
// Get local gist card.
|
// Get local gist card.
|
||||||
const localGistCardSVG = renderGistCard(GIST_DATA);
|
const localGistCardSVG = await getLocalSvg(cardPath);
|
||||||
|
|
||||||
// Get the Vercel preview gist card response.
|
// Get the Vercel preview gist card response.
|
||||||
const serverGistSvg = await axios.get(
|
const serverGistSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`);
|
||||||
`${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if Gist card from deployment matches the local Gist card.
|
// Check if Gist card from deployment matches the local Gist card.
|
||||||
expect(serverGistSvg.data).toEqual(localGistCardSVG);
|
expect(serverGistSvg.data).toEqual(localGistCardSVG);
|
||||||
}, 15000);
|
}, 20000);
|
||||||
});
|
});
|
||||||
|
|||||||
+124
-136
@@ -1,161 +1,149 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import axios from "axios";
|
import { getConfig, gist } from "@stats-organization/github-readme-stats-core";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import gist from "../api-renamed/gist.js";
|
import router from "../router.js";
|
||||||
import { renderGistCard } from "../src/cards/gist.js";
|
|
||||||
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
||||||
import { renderError } from "../src/common/render.js";
|
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
|
||||||
|
|
||||||
import { gist_data } from "./test-data/gist-data.js";
|
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
|
||||||
|
const { mockCore } = await import("./utils.js");
|
||||||
import "@testing-library/jest-dom/vitest";
|
return mockCore();
|
||||||
|
|
||||||
const gist_not_found_data = {
|
|
||||||
data: {
|
|
||||||
viewer: {
|
|
||||||
gist: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/gist", () => {
|
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
|
||||||
it("should test the request", async () => {
|
...(await importOriginal()),
|
||||||
const req = {
|
storeRequest: vi.fn(),
|
||||||
query: {
|
getUserAccessByName: vi.fn(),
|
||||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
}));
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
|
||||||
|
|
||||||
await gist(req, res);
|
const gistMock = vi.mocked(gist);
|
||||||
|
const getConfigMock = vi.mocked(getConfig);
|
||||||
|
const storeRequestMock = vi.mocked(storeRequest);
|
||||||
|
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
const createRequest = (search) => ({
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
headers: {},
|
||||||
renderGistCard({
|
url: `/api/gist?${search}`,
|
||||||
name: gist_data.data.viewer.gist.files[0].name,
|
});
|
||||||
nameWithOwner: `${gist_data.data.viewer.gist.owner.login}/${gist_data.data.viewer.gist.files[0].name}`,
|
|
||||||
description: gist_data.data.viewer.gist.description,
|
const createResponse = () => ({
|
||||||
language: gist_data.data.viewer.gist.files[0].language.name,
|
end: vi.fn(),
|
||||||
starsCount: gist_data.data.viewer.gist.stargazerCount,
|
setHeader: vi.fn(),
|
||||||
forksCount: gist_data.data.viewer.gist.forks.totalCount,
|
});
|
||||||
}),
|
|
||||||
);
|
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 get the query options", async () => {
|
it("should use the shorter error cache for temporary gist errors", async () => {
|
||||||
const req = {
|
gistMock.mockResolvedValue({
|
||||||
query: {
|
status: "error - temporary",
|
||||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
content: "temporary-error-svg",
|
||||||
title_color: "fff",
|
});
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
show_owner: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
|
||||||
|
|
||||||
await gist(req, res);
|
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d");
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
await router(req, res);
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderGistCard(
|
expect(gistMock).toHaveBeenCalledWith({
|
||||||
{
|
id: "bbfce31e0217a3689c8d961a356cb10d",
|
||||||
name: gist_data.data.viewer.gist.files[0].name,
|
});
|
||||||
nameWithOwner: `${gist_data.data.viewer.gist.owner.login}/${gist_data.data.viewer.gist.files[0].name}`,
|
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
|
||||||
description: gist_data.data.viewer.gist.description,
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
language: gist_data.data.viewer.gist.files[0].language.name,
|
["Cache-Control", errorCacheHeader],
|
||||||
starsCount: gist_data.data.viewer.gist.stargazerCount,
|
["Content-Type", "image/svg+xml"],
|
||||||
forksCount: gist_data.data.viewer.gist.forks.totalCount,
|
]);
|
||||||
},
|
expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg");
|
||||||
{ ...req.query },
|
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render error if gist is not found", async () => {
|
it("should not persist permanent gist errors returned by core", async () => {
|
||||||
const req = {
|
gistMock.mockResolvedValue({
|
||||||
query: {
|
status: "error - permanent",
|
||||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
content: "permanent-error-svg",
|
||||||
},
|
});
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(200, gist_not_found_data);
|
|
||||||
|
|
||||||
await gist(req, res);
|
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d");
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
await router(req, res);
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({ message: "Gist not found" }),
|
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 render error if wrong locale is provided", async () => {
|
it("should reject non-whitelisted gist ids before calling core logic", async () => {
|
||||||
const req = {
|
getConfigMock.mockReturnValue({ gistWhitelist: ["allowed-gist-id"] });
|
||||||
query: {
|
|
||||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
|
||||||
locale: "asdf",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
|
||||||
|
|
||||||
await gist(req, res);
|
const req = createRequest("id=blocked-gist-id");
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
await router(req, res);
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
|
||||||
message: "Something went wrong",
|
|
||||||
secondaryMessage: "Language not found",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache", async () => {
|
expect(gistMock).not.toHaveBeenCalled();
|
||||||
const req = {
|
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
|
||||||
query: {
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
id: "bbfce31e0217a3689c8d961a356cb10d",
|
["Cache-Control", defaultCacheHeader],
|
||||||
},
|
["Content-Type", "image/svg+xml"],
|
||||||
};
|
]);
|
||||||
const res = {
|
expect(res.end).toHaveBeenCalledExactlyOnceWith(
|
||||||
setHeader: vi.fn(),
|
"render-error:This gist ID is not whitelisted",
|
||||||
send: vi.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.setHeader).toHaveBeenCalledWith(
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
);
|
);
|
||||||
|
expect(storeRequestMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,15 @@
|
|||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterAll,
|
||||||
import patInfo, { RATE_LIMIT_SECONDS } from "../api-renamed/status/pat-info.js";
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
@@ -57,20 +63,35 @@ const bad_credentials_error = {
|
|||||||
message: "Bad credentials",
|
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(() => {
|
afterEach(() => {
|
||||||
mock.reset();
|
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", () => {
|
describe("Test /api/status/pat-info", () => {
|
||||||
beforeAll(() => {
|
|
||||||
// reset patenv first so that they are not populated with local envs
|
|
||||||
process.env = {};
|
|
||||||
process.env.PAT_1 = "testPAT1";
|
|
||||||
process.env.PAT_2 = "testPAT2";
|
|
||||||
process.env.PAT_3 = "testPAT3";
|
|
||||||
process.env.PAT_4 = "testPAT4";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return only 'validPATs' if all PATs are valid", async () => {
|
it("should return only 'validPATs' if all PATs are valid", async () => {
|
||||||
mock
|
mock
|
||||||
.onPost("https://api.github.com/graphql")
|
.onPost("https://api.github.com/graphql")
|
||||||
@@ -243,7 +264,6 @@ describe("Test /api/status/pat-info", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should have proper cache when error is thrown", async () => {
|
it("should have proper cache when error is thrown", async () => {
|
||||||
mock.reset();
|
|
||||||
mock.onPost("https://api.github.com/graphql").networkError();
|
mock.onPost("https://api.github.com/graphql").networkError();
|
||||||
|
|
||||||
const { req, res } = faker({}, {});
|
const { req, res } = faker({}, {});
|
||||||
|
|||||||
+71
-162
@@ -1,175 +1,84 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import axios from "axios";
|
import { pin } from "@stats-organization/github-readme-stats-core";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import pin from "../api-renamed/pin.js";
|
import router from "../router.js";
|
||||||
import { renderRepoCard } from "../src/cards/repo.js";
|
|
||||||
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
||||||
import { renderError } from "../src/common/render.js";
|
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
|
||||||
|
|
||||||
import { data_repo, data_user } from "./test-data/pin-data.js";
|
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
|
||||||
|
const { mockCore } = await import("./utils.js");
|
||||||
import "@testing-library/jest-dom/vitest";
|
return mockCore();
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/pin", () => {
|
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
|
||||||
it("should test the request", async () => {
|
...(await importOriginal()),
|
||||||
const req = {
|
storeRequest: vi.fn(),
|
||||||
query: {
|
getUserAccessByName: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const pinMock = vi.mocked(pin);
|
||||||
|
const storeRequestMock = vi.mocked(storeRequest);
|
||||||
|
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
|
||||||
|
|
||||||
|
const createRequest = (search = "") => ({
|
||||||
|
headers: {},
|
||||||
|
url: `/api/pin?${search}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultCacheHeader =
|
||||||
|
`max-age=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
|
||||||
|
`s-maxage=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
|
||||||
|
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pinMock.mockReset();
|
||||||
|
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/pin backend routing", () => {
|
||||||
|
it("happy path should pass query params and user PAT, respond with pin content and persist request", async () => {
|
||||||
|
getUserAccessByNameMock.mockResolvedValue({ token: "user-pat" });
|
||||||
|
pinMock.mockResolvedValue({
|
||||||
|
status: "success",
|
||||||
|
content: "mock-pin-svg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = createRequest(
|
||||||
|
"username=anuraghazra&repo=convoychat&theme=dark",
|
||||||
|
);
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
|
||||||
|
expect(pinMock).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
username: "anuraghazra",
|
username: "anuraghazra",
|
||||||
repo: "convoychat",
|
repo: "convoychat",
|
||||||
|
theme: "dark",
|
||||||
},
|
},
|
||||||
};
|
"user-pat",
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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(
|
|
||||||
// @ts-ignore
|
|
||||||
renderRepoCard({
|
|
||||||
...data_repo.repository,
|
|
||||||
starCount: data_repo.repository.stargazers.totalCount,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get the query options", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
repo: "convoychat",
|
|
||||||
title_color: "fff",
|
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
full_name: "1",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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(
|
|
||||||
renderRepoCard(
|
|
||||||
// @ts-ignore
|
|
||||||
{
|
|
||||||
...data_repo.repository,
|
|
||||||
starCount: data_repo.repository.stargazers.totalCount,
|
|
||||||
},
|
|
||||||
{ ...req.query },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error card if user repo not found", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
repo: "convoychat",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(200, { data: { user: { repository: null }, organization: null } });
|
|
||||||
|
|
||||||
await pin(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({ message: "User Repository Not found" }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error card if org repo not found", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
repo: "convoychat",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock
|
|
||||||
.onPost("https://api.github.com/graphql")
|
|
||||||
.reply(200, { data: { user: null, organization: { repository: null } } });
|
|
||||||
|
|
||||||
await pin(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({ message: "Organization Repository Not found" }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error card if wrong locale provided", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
repo: "convoychat",
|
|
||||||
locale: "asdf",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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: "Something went wrong",
|
|
||||||
secondaryMessage: "Language not found",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
repo: "convoychat",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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.setHeader).toHaveBeenCalledWith(
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
);
|
);
|
||||||
|
expect(req.query).toEqual({
|
||||||
|
username: "anuraghazra",
|
||||||
|
repo: "convoychat",
|
||||||
|
theme: "dark",
|
||||||
|
});
|
||||||
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
|
["Cache-Control", defaultCacheHeader],
|
||||||
|
["Content-Type", "image/svg+xml"],
|
||||||
|
]);
|
||||||
|
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-pin-svg");
|
||||||
|
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
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: vi.fn(),
|
|
||||||
send: vi.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 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
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: vi.fn(),
|
|
||||||
send: vi.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 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
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: vi.fn(),
|
|
||||||
send: vi.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: vi.fn(),
|
|
||||||
send: vi.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 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
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: vi.fn(),
|
|
||||||
send: vi.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,514 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test /api contract > should match the private missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api contract > should match the public blacklisted-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is blacklisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api contract > should match the public happy-path response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="450" height="195" viewBox="0 0 450 195" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId">Anurag Hazra's GitHub Stats, Rank: A+</title>
|
||||||
|
<desc id="descId">Total Stars Earned: 14099, Total Commits (last year) : 200, Total PRs: 4000, Total Issues: 340, Contributed to (last year): 51</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;
|
||||||
|
}
|
||||||
|
.rank-text {
|
||||||
|
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58;
|
||||||
|
animation: scaleInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.rank-percentile-header {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.rank-percentile-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not_bold { font-weight: 400 }
|
||||||
|
.bold { font-weight: 700 }
|
||||||
|
.icon {
|
||||||
|
fill: #4c71f2;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-circle-rim {
|
||||||
|
stroke: #2f80ed;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 6;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.rank-circle {
|
||||||
|
stroke: #2f80ed;
|
||||||
|
stroke-dasharray: 250;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 6;
|
||||||
|
stroke-linecap: round;
|
||||||
|
opacity: 0.8;
|
||||||
|
transform-origin: -10px 8px;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
animation: rankAnimation 1s forwards ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rankAnimation {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: 251.32741228718345;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 25.665014275610705;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="449" 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">Anurag Hazra's GitHub Stats</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
<g data-testid="rank-circle" transform="translate(365, 47.5)">
|
||||||
|
<circle class="rank-circle-rim" cx="-10" cy="8" r="40"/>
|
||||||
|
<circle class="rank-circle" cx="-10" cy="8" r="40"/>
|
||||||
|
<g class="rank-text">
|
||||||
|
|
||||||
|
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
|
||||||
|
A+
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<svg x="0" y="0">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">Total Stars Earned:</text>
|
||||||
|
<text class="stat bold" x="204.01" y="12.5" data-testid="stars">14.1k</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 25)">
|
||||||
|
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">Total Commits (last year):</text>
|
||||||
|
<text class="stat bold" x="204.01" y="12.5" data-testid="commits">200</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 50)">
|
||||||
|
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">Total PRs:</text>
|
||||||
|
<text class="stat bold" x="204.01" y="12.5" data-testid="prs">4k</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 75)">
|
||||||
|
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">Total Issues:</text>
|
||||||
|
<text class="stat bold" x="204.01" y="12.5" data-testid="issues">340</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 100)">
|
||||||
|
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">Contributed to (last year):</text>
|
||||||
|
<text class="stat bold" x="204.01" y="12.5" data-testid="contribs">51</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api contract > should match the public many-params response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="540" height="365" viewBox="0 0 540 365" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId">a custom title, Rank: A+</title>
|
||||||
|
<desc id="descId">कुल अर्जित सितारे: 4100, कुल commits (2024) : 200, कुल PR: 4000, कुल PR का विलय: 3200, मर्ज किए गए PRs प्रतिशत: 80.0, कुल PRs की समीक्षा की गई: 1234, कुल चर्चाएँ शुरू हुईं: 222, कुल चर्चाओं के उत्तर: 111, (पिछले वर्ष) में योगदान दिया: 51</desc>
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||||
|
fill: #123456;
|
||||||
|
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: #abcdef;
|
||||||
|
}
|
||||||
|
@supports(-moz-appearance: auto) {
|
||||||
|
/* Selector detects Firefox */
|
||||||
|
.stat { font-size:12px; }
|
||||||
|
}
|
||||||
|
.stagger {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.rank-text {
|
||||||
|
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef;
|
||||||
|
animation: scaleInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.rank-percentile-header {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.rank-percentile-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not_bold { font-weight: 400 }
|
||||||
|
.bold { font-weight: 700 }
|
||||||
|
.icon {
|
||||||
|
fill: #ff00aa;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-circle-rim {
|
||||||
|
stroke: #654321;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 6;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.rank-circle {
|
||||||
|
stroke: #654321;
|
||||||
|
stroke-dasharray: 250;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 6;
|
||||||
|
stroke-linecap: round;
|
||||||
|
opacity: 0.8;
|
||||||
|
transform-origin: -10px 8px;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
animation: rankAnimation 1s forwards ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rankAnimation {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: 251.32741228718345;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 26.378312814443284;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { animation-duration: 0s !important; animation-delay: 0s !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="539" fill="#0f172a" 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">a custom title</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
<g data-testid="rank-circle" transform="translate(463.5, 132.5)">
|
||||||
|
<circle class="rank-circle-rim" cx="-10" cy="8" r="40"/>
|
||||||
|
<circle class="rank-circle" cx="-10" cy="8" r="40"/>
|
||||||
|
<g class="rank-text">
|
||||||
|
|
||||||
|
<svg x="-38" y="-30" height="66" width="66" aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" data-testid="github-rank-icon">
|
||||||
|
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<svg x="0" y="0">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल अर्जित सितारे:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="stars">4100</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 32)">
|
||||||
|
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल commits (2024):</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="commits">200</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 64)">
|
||||||
|
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल PR:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="prs">4000</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 96)">
|
||||||
|
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल PR का विलय:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="prs_merged">3200</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 128)">
|
||||||
|
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">मर्ज किए गए PRs प्रतिशत:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="prs_merged_percentage">80.0 %</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 160)">
|
||||||
|
<g class="stagger" style="animation-delay: 1200ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल PRs की समीक्षा की गई:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="reviews">1234</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 192)">
|
||||||
|
<g class="stagger" style="animation-delay: 1350ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल चर्चाएँ शुरू हुईं:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="discussions_started">222</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 224)">
|
||||||
|
<g class="stagger" style="animation-delay: 1500ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">कुल चर्चाओं के उत्तर:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="discussions_answered">111</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 256)">
|
||||||
|
<g class="stagger" style="animation-delay: 1650ms" transform="translate(25, 0)">
|
||||||
|
|
||||||
|
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<text class="stat not_bold" x="25" y="12.5">(पिछले वर्ष) में योगदान दिया:</text>
|
||||||
|
<text class="stat not_bold" x="219.01" y="12.5" data-testid="contribs">51</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"graphqlRequest": "{"query":"\\n query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection (from: $startTime) {\\n totalCommitContributions,\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n \\n repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {\\n totalCount\\n nodes {\\n name\\n stargazers {\\n totalCount\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n\\n }\\n }\\n","variables":{"login":"anuraghazra","first":100,"after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api contract > should match the public missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray"/>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api contract > should render error card in same theme as requested card 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray"/>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test /api/gist contract > should match the private missing-id response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This gist ID is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/gist contract > should match the public happy-path response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
|
||||||
|
|
||||||
|
}
|
||||||
|
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.icon { fill: #586069 }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="card-title" transform="translate(25, 35)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(25, 0)">
|
||||||
|
<text x="0" y="0" class="header" data-testid="header">countries.json</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
<text class="description" x="25" y="-5">
|
||||||
|
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<g transform="translate(30, 105)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g data-testid="primary-lang">
|
||||||
|
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
|
||||||
|
<text data-testid="lang-name" class="gray" x="15">JSON</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="forksCount" class="gray">11</text></g></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/gist contract > should match the public many-params response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||||
|
fill: #123456;
|
||||||
|
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
@supports(-moz-appearance: auto) {
|
||||||
|
/* Selector detects Firefox */
|
||||||
|
.header { font-size: 15.5px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
|
||||||
|
|
||||||
|
}
|
||||||
|
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||||
|
.icon { fill: #ff00aa }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="399" fill="#0f172a" stroke-opacity="1"/>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="card-title" transform="translate(25, 35)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(25, 0)">
|
||||||
|
<text x="0" y="0" class="header" data-testid="header">Yizack/countries.json</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
<text class="description" x="25" y="-5">
|
||||||
|
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<g transform="translate(30, 105)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g data-testid="primary-lang">
|
||||||
|
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
|
||||||
|
<text data-testid="lang-name" class="gray" x="15">JSON</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="forksCount" class="gray">11</text></g></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n name\\n language {\\n name\\n }\\n size\\n }\\n }\\n }\\n}\\n","variables":{"gistName":"happy-gist-id"}}",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/gist contract > should match the public missing-id response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "id" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">/api/gist?id=GIST_ID</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should match the private missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should match the private non-whitelisted username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should match the public blacklisted-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is blacklisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should match the public happy-path response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="400" height="150" viewBox="0 0 400 150" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
|
||||||
|
|
||||||
|
}
|
||||||
|
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||||
|
.badge rect { opacity: 0.2 }
|
||||||
|
|
||||||
|
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.stagger {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.not_bold { font-weight: 400 }
|
||||||
|
.bold { font-weight: 700 }
|
||||||
|
.icon {
|
||||||
|
fill: #586069;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { animation-duration: 0s !important; animation-delay: 0s !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="card-title" transform="translate(25, 35)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(25, 0)">
|
||||||
|
<text x="0" y="0" class="header" data-testid="header">convoychat</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<text class="description" x="25" y="-5">
|
||||||
|
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<g transform="translate(30, 75)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g data-testid="primary-lang">
|
||||||
|
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
|
||||||
|
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="forkcount" class="gray">100</text></g></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<svg x="0" y="0"><g transform="translate(-3, 98)">
|
||||||
|
|
||||||
|
</g></svg>
|
||||||
|
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should match the public many-params response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="560" height="233" viewBox="0 0 560 233" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||||
|
fill: #123456;
|
||||||
|
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
@supports(-moz-appearance: auto) {
|
||||||
|
/* Selector detects Firefox */
|
||||||
|
.header { font-size: 15.5px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #abcdef;
|
||||||
|
|
||||||
|
}
|
||||||
|
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||||
|
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
|
||||||
|
.badge rect { opacity: 0.2 }
|
||||||
|
|
||||||
|
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
|
||||||
|
.stagger {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.not_bold { font-weight: 400 }
|
||||||
|
.bold { font-weight: 700 }
|
||||||
|
.icon {
|
||||||
|
fill: #ff00aa;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { animation-duration: 0s !important; animation-delay: 0s !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="559" fill="#0f172a" stroke-opacity="1"/>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="card-title" transform="translate(25, 35)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(25, 0)">
|
||||||
|
<text x="0" y="0" class="header" data-testid="header">anuraghazra/convoychat</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<text class="description" x="25" y="-5">
|
||||||
|
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable React, TypeScript and GraphQL...</tspan>
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<g transform="translate(30, 45)">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g data-testid="primary-lang">
|
||||||
|
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
|
||||||
|
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
|
||||||
|
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
|
||||||
|
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
|
||||||
|
</svg>
|
||||||
|
</g><g transform="translate(20, 0)"><text data-testid="forkcount" class="gray">100</text></g></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<svg x="0" y="0"><g transform="translate(-3, 68)">
|
||||||
|
<g transform="translate(0, 0)"><g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20author%3Aanuraghazra&type=pullrequests">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">my created PRs:</text>
|
||||||
|
<text class="stat bold" x="139.01" y="12.5" data-testid="prs_authored">1234</text></a>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(210, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20commenter%3Aanuraghazra+-author%3Aanuraghazra&type=pullrequests">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">my commented PRs:</text>
|
||||||
|
<text class="stat bold" x="139.01" y="12.5" data-testid="prs_commented">2345</text></a>
|
||||||
|
</g>
|
||||||
|
</g></g><g transform="translate(0, 30)"><g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20reviewed-by%3Aanuraghazra+-author%3Aanuraghazra&type=pullrequests">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">my reviewed PRs:</text>
|
||||||
|
<text class="stat bold" x="139.01" y="12.5" data-testid="prs_reviewed">3456</text></a>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(210, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20author%3Aanuraghazra&type=issues">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">my created issues:</text>
|
||||||
|
<text class="stat bold" x="139.01" y="12.5" data-testid="issues_authored">4567</text></a>
|
||||||
|
</g>
|
||||||
|
</g></g><g transform="translate(0, 60)"><g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20commenter%3Aanuraghazra+-author%3Aanuraghazra&type=issues">
|
||||||
|
|
||||||
|
<text class="stat bold" y="12.5">my commented issues:</text>
|
||||||
|
<text class="stat bold" x="139.01" y="12.5" data-testid="issues_commented">5678</text></a>
|
||||||
|
</g>
|
||||||
|
</g></g>
|
||||||
|
</g></svg>
|
||||||
|
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"graphqlRequest": "{"query":"\\n fragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazers {\\n totalCount\\n }\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n }\\n query getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","repo":"convoychat"}}",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should match the public missing-params response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username", "repo" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">/api/pin?username=USERNAME&repo=REPO_NAME</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/pin contract > should render error card in same theme as requested card 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username", "repo" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">/api/pin?username=USERNAME&repo=REPO_NAME</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should match the private missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should match the private non-whitelisted username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should match the public blacklisted-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is blacklisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should match the public happy-path response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="300" height="285" viewBox="0 0 300 285" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInAnimation {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: calc(100%-100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes growWidthAnimation {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
|
||||||
|
}
|
||||||
|
@supports(-moz-appearance: auto) {
|
||||||
|
/* Selector detects Firefox */
|
||||||
|
.stat { font-size:12px; }
|
||||||
|
}
|
||||||
|
.bold { font-weight: 700 }
|
||||||
|
.lang-name {
|
||||||
|
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
|
||||||
|
fill: #434d58;
|
||||||
|
}
|
||||||
|
.stagger {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
#rect-mask rect{
|
||||||
|
animation: slideInAnimation 1s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.lang-progress{
|
||||||
|
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="299" 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">Most Used Languages</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
<svg data-testid="lang-items" x="25">
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms">
|
||||||
|
<text data-testid="lang-name" x="2" y="15" class="lang-name">Rust</text>
|
||||||
|
<text x="215" y="34" class="lang-name">70.92%</text>
|
||||||
|
|
||||||
|
<svg width="205" x="0" y="25">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
|
||||||
|
<svg data-testid="lang-progress" width="70.92198581560284%">
|
||||||
|
<rect height="8" fill="#dea584" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 40)">
|
||||||
|
<g class="stagger" style="animation-delay: 600ms">
|
||||||
|
<text data-testid="lang-name" x="2" y="15" class="lang-name">HTML</text>
|
||||||
|
<text x="215" y="34" class="lang-name">12.77%</text>
|
||||||
|
|
||||||
|
<svg width="205" x="0" y="25">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
|
||||||
|
<svg data-testid="lang-progress" width="12.76595744680851%">
|
||||||
|
<rect height="8" fill="#0f0" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 80)">
|
||||||
|
<g class="stagger" style="animation-delay: 750ms">
|
||||||
|
<text data-testid="lang-name" x="2" y="15" class="lang-name">JavaScript</text>
|
||||||
|
<text x="215" y="34" class="lang-name">8.51%</text>
|
||||||
|
|
||||||
|
<svg width="205" x="0" y="25">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
|
||||||
|
<svg data-testid="lang-progress" width="8.51063829787234%">
|
||||||
|
<rect height="8" fill="#0ff" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 120)">
|
||||||
|
<g class="stagger" style="animation-delay: 900ms">
|
||||||
|
<text data-testid="lang-name" x="2" y="15" class="lang-name">TypeScript</text>
|
||||||
|
<text x="215" y="34" class="lang-name">6.38%</text>
|
||||||
|
|
||||||
|
<svg width="205" x="0" y="25">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
|
||||||
|
<svg data-testid="lang-progress" width="6.382978723404255%">
|
||||||
|
<rect height="8" fill="#3178c6" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 160)">
|
||||||
|
<g class="stagger" style="animation-delay: 1050ms">
|
||||||
|
<text data-testid="lang-name" x="2" y="15" class="lang-name">Java</text>
|
||||||
|
<text x="215" y="34" class="lang-name">1.42%</text>
|
||||||
|
|
||||||
|
<svg width="205" x="0" y="25">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8" fill="#ddd"/>
|
||||||
|
<svg data-testid="lang-progress" width="2%">
|
||||||
|
<rect height="8" fill="#f00" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should match the public many-params response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="420" height="110" viewBox="0 0 420 110" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||||
|
fill: #123456;
|
||||||
|
animation: fadeInAnimation 0.8s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
@supports(-moz-appearance: auto) {
|
||||||
|
/* Selector detects Firefox */
|
||||||
|
.header { font-size: 15.5px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInAnimation {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: calc(100%-100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes growWidthAnimation {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #abcdef;
|
||||||
|
}
|
||||||
|
@supports(-moz-appearance: auto) {
|
||||||
|
/* Selector detects Firefox */
|
||||||
|
.stat { font-size:12px; }
|
||||||
|
}
|
||||||
|
.bold { font-weight: 700 }
|
||||||
|
.lang-name {
|
||||||
|
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
|
||||||
|
fill: #abcdef;
|
||||||
|
}
|
||||||
|
.stagger {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInAnimation 0.3s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
#rect-mask rect{
|
||||||
|
animation: slideInAnimation 1s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.lang-progress{
|
||||||
|
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { animation-duration: 0s !important; animation-delay: 0s !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="419" fill="#0f172a" stroke-opacity="0"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 25)">
|
||||||
|
|
||||||
|
<svg data-testid="lang-items" x="25">
|
||||||
|
|
||||||
|
|
||||||
|
<mask id="rect-mask">
|
||||||
|
<rect x="0" y="0" width="370" height="8" fill="white" rx="5"/>
|
||||||
|
</mask>
|
||||||
|
|
||||||
|
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="0" y="0" width="256.98" height="8" fill="#3178c6"/>
|
||||||
|
|
||||||
|
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="256.98" y="0" width="60.57" height="8" fill="#f00"/>
|
||||||
|
|
||||||
|
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="317.55" y="0" width="52.45" height="8" fill="#00f"/>
|
||||||
|
|
||||||
|
|
||||||
|
<g transform="translate(0, 25)">
|
||||||
|
<g transform="translate(0, 0)"><g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms">
|
||||||
|
<circle cx="5" cy="6" r="5" fill="#3178c6"/>
|
||||||
|
<text data-testid="lang-name" x="15" y="10" class="lang-name">
|
||||||
|
TypeScript 19.0 B
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 25)">
|
||||||
|
<g class="stagger" style="animation-delay: 600ms">
|
||||||
|
<circle cx="5" cy="6" r="5" fill="#f00"/>
|
||||||
|
<text data-testid="lang-name" x="15" y="10" class="lang-name">
|
||||||
|
Java 4.5 B
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g></g><g transform="translate(150, 0)"><g transform="translate(0, 0)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms">
|
||||||
|
<circle cx="5" cy="6" r="5" fill="#00f"/>
|
||||||
|
<text data-testid="lang-name" x="15" y="10" class="lang-name">
|
||||||
|
Python 3.9 B
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g></g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"graphqlRequest": "{"query":"\\n query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n # do not fetch forks\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n size\\n node {\\n color\\n name\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should match the public missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray"/>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/top-langs contract > should render error card in same theme as requested card 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray"/>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should match the private missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should match the private non-whitelisted username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">This username is not whitelisted</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should match the public happy-path response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="495" height="195" viewBox="0 0 495 195" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</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 (last 7 days)</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)">
|
||||||
|
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
|
||||||
|
<text class="stat bold" y="12.5" data-testid="TypeScript">TypeScript:</text>
|
||||||
|
<text class="stat" x="350" y="12.5">12 hrs</text>
|
||||||
|
|
||||||
|
<svg width="220" x="110" y="4">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
|
||||||
|
<svg data-testid="lang-progress" width="52%">
|
||||||
|
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 25)">
|
||||||
|
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
|
||||||
|
<text class="stat bold" y="12.5" data-testid="JavaScript">JavaScript:</text>
|
||||||
|
<text class="stat" x="350" y="12.5">6 hrs 30 mins</text>
|
||||||
|
|
||||||
|
<svg width="220" x="110" y="4">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
|
||||||
|
<svg data-testid="lang-progress" width="28%">
|
||||||
|
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 50)">
|
||||||
|
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
|
||||||
|
<text class="stat bold" y="12.5" data-testid="Other">Other:</text>
|
||||||
|
<text class="stat" x="350" y="12.5">3 hrs</text>
|
||||||
|
|
||||||
|
<svg width="220" x="110" y="4">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
|
||||||
|
<svg data-testid="lang-progress" width="13%">
|
||||||
|
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 75)">
|
||||||
|
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
|
||||||
|
<text class="stat bold" y="12.5" data-testid="YAML">YAML:</text>
|
||||||
|
<text class="stat" x="350" y="12.5">1 hr</text>
|
||||||
|
|
||||||
|
<svg width="220" x="110" y="4">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
|
||||||
|
<svg data-testid="lang-progress" width="4%">
|
||||||
|
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g><g transform="translate(0, 100)">
|
||||||
|
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
|
||||||
|
<text class="stat bold" y="12.5" data-testid="JSON">JSON:</text>
|
||||||
|
<text class="stat" x="350" y="12.5">30 mins</text>
|
||||||
|
|
||||||
|
<svg width="220" x="110" y="4">
|
||||||
|
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8" fill="#434d58"/>
|
||||||
|
<svg data-testid="lang-progress" width="3%">
|
||||||
|
<rect height="8" fill="#2f80ed" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should match the public inaccessible-profile response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<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"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</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>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should match the public many-params response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="620" height="140" viewBox="0 0 620 140" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
|
||||||
|
<title id="titleId"/>
|
||||||
|
<desc id="descId"/>
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
|
||||||
|
fill: #123456;
|
||||||
|
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: #abcdef;
|
||||||
|
}
|
||||||
|
@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: #abcdef }
|
||||||
|
#rect-mask rect{
|
||||||
|
animation: slideInAnimation 1s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
.lang-progress{
|
||||||
|
animation: growWidthAnimation 0.6s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes scaleInAnimation {
|
||||||
|
from {
|
||||||
|
transform: translate(-5px, 5px) scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-5px, 5px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeInAnimation {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { animation-duration: 0s !important; animation-delay: 0s !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<rect data-testid="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="619" fill="#0f172a" 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">a custom title</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
<g data-testid="main-card-body" transform="translate(0, 55)">
|
||||||
|
|
||||||
|
<svg x="0" y="0" width="100%">
|
||||||
|
|
||||||
|
<mask id="rect-mask">
|
||||||
|
<rect x="25" y="0" width="565" height="8" fill="white" rx="5"/>
|
||||||
|
</mask>
|
||||||
|
|
||||||
|
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="0" y="0" width="365.09200000000004" height="8" fill="#3178c6"/>
|
||||||
|
|
||||||
|
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="365.09200000000004" y="0" width="196.588" height="8" fill="#f1e05a"/>
|
||||||
|
|
||||||
|
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="561.6800000000001" y="0" width="28.084" height="8" fill="#cb171e"/>
|
||||||
|
|
||||||
|
|
||||||
|
<g transform="translate(25, 25)">
|
||||||
|
<circle cx="5" cy="6" r="5" fill="#3178c6"/>
|
||||||
|
<text data-testid="lang-name" x="15" y="10" class="lang-name">
|
||||||
|
TypeScript - 61.88 %
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(292.5, 25)">
|
||||||
|
<circle cx="5" cy="6" r="5" fill="#f1e05a"/>
|
||||||
|
<text data-testid="lang-name" x="15" y="10" class="lang-name">
|
||||||
|
JavaScript - 33.32 %
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(25, 50)">
|
||||||
|
<circle cx="5" cy="6" r="5" fill="#cb171e"/>
|
||||||
|
<text data-testid="lang-name" x="15" y="10" class="lang-name">
|
||||||
|
YAML - 4.76 %
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should match the public missing-username response snapshot 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray"/>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Test /api/wakatime contract > should render error card in same theme as requested card 1`] = `
|
||||||
|
{
|
||||||
|
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
|
||||||
|
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
|
||||||
|
.gray { fill: #858585 }
|
||||||
|
</style>
|
||||||
|
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
|
||||||
|
<text x="25" y="45" class="text">Something went wrong!</text>
|
||||||
|
<text data-testid="message" x="25" y="55" class="text small">
|
||||||
|
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
|
||||||
|
<tspan x="25" dy="18" class="gray"/>
|
||||||
|
</text>
|
||||||
|
</svg>",
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"Cache-Control",
|
||||||
|
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Content-Type",
|
||||||
|
"image/svg+xml",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,41 +1,196 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
import api from "../../api-renamed/index.js";
|
import { data_stats, normalizeSvg } from "../utils.js";
|
||||||
import { renderError } from "../../src/common/render.js";
|
|
||||||
import { data_stats } from "../test-data/api-data.js";
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply(200, data_stats);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.spyOn(logger, "log").mockImplementation(() => {});
|
||||||
|
vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.reset();
|
mock.reset();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
// modules may cache environment variables, so we need to reset them
|
||||||
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/", () => {
|
afterAll(() => {
|
||||||
it("should render error card if username in blacklist", async () => {
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test /api contract", () => {
|
||||||
|
it("should match the public happy-path response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
query: {
|
headers: {},
|
||||||
username: "renovate-bot",
|
url: "/api?username=anuraghazra",
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const res = {
|
const res = createResponse();
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public many-params response snapshot", async () => {
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply(200, data_stats);
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
username: "anuraghazra",
|
||||||
|
show_icons: "true",
|
||||||
|
card_width: "540",
|
||||||
|
line_height: "32",
|
||||||
|
title_color: "123456",
|
||||||
|
ring_color: "654321",
|
||||||
|
icon_color: "ff00aa",
|
||||||
|
text_color: "abcdef",
|
||||||
|
text_bold: "false",
|
||||||
|
bg_color: "0f172a",
|
||||||
|
exclude_repo: "repo-exclude-me",
|
||||||
|
custom_title: "a custom title",
|
||||||
|
locale: "hi",
|
||||||
|
disable_animations: "true",
|
||||||
|
border_radius: "12",
|
||||||
|
number_format: "long",
|
||||||
|
number_precision: "1",
|
||||||
|
border_color: "fedcba",
|
||||||
|
rank_icon: "github",
|
||||||
|
commits_year: "2024",
|
||||||
|
hide: "issues",
|
||||||
|
role: "OWNER,COLLABORATOR",
|
||||||
|
show: "reviews,prs_merged,prs_merged_percentage,discussions_started,discussions_answered",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: `/api?${params.toString()}`,
|
||||||
};
|
};
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
|
const res = createResponse();
|
||||||
|
|
||||||
await api(req, res);
|
await router(req, res);
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
expect({
|
||||||
message: "This username is blacklisted",
|
graphqlRequest: mock.history.post[0].data,
|
||||||
secondaryMessage: "Please deploy your own instance",
|
headers: res.setHeader.mock.calls,
|
||||||
renderOptions: { show_repo_link: false },
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
}),
|
}).toMatchSnapshot();
|
||||||
);
|
});
|
||||||
|
|
||||||
|
it("should match the public missing-username response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render error card in same theme as requested card", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api?theme=merko",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public blacklisted-username response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api?username=renovate-bot",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private missing-username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,39 +1,143 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
import gist from "../../api-renamed/gist.js";
|
import { happy_path_gist_data, normalizeSvg } from "../utils.js";
|
||||||
import { renderError } from "../../src/common/render.js";
|
|
||||||
import { gist_data } from "../test-data/gist-data.js";
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
mock
|
||||||
|
.onPost("https://api.github.com/graphql")
|
||||||
|
.reply(200, happy_path_gist_data);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.spyOn(logger, "log").mockImplementation(() => {});
|
||||||
|
vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.reset();
|
mock.reset();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
// modules may cache environment variables, so we need to reset them
|
||||||
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/gist", () => {
|
afterAll(() => {
|
||||||
it("should render error if id is not provided", async () => {
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test /api/gist contract", () => {
|
||||||
|
it("should match the public happy-path response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
query: {},
|
headers: {},
|
||||||
|
url: "/api/gist?id=happy-gist-id",
|
||||||
};
|
};
|
||||||
const res = {
|
const res = createResponse();
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public many-params response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
id: "happy-gist-id",
|
||||||
|
title_color: "123456",
|
||||||
|
icon_color: "ff00aa",
|
||||||
|
text_color: "abcdef",
|
||||||
|
bg_color: "0f172a",
|
||||||
|
border_radius: "12",
|
||||||
|
border_color: "fedcba",
|
||||||
|
show_owner: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: `/api/gist?${params.toString()}`,
|
||||||
};
|
};
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
|
const res = createResponse();
|
||||||
|
|
||||||
await gist(req, res);
|
await router(req, res);
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
expect({
|
||||||
message: 'Missing params "id" make sure you pass the parameters in URL',
|
graphqlRequest: mock.history.post[0].data,
|
||||||
secondaryMessage: "/api/gist?id=GIST_ID",
|
headers: res.setHeader.mock.calls,
|
||||||
renderOptions: { show_repo_link: false },
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
}),
|
}).toMatchSnapshot();
|
||||||
);
|
});
|
||||||
|
|
||||||
|
it("should match the public missing-id response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/gist",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private missing-id response snapshot", async () => {
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "allowed-gist-id");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/gist",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,65 +1,235 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
import pin from "../../api-renamed/pin.js";
|
import { data_user, normalizeSvg } from "../utils.js";
|
||||||
import { renderError } from "../../src/common/render.js";
|
|
||||||
import { data_user } from "../test-data/pin-data.js";
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.spyOn(logger, "log").mockImplementation(() => {});
|
||||||
|
vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.reset();
|
mock.reset();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
// modules may cache environment variables, so we need to reset them
|
||||||
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/pin", () => {
|
afterAll(() => {
|
||||||
it("should render error card if username in blacklist", async () => {
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test /api/pin contract", () => {
|
||||||
|
it("should match the public happy-path response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
query: {
|
headers: {},
|
||||||
username: "renovate-bot",
|
url: "/api/pin?username=anuraghazra&repo=convoychat",
|
||||||
repo: "convoychat",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const res = {
|
const res = createResponse();
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
|
||||||
|
|
||||||
await pin(req, res);
|
await router(req, res);
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
expect({
|
||||||
message: "This username is blacklisted",
|
headers: res.setHeader.mock.calls,
|
||||||
secondaryMessage: "Please deploy your own instance",
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
renderOptions: { show_repo_link: false },
|
}).toMatchSnapshot();
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render error card if missing required parameters", async () => {
|
it("should match the public many-params response snapshot", async () => {
|
||||||
const req = {
|
|
||||||
query: {},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+author:anuraghazra+type:pr",
|
||||||
|
)
|
||||||
|
.reply(200, { total_count: 1234 });
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+commenter:anuraghazra+-author:anuraghazra+type:pr",
|
||||||
|
)
|
||||||
|
.reply(200, { total_count: 2345 });
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+reviewed-by:anuraghazra+-author:anuraghazra+type:pr",
|
||||||
|
)
|
||||||
|
.reply(200, { total_count: 3456 });
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+author:anuraghazra+type:issue",
|
||||||
|
)
|
||||||
|
.reply(200, { total_count: 4567 });
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+commenter:anuraghazra+-author:anuraghazra+type:issue",
|
||||||
|
)
|
||||||
|
.reply(200, { total_count: 5678 });
|
||||||
|
|
||||||
await pin(req, res);
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
const params = new URLSearchParams({
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
username: "anuraghazra",
|
||||||
renderError({
|
repo: "convoychat",
|
||||||
message:
|
title_color: "123456",
|
||||||
'Missing params "username", "repo" make sure you pass the parameters in URL',
|
icon_color: "ff00aa",
|
||||||
secondaryMessage: "/api/pin?username=USERNAME&repo=REPO_NAME",
|
text_color: "abcdef",
|
||||||
renderOptions: { show_repo_link: false },
|
bg_color: "0f172a",
|
||||||
}),
|
card_width: "560",
|
||||||
);
|
show_owner: "true",
|
||||||
|
show: "prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented",
|
||||||
|
show_icons: "false",
|
||||||
|
number_format: "long",
|
||||||
|
text_bold: "true",
|
||||||
|
line_height: "30",
|
||||||
|
border_radius: "12",
|
||||||
|
border_color: "fedcba",
|
||||||
|
description_lines_count: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: `/api/pin?${params.toString()}`,
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
graphqlRequest: mock.history.post[0].data,
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public missing-params response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/pin",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render error card in same theme as requested card", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/pin?theme=merko",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public blacklisted-username response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/pin?username=renovate-bot&repo=convoychat",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private non-whitelisted username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/pin?username=martin-mfg",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private missing-username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/pin",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,41 +1,210 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
import topLangs from "../../api-renamed/top-langs.js";
|
import { data_langs, normalizeSvg } from "../utils.js";
|
||||||
import { renderError } from "../../src/common/render.js";
|
|
||||||
import { data_langs } from "../test-data/langs-data.js";
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.spyOn(logger, "log").mockImplementation(() => {});
|
||||||
|
vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.reset();
|
mock.reset();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
// modules may cache environment variables, so we need to reset them
|
||||||
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/top-langs", () => {
|
afterAll(() => {
|
||||||
it("should render error card if username in blacklist", async () => {
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test /api/top-langs contract", () => {
|
||||||
|
it("should match the public happy-path response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
const req = {
|
const req = {
|
||||||
query: {
|
headers: {},
|
||||||
username: "renovate-bot",
|
url: "/api/top-langs?username=anuraghazra",
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public many-params response snapshot", async () => {
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
||||||
|
|
||||||
await topLangs(req, res);
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
const params = new URLSearchParams({
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
username: "anuraghazra",
|
||||||
renderError({
|
hide: "javascript,HTML",
|
||||||
message: "This username is blacklisted",
|
hide_title: "true",
|
||||||
secondaryMessage: "Please deploy your own instance",
|
hide_border: "true",
|
||||||
renderOptions: { show_repo_link: false },
|
card_width: "420",
|
||||||
}),
|
title_color: "123456",
|
||||||
);
|
text_color: "abcdef",
|
||||||
|
bg_color: "0f172a",
|
||||||
|
layout: "compact",
|
||||||
|
langs_count: "3",
|
||||||
|
exclude_repo: "repo-hidden",
|
||||||
|
size_weight: "0.5",
|
||||||
|
count_weight: "1",
|
||||||
|
role: "OWNER,COLLABORATOR",
|
||||||
|
disable_animations: "true",
|
||||||
|
stats_format: "bytes",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: `/api/top-langs?${params.toString()}`,
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
graphqlRequest: mock.history.post[0].data,
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public missing-username response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/top-langs",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render error card in same theme as requested card", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/top-langs?theme=merko",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public blacklisted-username response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/top-langs?username=renovate-bot",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private non-whitelisted username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/top-langs?username=martin-mfg",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private missing-username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/top-langs",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
|
import axios from "axios";
|
||||||
|
import MockAdapter from "axios-mock-adapter";
|
||||||
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
|
import { normalizeSvg, wakaTimeData } from "../utils.js";
|
||||||
|
|
||||||
|
const mock = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const wakaTimeProfileNotPublicData = {
|
||||||
|
data: {
|
||||||
|
viewer: {
|
||||||
|
gist: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResponse = () => ({
|
||||||
|
end: vi.fn(),
|
||||||
|
setHeader: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("CACHE_SECONDS", "");
|
||||||
|
vi.stubEnv("GIST_WHITELIST", "");
|
||||||
|
vi.stubEnv("POSTGRES_URL", "");
|
||||||
|
vi.stubEnv("WHITELIST", "");
|
||||||
|
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://wakatime.com/api/v1/users/anuraghazra/stats?is_including_today=true",
|
||||||
|
)
|
||||||
|
.reply(200, wakaTimeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.spyOn(logger, "log").mockImplementation(() => {});
|
||||||
|
vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.reset();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
// modules may cache environment variables, so we need to reset them
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test /api/wakatime contract", () => {
|
||||||
|
it("should match the public happy-path response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/wakatime?username=anuraghazra",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public many-params response snapshot", async () => {
|
||||||
|
mock.reset(); // to verify api_domain param is working
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://wakatime.local/api/v1/users/anuraghazra/stats?is_including_today=true",
|
||||||
|
)
|
||||||
|
.reply(200, wakaTimeData);
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
username: "anuraghazra",
|
||||||
|
title_color: "123456",
|
||||||
|
text_color: "abcdef",
|
||||||
|
bg_color: "0f172a",
|
||||||
|
card_width: "620",
|
||||||
|
custom_title: "a custom title",
|
||||||
|
layout: "compact",
|
||||||
|
langs_count: "3",
|
||||||
|
hide: "other",
|
||||||
|
api_domain: "wakatime.local",
|
||||||
|
border_radius: "12",
|
||||||
|
border_color: "fedcba",
|
||||||
|
display_format: "percent",
|
||||||
|
disable_animations: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: `/api/wakatime?${params.toString()}`,
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public missing-username response snapshot", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/wakatime",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render error card in same theme as requested card", async () => {
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/wakatime?theme=merko",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the public inaccessible-profile response snapshot", async () => {
|
||||||
|
mock.reset();
|
||||||
|
mock
|
||||||
|
.onGet(
|
||||||
|
"https://wakatime.com/api/v1/users/anuraghazra/stats?is_including_today=true",
|
||||||
|
)
|
||||||
|
.reply(200, wakaTimeProfileNotPublicData);
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/wakatime?username=anuraghazra",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private non-whitelisted username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/wakatime?username=martin-mfg",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match the private missing-username response snapshot", async () => {
|
||||||
|
vi.stubEnv("WHITELIST", "anuraghazra");
|
||||||
|
|
||||||
|
const { default: router } = await import("../../router.js");
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {},
|
||||||
|
url: "/api/wakatime",
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
|
await router(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect({
|
||||||
|
headers: res.setHeader.mock.calls,
|
||||||
|
content: normalizeSvg(res.end.mock.calls[0][0]),
|
||||||
|
}).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { queryByTestId } from "@testing-library/dom";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { renderError } from "../src/common/render.js";
|
|
||||||
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
|
||||||
|
|
||||||
describe("Test render.js", () => {
|
|
||||||
it("should test renderError", () => {
|
|
||||||
document.body.innerHTML = renderError({ message: "Something went wrong" });
|
|
||||||
expect(
|
|
||||||
queryByTestId(document.body, "message")?.children[0],
|
|
||||||
).toHaveTextContent(/Something went wrong/gim);
|
|
||||||
expect(
|
|
||||||
queryByTestId(document.body, "message")?.children[1],
|
|
||||||
).toBeEmptyDOMElement();
|
|
||||||
|
|
||||||
// Secondary message
|
|
||||||
document.body.innerHTML = renderError({
|
|
||||||
message: "Something went wrong",
|
|
||||||
secondaryMessage: "Secondary Message",
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
queryByTestId(document.body, "message")?.children[1],
|
|
||||||
).toHaveTextContent(/Secondary Message/gim);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
|
||||||
|
|
||||||
import { logger } from "../src/common/log.js";
|
|
||||||
import { retryer } from "../src/common/retryer.js";
|
|
||||||
|
|
||||||
const fetcher = vi.fn((variables, token) => {
|
|
||||||
logger.log(variables, token);
|
|
||||||
return new Promise((res) => res({ data: "ok" }));
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetcherFail = vi.fn(() => {
|
|
||||||
return new Promise((res) =>
|
|
||||||
res({ data: { errors: [{ type: "RATE_LIMITED" }] } }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetcherFailOnSecondTry = vi.fn((_vars, _token, retries) => {
|
|
||||||
return new Promise((res) => {
|
|
||||||
// faking rate limit
|
|
||||||
// @ts-ignore
|
|
||||||
if (retries < 1) {
|
|
||||||
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
|
|
||||||
}
|
|
||||||
return res({ data: "ok" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
|
|
||||||
(_vars, _token, retries) => {
|
|
||||||
return new Promise((res) => {
|
|
||||||
// faking rate limit
|
|
||||||
// @ts-ignore
|
|
||||||
if (retries < 1) {
|
|
||||||
return res({
|
|
||||||
data: {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
type: "ASDF",
|
|
||||||
message: "API rate limit already exceeded for user ID 11111111",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res({ data: "ok" });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("Test Retryer", () => {
|
|
||||||
it("retryer should return value and have zero retries on first try", async () => {
|
|
||||||
let res = await retryer(fetcher, {});
|
|
||||||
|
|
||||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
||||||
expect(res).toStrictEqual({ data: "ok" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retryer should return value and have 2 retries", async () => {
|
|
||||||
let res = await retryer(fetcherFailOnSecondTry, {});
|
|
||||||
|
|
||||||
expect(fetcherFailOnSecondTry).toHaveBeenCalledTimes(2);
|
|
||||||
expect(res).toStrictEqual({ data: "ok" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retryer should return value and have 2 retries with message based rate limit error", async () => {
|
|
||||||
let res = await retryer(fetcherFailWithMessageBasedRateLimitErr, {});
|
|
||||||
|
|
||||||
expect(fetcherFailWithMessageBasedRateLimitErr).toHaveBeenCalledTimes(2);
|
|
||||||
expect(res).toStrictEqual({ data: "ok" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retryer should throw specific error if maximum retries reached", async () => {
|
|
||||||
try {
|
|
||||||
await retryer(fetcherFail, {});
|
|
||||||
} catch (err) {
|
|
||||||
expect(fetcherFail).toHaveBeenCalledTimes(2);
|
|
||||||
// @ts-ignore
|
|
||||||
expect(err.message).toBe("Downtime due to GitHub API rate limiting");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,9 +2,18 @@
|
|||||||
* @file Tests for the status/up cloud function.
|
* @file Tests for the status/up cloud function.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { logger } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import MockAdapter from "axios-mock-adapter";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
|
||||||
import up, { RATE_LIMIT_SECONDS } from "../api-renamed/status/up.js";
|
import up, { RATE_LIMIT_SECONDS } from "../api-renamed/status/up.js";
|
||||||
|
|
||||||
@@ -55,10 +64,19 @@ const shields_down = {
|
|||||||
color: "red",
|
color: "red",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.spyOn(logger, "log").mockImplementation(() => {});
|
||||||
|
vi.spyOn(logger, "error").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.reset();
|
mock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe("Test /api/status/up", () => {
|
describe("Test /api/status/up", () => {
|
||||||
it("should return `true` if request was successful", async () => {
|
it("should return `true` if request was successful", async () => {
|
||||||
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
mock.onPost("https://api.github.com/graphql").replyOnce(200, successData);
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
// @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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// @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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// @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" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// @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,192 +1,84 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import axios from "axios";
|
import { topLangs } from "@stats-organization/github-readme-stats-core";
|
||||||
import MockAdapter from "axios-mock-adapter";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import topLangs from "../api-renamed/top-langs.js";
|
import router from "../router.js";
|
||||||
import { renderTopLanguages } from "../src/cards/top-languages.js";
|
|
||||||
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
||||||
import { renderError } from "../src/common/render.js";
|
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
|
||||||
|
|
||||||
import { data_langs } from "./test-data/langs-data.js";
|
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
|
||||||
|
const { mockCore } = await import("./utils.js");
|
||||||
import "@testing-library/jest-dom/vitest";
|
return mockCore();
|
||||||
|
|
||||||
const error = {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
type: "NOT_FOUND",
|
|
||||||
path: ["user"],
|
|
||||||
locations: [],
|
|
||||||
message: "Could not fetch user",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const langs = {
|
|
||||||
HTML: {
|
|
||||||
color: "#0f0",
|
|
||||||
name: "HTML",
|
|
||||||
size: 250,
|
|
||||||
},
|
|
||||||
javascript: {
|
|
||||||
color: "#0ff",
|
|
||||||
name: "javascript",
|
|
||||||
size: 200,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/top-langs", () => {
|
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
|
||||||
it("should test the request", async () => {
|
...(await importOriginal()),
|
||||||
const req = {
|
storeRequest: vi.fn(),
|
||||||
query: {
|
getUserAccessByName: vi.fn(),
|
||||||
username: "anuraghazra",
|
}));
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
|
||||||
|
|
||||||
await topLangs(req, res);
|
const topLangsMock = vi.mocked(topLangs);
|
||||||
|
const storeRequestMock = vi.mocked(storeRequest);
|
||||||
|
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
const createRequest = (search = "") => ({
|
||||||
expect(res.send).toHaveBeenCalledWith(renderTopLanguages(langs));
|
headers: {},
|
||||||
});
|
url: `/api/top-langs?${search}`,
|
||||||
|
});
|
||||||
|
|
||||||
it("should work with the query options", async () => {
|
const createResponse = () => ({
|
||||||
const req = {
|
end: vi.fn(),
|
||||||
query: {
|
setHeader: vi.fn(),
|
||||||
username: "anuraghazra",
|
});
|
||||||
hide_title: true,
|
|
||||||
card_width: 100,
|
|
||||||
title_color: "fff",
|
|
||||||
icon_color: "fff",
|
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
|
|
||||||
|
|
||||||
await topLangs(req, res);
|
const defaultCacheHeader =
|
||||||
|
`max-age=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` +
|
||||||
|
`s-maxage=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` +
|
||||||
|
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
beforeEach(() => {
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
topLangsMock.mockReset();
|
||||||
renderTopLanguages(langs, {
|
storeRequestMock.mockReset().mockResolvedValue(undefined);
|
||||||
hide_title: true,
|
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
|
||||||
card_width: 100,
|
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
|
||||||
title_color: "fff",
|
delete process.env.CACHE_SECONDS;
|
||||||
icon_color: "fff",
|
});
|
||||||
text_color: "fff",
|
|
||||||
bg_color: "fff",
|
describe("Test /api/top-langs backend routing", () => {
|
||||||
}),
|
it("happy path should pass query params and user PAT, respond with top languages content and persist request", async () => {
|
||||||
|
getUserAccessByNameMock.mockResolvedValue({ token: "user-pat" });
|
||||||
|
topLangsMock.mockResolvedValue({
|
||||||
|
status: "success",
|
||||||
|
content: "mock-top-langs-svg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = createRequest(
|
||||||
|
"username=anuraghazra&layout=compact&langs_count=5",
|
||||||
);
|
);
|
||||||
});
|
const res = createResponse();
|
||||||
|
|
||||||
it("should render error card on user data fetch error", async () => {
|
await router(req, res);
|
||||||
const req = {
|
|
||||||
query: {
|
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
|
||||||
|
expect(topLangsMock).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
username: "anuraghazra",
|
username: "anuraghazra",
|
||||||
|
layout: "compact",
|
||||||
|
langs_count: "5",
|
||||||
},
|
},
|
||||||
};
|
"user-pat",
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.fn(),
|
|
||||||
};
|
|
||||||
mock.onPost("https://api.github.com/graphql").reply(200, error);
|
|
||||||
|
|
||||||
await topLangs(req, res);
|
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
renderError({
|
|
||||||
message: error.errors[0].message,
|
|
||||||
secondaryMessage:
|
|
||||||
"Make sure the provided username is not an organization",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error card on incorrect layout input", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
layout: ["pie"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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: "Something went wrong",
|
|
||||||
secondaryMessage: "Incorrect layout input",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error card if wrong locale provided", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
locale: "asdf",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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: "Something went wrong",
|
|
||||||
secondaryMessage: "Locale not found",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache", async () => {
|
|
||||||
const req = {
|
|
||||||
query: {
|
|
||||||
username: "anuraghazra",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
send: vi.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.setHeader).toHaveBeenCalledWith(
|
|
||||||
"Cache-Control",
|
|
||||||
`max-age=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` +
|
|
||||||
`s-maxage=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` +
|
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
);
|
);
|
||||||
|
expect(req.query).toEqual({
|
||||||
|
username: "anuraghazra",
|
||||||
|
layout: "compact",
|
||||||
|
langs_count: "5",
|
||||||
|
});
|
||||||
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
|
["Cache-Control", defaultCacheHeader],
|
||||||
|
["Content-Type", "image/svg+xml"],
|
||||||
|
]);
|
||||||
|
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-top-langs-svg");
|
||||||
|
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+293
-35
@@ -1,41 +1,299 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
/**
|
import { vi } from "vitest";
|
||||||
* Creates an asymmetric matcher for approximate numeric equality.
|
|
||||||
*
|
export const data_stats = {
|
||||||
* This helper is intended for use in test frameworks (e.g., Jest) where
|
data: {
|
||||||
* values need to be compared within a configurable decimal precision
|
user: {
|
||||||
* instead of strict equality.
|
name: "Anurag Hazra",
|
||||||
*
|
login: "anuraghazra",
|
||||||
* The comparison succeeds when:
|
repositoriesContributedTo: { totalCount: 51 },
|
||||||
*
|
commits: {
|
||||||
* |actual - expected| < 10^(-precision)
|
totalCommitContributions: 200,
|
||||||
*
|
},
|
||||||
* For example, with `precision = 3`, values must be within `0.001`.
|
reviews: {
|
||||||
*
|
totalPullRequestReviewContributions: 1234,
|
||||||
* @param {number} expected The expected numeric value to compare against.
|
},
|
||||||
*
|
pullRequests: { totalCount: 4000 },
|
||||||
* @param {number} [precision=10]
|
mergedPullRequests: { totalCount: 3200 },
|
||||||
* The number of decimal places of tolerance. Higher values mean stricter
|
openIssues: { totalCount: 300 },
|
||||||
* comparison. Internally converted to epsilon = 10^-precision.
|
closedIssues: { totalCount: 40 },
|
||||||
*
|
followers: { totalCount: 150 },
|
||||||
* @returns {{
|
repositoryDiscussions: { totalCount: 222 },
|
||||||
* asymmetricMatch(actual: unknown): boolean,
|
repositoryDiscussionComments: {
|
||||||
* toAsymmetricMatcher(): string
|
totalCount: 111,
|
||||||
* }} An object implementing Jest-style asymmetric matcher methods.
|
},
|
||||||
*
|
repositories: {
|
||||||
*/
|
totalCount: 3,
|
||||||
export function approxNumber(expected, precision = 10) {
|
nodes: [
|
||||||
return {
|
{ name: "repo-keep-1", stargazers: { totalCount: 1500 } },
|
||||||
asymmetricMatch(actual) {
|
{ name: "repo-exclude-me", stargazers: { totalCount: 9999 } },
|
||||||
if (typeof actual !== "number" || typeof expected !== "number") {
|
{ name: "repo-keep-2", stargazers: { totalCount: 2600 } },
|
||||||
return false;
|
],
|
||||||
}
|
pageInfo: {
|
||||||
const epsilon = Math.pow(10, -precision);
|
hasNextPage: false,
|
||||||
return Math.abs(actual - expected) < epsilon;
|
endCursor: "cursor",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
toAsymmetricMatcher() {
|
},
|
||||||
return `≈ ${expected} (precision ${precision})`;
|
};
|
||||||
|
|
||||||
|
export const happy_path_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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const data_user = {
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
repository: {
|
||||||
|
username: "anuraghazra",
|
||||||
|
name: "convoychat",
|
||||||
|
nameWithOwner: "anuraghazra/convoychat",
|
||||||
|
stargazers: {
|
||||||
|
totalCount: 38000,
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Help us take over the world with a deeply customizable React, TypeScript and GraphQL chat app that has enough text to wrap across multiple lines in the repository card.",
|
||||||
|
primaryLanguage: {
|
||||||
|
color: "#2b7489",
|
||||||
|
id: "MDg6TGFuZ3VhZ2UyODc=",
|
||||||
|
name: "TypeScript",
|
||||||
|
},
|
||||||
|
forkCount: 100,
|
||||||
|
isTemplate: false,
|
||||||
|
isArchived: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
organization: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const data_langs = {
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
repositories: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
name: "repo-html",
|
||||||
|
languages: {
|
||||||
|
edges: [{ size: 180, node: { color: "#0f0", name: "HTML" } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repo-javascript",
|
||||||
|
languages: {
|
||||||
|
edges: [
|
||||||
|
{ size: 120, node: { color: "#0ff", name: "JavaScript" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repo-ts-1",
|
||||||
|
languages: {
|
||||||
|
edges: [
|
||||||
|
{ size: 45, node: { color: "#3178c6", name: "TypeScript" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repo-ts-2",
|
||||||
|
languages: {
|
||||||
|
edges: [
|
||||||
|
{ size: 45, node: { color: "#3178c6", name: "TypeScript" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repo-other-langs",
|
||||||
|
languages: {
|
||||||
|
edges: [
|
||||||
|
{ size: 20, node: { color: "#f00", name: "Java" } },
|
||||||
|
{ size: 10, node: { color: "#0f0", name: "Cobol" } },
|
||||||
|
{ size: 15, node: { color: "#00f", name: "Python" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repo-hidden",
|
||||||
|
languages: {
|
||||||
|
edges: [{ size: 1000, node: { color: "#dea584", name: "Rust" } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wakaTimeData = {
|
||||||
|
data: {
|
||||||
|
categories: [
|
||||||
|
{
|
||||||
|
digital: "22:40",
|
||||||
|
hours: 22,
|
||||||
|
minutes: 40,
|
||||||
|
name: "Coding",
|
||||||
|
percent: 100,
|
||||||
|
text: "22 hrs 40 mins",
|
||||||
|
total_seconds: 81643.570077,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
daily_average: 16095,
|
||||||
|
daily_average_including_other_language: 16329,
|
||||||
|
days_including_holidays: 7,
|
||||||
|
days_minus_holidays: 5,
|
||||||
|
editors: [
|
||||||
|
{
|
||||||
|
digital: "22:40",
|
||||||
|
hours: 22,
|
||||||
|
minutes: 40,
|
||||||
|
name: "VS Code",
|
||||||
|
percent: 100,
|
||||||
|
text: "22 hrs 40 mins",
|
||||||
|
total_seconds: 81643.570077,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
holidays: 2,
|
||||||
|
human_readable_daily_average: "4 hrs 28 mins",
|
||||||
|
human_readable_daily_average_including_other_language: "4 hrs 32 mins",
|
||||||
|
human_readable_total: "22 hrs 21 mins",
|
||||||
|
human_readable_total_including_other_language: "22 hrs 40 mins",
|
||||||
|
id: "random hash",
|
||||||
|
is_already_updating: false,
|
||||||
|
is_coding_activity_visible: true,
|
||||||
|
is_including_today: false,
|
||||||
|
is_other_usage_visible: true,
|
||||||
|
is_stuck: false,
|
||||||
|
is_up_to_date: true,
|
||||||
|
languages: [
|
||||||
|
{
|
||||||
|
digital: "12:00",
|
||||||
|
hours: 12,
|
||||||
|
minutes: 0,
|
||||||
|
name: "TypeScript",
|
||||||
|
percent: 52,
|
||||||
|
text: "12 hrs",
|
||||||
|
total_seconds: 43200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
digital: "6:30",
|
||||||
|
hours: 6,
|
||||||
|
minutes: 30,
|
||||||
|
name: "JavaScript",
|
||||||
|
percent: 28,
|
||||||
|
text: "6 hrs 30 mins",
|
||||||
|
total_seconds: 23400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
digital: "3:00",
|
||||||
|
hours: 3,
|
||||||
|
minutes: 0,
|
||||||
|
name: "Other",
|
||||||
|
percent: 13,
|
||||||
|
text: "3 hrs",
|
||||||
|
total_seconds: 10800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
digital: "1:00",
|
||||||
|
hours: 1,
|
||||||
|
minutes: 0,
|
||||||
|
name: "YAML",
|
||||||
|
percent: 4,
|
||||||
|
text: "1 hr",
|
||||||
|
total_seconds: 3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
digital: "0:30",
|
||||||
|
hours: 0,
|
||||||
|
minutes: 30,
|
||||||
|
name: "JSON",
|
||||||
|
percent: 3,
|
||||||
|
text: "30 mins",
|
||||||
|
total_seconds: 1800,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
operating_systems: [
|
||||||
|
{
|
||||||
|
digital: "22:40",
|
||||||
|
hours: 22,
|
||||||
|
minutes: 40,
|
||||||
|
name: "Mac",
|
||||||
|
percent: 100,
|
||||||
|
text: "22 hrs 40 mins",
|
||||||
|
total_seconds: 81643.570077,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
percent_calculated: 100,
|
||||||
|
range: "last_7_days",
|
||||||
|
status: "ok",
|
||||||
|
timeout: 15,
|
||||||
|
total_seconds: 80473.135716,
|
||||||
|
total_seconds_including_other_language: 81643.570077,
|
||||||
|
user_id: "random hash",
|
||||||
|
username: "anuraghazra",
|
||||||
|
writes_only: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @typedef {import('@stats-organization/github-readme-stats-core')} CoreModule */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock module for @stats-organization/github-readme-stats-core.
|
||||||
|
* @returns {CoreModule} Mocked core module.
|
||||||
|
*/
|
||||||
|
export function mockCore() {
|
||||||
|
return {
|
||||||
|
// @ts-expect-error no need to mock themes at the moment
|
||||||
|
themes: {},
|
||||||
|
request: vi.fn(),
|
||||||
|
fetchWakatimeStats: vi.fn(),
|
||||||
|
retryer: vi.fn(),
|
||||||
|
dateDiff: vi.fn(),
|
||||||
|
api: vi.fn(),
|
||||||
|
gist: vi.fn(),
|
||||||
|
pin: vi.fn(),
|
||||||
|
topLangs: vi.fn(),
|
||||||
|
wakatime: vi.fn(),
|
||||||
|
getConfig: vi.fn().mockReturnValue({}),
|
||||||
|
renderError: ({ message }) => `render-error:${message}`,
|
||||||
|
clampValue: (value, min, max) => Math.min(Math.max(value, min), max),
|
||||||
|
logger: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes SVG code for stable test comparisons.
|
||||||
|
* @param {string} svg SVG code to normalize.
|
||||||
|
* @returns {string} Normalized SVG code.
|
||||||
|
*/
|
||||||
|
export function normalizeSvg(svg) {
|
||||||
|
const document = new DOMParser().parseFromString(svg, "image/svg+xml");
|
||||||
|
return new XMLSerializer().serializeToString(document);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,195 +1,78 @@
|
|||||||
import axios from "axios";
|
// @ts-check
|
||||||
import MockAdapter from "axios-mock-adapter";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import wakatime from "../api-renamed/wakatime.js";
|
import { wakatime } from "@stats-organization/github-readme-stats-core";
|
||||||
import { renderWakatimeCard } from "../src/cards/wakatime.js";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import router from "../router.js";
|
||||||
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
|
||||||
import { renderError } from "../src/index.js";
|
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
|
||||||
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
|
||||||
|
const { mockCore } = await import("./utils.js");
|
||||||
const wakaTimeData = {
|
return mockCore();
|
||||||
data: {
|
|
||||||
categories: [
|
|
||||||
{
|
|
||||||
digital: "22:40",
|
|
||||||
hours: 22,
|
|
||||||
minutes: 40,
|
|
||||||
name: "Coding",
|
|
||||||
percent: 100,
|
|
||||||
text: "22 hrs 40 mins",
|
|
||||||
total_seconds: 81643.570077,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
daily_average: 16095,
|
|
||||||
daily_average_including_other_language: 16329,
|
|
||||||
days_including_holidays: 7,
|
|
||||||
days_minus_holidays: 5,
|
|
||||||
editors: [
|
|
||||||
{
|
|
||||||
digital: "22:40",
|
|
||||||
hours: 22,
|
|
||||||
minutes: 40,
|
|
||||||
name: "VS Code",
|
|
||||||
percent: 100,
|
|
||||||
text: "22 hrs 40 mins",
|
|
||||||
total_seconds: 81643.570077,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
holidays: 2,
|
|
||||||
human_readable_daily_average: "4 hrs 28 mins",
|
|
||||||
human_readable_daily_average_including_other_language: "4 hrs 32 mins",
|
|
||||||
human_readable_total: "22 hrs 21 mins",
|
|
||||||
human_readable_total_including_other_language: "22 hrs 40 mins",
|
|
||||||
id: "random hash",
|
|
||||||
is_already_updating: false,
|
|
||||||
is_coding_activity_visible: true,
|
|
||||||
is_including_today: false,
|
|
||||||
is_other_usage_visible: true,
|
|
||||||
is_stuck: false,
|
|
||||||
is_up_to_date: true,
|
|
||||||
languages: [
|
|
||||||
{
|
|
||||||
digital: "0:19",
|
|
||||||
hours: 0,
|
|
||||||
minutes: 19,
|
|
||||||
name: "Other",
|
|
||||||
percent: 1.43,
|
|
||||||
text: "19 mins",
|
|
||||||
total_seconds: 1170.434361,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
digital: "0:01",
|
|
||||||
hours: 0,
|
|
||||||
minutes: 1,
|
|
||||||
name: "TypeScript",
|
|
||||||
percent: 0.1,
|
|
||||||
text: "1 min",
|
|
||||||
total_seconds: 83.293809,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
digital: "0:00",
|
|
||||||
hours: 0,
|
|
||||||
minutes: 0,
|
|
||||||
name: "YAML",
|
|
||||||
percent: 0.07,
|
|
||||||
text: "0 secs",
|
|
||||||
total_seconds: 54.975151,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
operating_systems: [
|
|
||||||
{
|
|
||||||
digital: "22:40",
|
|
||||||
hours: 22,
|
|
||||||
minutes: 40,
|
|
||||||
name: "Mac",
|
|
||||||
percent: 100,
|
|
||||||
text: "22 hrs 40 mins",
|
|
||||||
total_seconds: 81643.570077,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
percent_calculated: 100,
|
|
||||||
range: "last_7_days",
|
|
||||||
status: "ok",
|
|
||||||
timeout: 15,
|
|
||||||
total_seconds: 80473.135716,
|
|
||||||
total_seconds_including_other_language: 81643.570077,
|
|
||||||
user_id: "random hash",
|
|
||||||
username: "anuraghazra",
|
|
||||||
writes_only: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const wakaTimeNotFoundData = {
|
|
||||||
data: {
|
|
||||||
viewer: {
|
|
||||||
gist: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mock = new MockAdapter(axios);
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
mock.reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test /api/wakatime", () => {
|
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
|
||||||
it("should test the request", async () => {
|
...(await importOriginal()),
|
||||||
const username = "anuraghazra";
|
storeRequest: vi.fn(),
|
||||||
const req = { query: { username } };
|
getUserAccessByName: vi.fn(),
|
||||||
const res = { setHeader: vi.fn(), send: vi.fn() };
|
}));
|
||||||
mock
|
|
||||||
.onGet(
|
|
||||||
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
|
|
||||||
)
|
|
||||||
.reply(200, wakaTimeData);
|
|
||||||
|
|
||||||
await wakatime(req, res);
|
const wakatimeMock = vi.mocked(wakatime);
|
||||||
|
const storeRequestMock = vi.mocked(storeRequest);
|
||||||
|
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
const createRequest = (search = "") => ({
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
headers: {},
|
||||||
renderWakatimeCard(wakaTimeData.data, {}),
|
url: `/api/wakatime?${search}`,
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error if wrong locale is provided", async () => {
|
const createResponse = () => ({
|
||||||
const username = "anuraghazra";
|
end: vi.fn(),
|
||||||
const req = { query: { username, locale: "asdf" } };
|
setHeader: vi.fn(),
|
||||||
const res = { setHeader: vi.fn(), send: vi.fn() };
|
});
|
||||||
mock
|
|
||||||
.onGet(
|
|
||||||
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
|
|
||||||
)
|
|
||||||
.reply(200, wakaTimeData);
|
|
||||||
|
|
||||||
await wakatime(req, res);
|
const defaultCacheHeader =
|
||||||
|
`max-age=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` +
|
||||||
|
`s-maxage=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` +
|
||||||
|
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
beforeEach(() => {
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
wakatimeMock.mockReset();
|
||||||
renderError({
|
storeRequestMock.mockReset().mockResolvedValue(undefined);
|
||||||
message: "Something went wrong",
|
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
|
||||||
secondaryMessage: "Language not found",
|
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
|
||||||
}),
|
delete process.env.CACHE_SECONDS;
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should render error if user data is not accessible", async () => {
|
describe("Test /api/wakatime backend routing", () => {
|
||||||
const username = "anuraghazra";
|
it("happy path should pass query params, respond with wakatime content and persist request", async () => {
|
||||||
const req = { query: { username } };
|
wakatimeMock.mockResolvedValue({
|
||||||
const res = { setHeader: vi.fn(), send: vi.fn() };
|
status: "success",
|
||||||
mock
|
content: "mock-wakatime-svg",
|
||||||
.onGet(
|
});
|
||||||
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
|
|
||||||
)
|
|
||||||
.reply(200, wakaTimeNotFoundData);
|
|
||||||
|
|
||||||
await wakatime(req, res);
|
const req = createRequest("username=anuraghazra&theme=dark&layout=compact");
|
||||||
|
const res = createResponse();
|
||||||
|
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
await router(req, res);
|
||||||
expect(res.send).toHaveBeenCalledTimes(1);
|
|
||||||
expect(res.send.mock.calls[0][0]).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper cache", async () => {
|
expect(wakatimeMock).toHaveBeenCalledWith({
|
||||||
const username = "anuraghazra";
|
username: "anuraghazra",
|
||||||
const req = { query: { username } };
|
theme: "dark",
|
||||||
const res = { setHeader: vi.fn(), send: vi.fn() };
|
layout: "compact",
|
||||||
mock
|
});
|
||||||
.onGet(
|
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
|
||||||
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
|
expect(req.query).toEqual({
|
||||||
)
|
username: "anuraghazra",
|
||||||
.reply(200, wakaTimeData);
|
theme: "dark",
|
||||||
|
layout: "compact",
|
||||||
await wakatime(req, res);
|
});
|
||||||
|
expect(res.setHeader.mock.calls).toEqual([
|
||||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
|
["Cache-Control", defaultCacheHeader],
|
||||||
expect(res.setHeader).toHaveBeenCalledWith(
|
["Content-Type", "image/svg+xml"],
|
||||||
"Cache-Control",
|
]);
|
||||||
`max-age=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` +
|
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-wakatime-svg");
|
||||||
`s-maxage=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` +
|
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
|
||||||
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": ["./tsconfig.json"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"customConditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
"buildCommand": "cd ../../ && git clean ./apps -fx && pnpm --filter ./apps/backend/ --legacy deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/backend/node_modules/ && ./vercel-preparation.sh",
|
"buildCommand": "cd ../../ && git clean ./apps -fx && pnpm run build:packages && pnpm --filter ./apps/backend/ --legacy --prod deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/backend/node_modules/ && ./vercel-preparation.sh",
|
||||||
"redirects": [
|
"redirects": [
|
||||||
{
|
{
|
||||||
"source": "/",
|
"source": "/",
|
||||||
"destination": "https://github.com/stats-organization/github-stats-extended"
|
"destination": "https://github.com/stats-organization/github-stats-extended"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"rewrites": [
|
|
||||||
{
|
|
||||||
"source": "/frontend/:match*",
|
|
||||||
"destination": "https://gse-frontend.vercel.app/:match*"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
conditions: ["@stats/source"],
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
dir: "./tests/bench",
|
dir: "./tests/bench",
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
conditions: ["@stats/source"],
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: "node",
|
environment: "node",
|
||||||
dir: "tests/e2e",
|
dir: "tests/e2e",
|
||||||
|
|||||||
@@ -1,32 +1,15 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineProject } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineProject({
|
||||||
|
resolve: {
|
||||||
|
conditions: ["@stats/source"],
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
coverage: {
|
environment: "jsdom",
|
||||||
enabled: true,
|
include: [
|
||||||
},
|
"./tests/*.test.{ts,js}",
|
||||||
projects: [
|
"./tests/public-instance/*.test.{ts,js}",
|
||||||
{
|
|
||||||
test: {
|
|
||||||
name: "backend/public-instance",
|
|
||||||
environment: "jsdom",
|
|
||||||
dir: "tests",
|
|
||||||
include: ["./*.test.{ts,js}", "./public-instance/*.test.{ts,js}"],
|
|
||||||
setupFiles: ["./tests/_setup.js"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: {
|
|
||||||
name: "backend/private-instance",
|
|
||||||
environment: "jsdom",
|
|
||||||
dir: "tests",
|
|
||||||
include: ["./*.test.{ts,js}", "./private-instance/*.test.{ts,js}"],
|
|
||||||
setupFiles: [
|
|
||||||
"./tests/_setup.js",
|
|
||||||
"./tests/_setup.private-instance.js",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
setupFiles: ["./tests/_setup.js"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+33
-36
@@ -1,48 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "@stats-organization/github-readme-stats-frontend",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"license": "MIT",
|
||||||
"@reduxjs/toolkit": "2.11.2",
|
"private": true,
|
||||||
"@tailwindcss/vite": "4.2.1",
|
|
||||||
"axios": "^1",
|
|
||||||
"axios-cache-interceptor": "^1",
|
|
||||||
"daisyui": "5.5.19",
|
|
||||||
"emoji-name-map": "^2.0.3",
|
|
||||||
"github-username-regex": "^1.0.0",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-dom": "18.3.1",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-loading-skeleton": "^3.3.1",
|
|
||||||
"react-redux": "9.2.0",
|
|
||||||
"react-spinners": "^0.17.0",
|
|
||||||
"react-toastify": "11.0.5",
|
|
||||||
"redux": "5.0.1",
|
|
||||||
"save-svg-as-png": "^1.4.17",
|
|
||||||
"uuid": "13.0.0",
|
|
||||||
"word-wrap": "^1.2.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "18.3.27",
|
|
||||||
"@types/react-dom": "18.3.7",
|
|
||||||
"@vitejs/plugin-react-swc": "4.2.3",
|
|
||||||
"clsx": "2.1.1",
|
|
||||||
"tailwindcss": "4.2.1",
|
|
||||||
"vite": "catalog:default",
|
|
||||||
"vite-plugin-node-polyfills": "0.25.0",
|
|
||||||
"vite-plugin-string-replace": "1.1.5",
|
|
||||||
"vitest": "catalog:default"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"typecheck": "tsc --noEmit"
|
"lint": "eslint",
|
||||||
|
"typecheck": "tsc -p tsconfig.typecheck.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
|
"axios": "^1",
|
||||||
|
"axios-cache-interceptor": "^1",
|
||||||
|
"daisyui": "^5.5.19",
|
||||||
|
"@stats-organization/github-readme-stats-backend": "workspace:^",
|
||||||
|
"@stats-organization/github-readme-stats-core": "workspace:^",
|
||||||
|
"react": "^19.2.5",
|
||||||
|
"react-dom": "^19.2.5",
|
||||||
|
"react-icons": "^5.6.0",
|
||||||
|
"react-loading-skeleton": "^3.3.1",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"react-spinners": "^0.17.0",
|
||||||
|
"react-toastify": "^11.1.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"save-svg-as-png": "^1.4.17",
|
||||||
|
"uuid": "^14.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "6.0.1",
|
||||||
|
"clsx": "2.1.1",
|
||||||
|
"tailwindcss": "4.2.2",
|
||||||
|
"vite": "catalog:default",
|
||||||
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
"homepage": "/frontend",
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// @ts-expect-error type info should be added later
|
||||||
|
import { router } from "@stats-organization/github-readme-stats-backend";
|
||||||
|
import { loadConfigFromEnv } from "@stats-organization/github-readme-stats-core";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
@@ -5,8 +8,6 @@ import Skeleton from "react-loading-skeleton";
|
|||||||
import "react-loading-skeleton/dist/skeleton.css";
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
|
||||||
import { setShouldMock } from "../../axios-override.js";
|
import { setShouldMock } from "../../axios-override.js";
|
||||||
// @ts-expect-error will be solved by npm package
|
|
||||||
import { default as router } from "../../backend/.vercel/output/functions/api.func/router.js";
|
|
||||||
import { createMockRequest, createMockResponse } from "../../mock-http.js";
|
import { createMockRequest, createMockResponse } from "../../mock-http.js";
|
||||||
import {
|
import {
|
||||||
useIsAuthenticated,
|
useIsAuthenticated,
|
||||||
@@ -45,7 +46,12 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
|
|||||||
let isCurrent = true;
|
let isCurrent = true;
|
||||||
|
|
||||||
const loadSvg = async () => {
|
const loadSvg = async () => {
|
||||||
window.process.env.PAT_1 = userToken as string;
|
const config: Record<string, string | undefined> = {
|
||||||
|
FETCH_MULTI_PAGE_STARS: "10",
|
||||||
|
PAT_1: userToken as string, // even if it's null, core's retryer.js sees there is 1 PAT and sets `RETRIES` accordingly
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
loadConfigFromEnv(config);
|
||||||
|
|
||||||
setLoaded(false);
|
setLoaded(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
const PROD = true as boolean;
|
|
||||||
|
|
||||||
export const USE_LOGGER = true as boolean;
|
export const USE_LOGGER = true as boolean;
|
||||||
|
|
||||||
export const CLIENT_ID = "Ov23liZSweT9LJrck9i8";
|
export const CLIENT_ID = "Ov23liZSweT9LJrck9i8";
|
||||||
|
|
||||||
export const HOST = PROD
|
export const HOST = window.location.host;
|
||||||
? "github-stats-extended.vercel.app"
|
|
||||||
: "github-stats-extended-preview.vercel.app";
|
|
||||||
|
|
||||||
const REDIRECT_URI = `https://${HOST}/frontend`;
|
const REDIRECT_URI = `https://${HOST}/frontend`;
|
||||||
|
|
||||||
export const GITHUB_PRIVATE_AUTH_URL = `https://github.com/login/oauth/authorize?scope=user,repo&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}?mode=private`;
|
export const GITHUB_PRIVATE_AUTH_URL = `https://github.com/login/oauth/authorize?scope=user,repo&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}?mode=private`;
|
||||||
export const GITHUB_PUBLIC_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}?mode=public`;
|
export const GITHUB_PUBLIC_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}?mode=public`;
|
||||||
|
|
||||||
export const DEMO_USER = "anuraghazra";
|
export const DEMO_USER = "anuraghazra" as string;
|
||||||
export const DEMO_REPO = "anuraghazra/github-readme-stats";
|
export const DEMO_REPO = "anuraghazra/github-readme-stats" as string;
|
||||||
export const DEMO_GIST = "bbfce31e0217a3689c8d961a356cb10d";
|
export const DEMO_GIST = "bbfce31e0217a3689c8d961a356cb10d" as string;
|
||||||
export const DEMO_WAKATIME_USER = "alan";
|
export const DEMO_WAKATIME_USER = "alan" as string;
|
||||||
|
|
||||||
|
// imported backend code expects process.env to be defined
|
||||||
window.process = {
|
window.process = {
|
||||||
env: {
|
env: {},
|
||||||
FETCH_MULTI_PAGE_STARS: "10",
|
|
||||||
PAT_1: "placeholderPAT", // so the backend's retryer.js sees there is 1 PAT and sets `RETRIES` accordingly
|
|
||||||
},
|
|
||||||
} as (typeof window)["process"];
|
} as (typeof window)["process"];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import BounceLoader from "react-spinners/BounceLoader";
|
import { BounceLoader } from "react-spinners";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import { authenticate } from "../../api/user";
|
import { authenticate } from "../../api/user";
|
||||||
@@ -48,7 +48,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
// for stage two
|
// for stage two
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>(userId);
|
const [selectedUserId, setSelectedUserId] = useState(userId);
|
||||||
const [repo, setRepo] = useState(DEMO_REPO);
|
const [repo, setRepo] = useState(DEMO_REPO);
|
||||||
const [gist, setGist] = useState(DEMO_GIST);
|
const [gist, setGist] = useState(DEMO_GIST);
|
||||||
const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER);
|
const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER);
|
||||||
@@ -76,6 +76,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
>();
|
>();
|
||||||
const [customTitle, setCustomTitle] = useState("");
|
const [customTitle, setCustomTitle] = useState("");
|
||||||
const [langsCount, setLangsCount] = useState<number | undefined>();
|
const [langsCount, setLangsCount] = useState<number | undefined>();
|
||||||
|
const [hideValues, setHideValues] = useState(false);
|
||||||
const [showAllStats, setShowAllStats] = useState(false);
|
const [showAllStats, setShowAllStats] = useState(false);
|
||||||
const [showIcons, setShowIcons] = useState(false);
|
const [showIcons, setShowIcons] = useState(false);
|
||||||
const [includeAllCommits, setIncludeAllCommits] = useState(true);
|
const [includeAllCommits, setIncludeAllCommits] = useState(true);
|
||||||
@@ -121,6 +122,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
descriptionLines,
|
descriptionLines,
|
||||||
customTitle,
|
customTitle,
|
||||||
langsCount,
|
langsCount,
|
||||||
|
hideValues,
|
||||||
showAllStats,
|
showAllStats,
|
||||||
showIcons,
|
showIcons,
|
||||||
includeAllCommits,
|
includeAllCommits,
|
||||||
@@ -306,6 +308,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
|
|||||||
setCustomTitle={setCustomTitle}
|
setCustomTitle={setCustomTitle}
|
||||||
langsCount={langsCount}
|
langsCount={langsCount}
|
||||||
setLangsCount={setLangsCount}
|
setLangsCount={setLangsCount}
|
||||||
|
hideValues={hideValues}
|
||||||
|
setHideValues={setHideValues}
|
||||||
showAllStats={showAllStats}
|
showAllStats={showAllStats}
|
||||||
setShowAllStats={setShowAllStats}
|
setShowAllStats={setShowAllStats}
|
||||||
showIcons={showIcons}
|
showIcons={showIcons}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const baseOptions = {
|
|||||||
descriptionLines: undefined,
|
descriptionLines: undefined,
|
||||||
customTitle: "",
|
customTitle: "",
|
||||||
langsCount: undefined,
|
langsCount: undefined,
|
||||||
|
hideValues: false,
|
||||||
showAllStats: false,
|
showAllStats: false,
|
||||||
showIcons: false,
|
showIcons: false,
|
||||||
includeAllCommits: false,
|
includeAllCommits: false,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface Options {
|
|||||||
descriptionLines: number | undefined;
|
descriptionLines: number | undefined;
|
||||||
customTitle: string;
|
customTitle: string;
|
||||||
langsCount: number | undefined;
|
langsCount: number | undefined;
|
||||||
|
hideValues: boolean;
|
||||||
showAllStats: boolean;
|
showAllStats: boolean;
|
||||||
showIcons: boolean;
|
showIcons: boolean;
|
||||||
includeAllCommits: boolean;
|
includeAllCommits: boolean;
|
||||||
@@ -41,6 +42,7 @@ export function getFullSuffix({
|
|||||||
descriptionLines,
|
descriptionLines,
|
||||||
customTitle,
|
customTitle,
|
||||||
langsCount,
|
langsCount,
|
||||||
|
hideValues,
|
||||||
showAllStats,
|
showAllStats,
|
||||||
showIcons,
|
showIcons,
|
||||||
includeAllCommits,
|
includeAllCommits,
|
||||||
@@ -138,6 +140,10 @@ export function getFullSuffix({
|
|||||||
fullSuffix += `&langs_count=${langsCount}`;
|
fullSuffix += `&langs_count=${langsCount}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hideValues && selectedCard === CardType.TOP_LANGS) {
|
||||||
|
fullSuffix += `&hide_values=true`;
|
||||||
|
}
|
||||||
|
|
||||||
if (showAllStats && selectedCard === CardType.STATS) {
|
if (showAllStats && selectedCard === CardType.STATS) {
|
||||||
fullSuffix += `&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented`;
|
fullSuffix += `&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ interface CustomizeStageProps {
|
|||||||
setCustomTitle: Updater<string>;
|
setCustomTitle: Updater<string>;
|
||||||
langsCount: number | undefined;
|
langsCount: number | undefined;
|
||||||
setLangsCount: Updater<number | undefined>;
|
setLangsCount: Updater<number | undefined>;
|
||||||
|
hideValues: boolean;
|
||||||
|
setHideValues: Updater<boolean>;
|
||||||
showIcons: boolean;
|
showIcons: boolean;
|
||||||
setShowIcons: Updater<boolean>;
|
setShowIcons: Updater<boolean>;
|
||||||
showAllStats: boolean;
|
showAllStats: boolean;
|
||||||
@@ -87,6 +89,8 @@ export function CustomizeStage({
|
|||||||
setCustomTitle,
|
setCustomTitle,
|
||||||
langsCount,
|
langsCount,
|
||||||
setLangsCount,
|
setLangsCount,
|
||||||
|
hideValues,
|
||||||
|
setHideValues,
|
||||||
showIcons,
|
showIcons,
|
||||||
setShowIcons,
|
setShowIcons,
|
||||||
showAllStats,
|
showAllStats,
|
||||||
@@ -321,6 +325,15 @@ export function CustomizeStage({
|
|||||||
max={20}
|
max={20}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{cardType === CardType.TOP_LANGS && (
|
||||||
|
<CheckboxSection
|
||||||
|
title="Hide Values?"
|
||||||
|
text="Hide language percentages or bytes while keeping the selected layout visible."
|
||||||
|
question="Hide values?"
|
||||||
|
checked={hideValues}
|
||||||
|
onCheckedChange={setHideValues}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{cardType === CardType.WAKATIME && (
|
{cardType === CardType.WAKATIME && (
|
||||||
<CheckboxSection
|
<CheckboxSection
|
||||||
title="Show Percentages?"
|
title="Show Percentages?"
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
|
import { themes } from "@stats-organization/github-readme-stats-core";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
// @ts-expect-error this will be provided by the npm package
|
|
||||||
import { themes } from "../../../backend/themes/index";
|
|
||||||
import { Card } from "../../../components/Card/Card";
|
import { Card } from "../../../components/Card/Card";
|
||||||
|
|
||||||
// to be removed once npm package has been created
|
|
||||||
type ThemeData = Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
title_color: string;
|
|
||||||
icon_color: string;
|
|
||||||
text_color: string;
|
|
||||||
bg_color: string;
|
|
||||||
border_color: string;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
const excludedThemes = [
|
const excludedThemes = [
|
||||||
"merko",
|
"merko",
|
||||||
"blue-green",
|
"blue-green",
|
||||||
@@ -25,10 +12,9 @@ const excludedThemes = [
|
|||||||
"holi",
|
"holi",
|
||||||
];
|
];
|
||||||
|
|
||||||
const themeList = Object.keys(
|
const themeList = Object.keys(themes).filter(
|
||||||
/* Needed until themes is typed correctly and retrieved from npm package */
|
(myTheme) => !excludedThemes.includes(myTheme),
|
||||||
themes as ThemeData,
|
);
|
||||||
).filter((myTheme) => !excludedThemes.includes(myTheme));
|
|
||||||
|
|
||||||
interface ThemeStageProps {
|
interface ThemeStageProps {
|
||||||
fullSuffix: string;
|
fullSuffix: string;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": ["./tsconfig.json"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"customConditions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
"installCommand": "cd ../../ && git clean ./apps -fx && pnpm install && rm -rf ./apps/frontend/node_modules && ./vercel-preparation.sh && pnpm --filter ./apps/frontend/ --legacy deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/frontend/node_modules/",
|
|
||||||
"buildCommand": "pnpm run build",
|
|
||||||
"outputDirectory": "build"
|
"outputDirectory": "build"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,29 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react";
|
||||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
import { defineProject } from "vitest/config";
|
||||||
import StringReplace from "vite-plugin-string-replace";
|
|
||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineProject({
|
||||||
base: "/frontend/",
|
base: "/frontend/",
|
||||||
plugins: [
|
plugins: [react(), tailwindcss()],
|
||||||
StringReplace([
|
|
||||||
{
|
|
||||||
search: "process.env",
|
|
||||||
replace: "window.process.env",
|
|
||||||
fileName: ".*backend.*",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
nodePolyfills({
|
|
||||||
include: [
|
|
||||||
"path",
|
|
||||||
"querystring",
|
|
||||||
"url",
|
|
||||||
"http",
|
|
||||||
"util",
|
|
||||||
"stream",
|
|
||||||
"buffer",
|
|
||||||
],
|
|
||||||
exclude: ["fs", "net"],
|
|
||||||
}),
|
|
||||||
|
|
||||||
react(),
|
|
||||||
tailwindcss(),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* mock pg (postgres) package in the browser to avoid runtime errors
|
|
||||||
* @see apps/backend/src/common/database.js
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
name: "empty-pg-package",
|
|
||||||
resolveId(id) {
|
|
||||||
if (id === "pg") {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
load(id) {
|
|
||||||
if (id === "pg") {
|
|
||||||
return "export class Pool { constructor(config) {} }";
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
build: {
|
build: {
|
||||||
outDir: "build",
|
outDir: "build",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|
||||||
/** @todo use chunks to split bundle? */
|
/** @todo use chunks to split bundle? */
|
||||||
chunkSizeWarningLimit: 800,
|
chunkSizeWarningLimit: 800,
|
||||||
|
|
||||||
|
rolldownOptions: {
|
||||||
|
external: ["pg"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
conditions: ["@stats/source"],
|
||||||
alias: [
|
alias: [
|
||||||
{
|
{
|
||||||
find: "../src/fetchers/wakatime.js",
|
find: "../fetchers/wakatime.js",
|
||||||
replacement: path.resolve(
|
replacement: path.resolve(
|
||||||
import.meta.dirname,
|
import.meta.dirname,
|
||||||
"src/wakatime-override.ts",
|
"src/wakatime-override.ts",
|
||||||
@@ -72,7 +32,6 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
dir: "./src",
|
dir: path.join(import.meta.dirname, "./src"),
|
||||||
exclude: ["**/backend/**"],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ GitHub Stats Extended comes with several built-in themes (e.g. `dark`, `radical`
|
|||||||
|
|
||||||
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Stats Extended Themes" width="600px"/>
|
<img src="https://res.cloudinary.com/anuraghazra/image/upload/v1595174536/grs-themes_l4ynja.png" alt="GitHub Stats Extended Themes" width="600px"/>
|
||||||
|
|
||||||
You can look at a preview for [all available themes](../backend/themes/README.md) or checkout the [theme config file](../backend/themes/index.js). Please note that we paused the addition of new themes to decrease maintenance efforts; all pull requests related to new themes will be closed.
|
You can look at a preview for [all available themes](../packages/core/src/themes/README.md) or checkout the [theme config file](../packages/core/src/themes/index.js). Please note that we paused the addition of new themes to decrease maintenance efforts; all pull requests related to new themes will be closed.
|
||||||
|
|
||||||
#### Responsive Card Theme
|
#### Responsive Card Theme
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ We have included a `transparent` theme that has a transparent background. This t
|
|||||||
|
|
||||||
##### Add transparent alpha channel to a themes bg\_color
|
##### Add transparent alpha channel to a themes bg\_color
|
||||||
|
|
||||||
You can use the `bg_color` parameter to make any of [the available themes](../backend/themes/README.md) transparent. This is done by setting the `bg_color` to a color with a transparent alpha channel (i.e. `bg_color=00000000`):
|
You can use the `bg_color` parameter to make any of [the available themes](../packages/core/src/themes/README.md) transparent. This is done by setting the `bg_color` to a color with a transparent alpha channel (i.e. `bg_color=00000000`):
|
||||||
|
|
||||||
```md
|
```md
|
||||||

|

|
||||||
@@ -181,7 +181,7 @@ You can customize the appearance of all your cards however you wish with URL par
|
|||||||
| `border_color` | Card's border color. Does not apply when `hide_border` is enabled. | string (hex color) | `e4e2e2` |
|
| `border_color` | Card's border color. Does not apply when `hide_border` is enabled. | string (hex color) | `e4e2e2` |
|
||||||
| `bg_color` | Card's background color. | string (hex color or a gradient in the form of *angle,start,end*) | `fffefe` |
|
| `bg_color` | Card's background color. | string (hex color or a gradient in the form of *angle,start,end*) | `fffefe` |
|
||||||
| `hide_border` | Hides the card's border. | boolean | `false` |
|
| `hide_border` | Hides the card's border. | boolean | `false` |
|
||||||
| `theme` | Name of the theme, choose from [all available themes](../backend/themes/README.md). | enum | `default` |
|
| `theme` | Name of the theme, choose from [all available themes](../packages/core/src/themes/README.md). | enum | `default` |
|
||||||
| `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `21600` |
|
| `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `21600` |
|
||||||
| `locale` | Sets the language in the card, you can check full list of available locales [here](#available-locales). | enum | `en` |
|
| `locale` | Sets the language in the card, you can check full list of available locales [here](#available-locales). | enum | `en` |
|
||||||
| `border_radius` | Corner rounding on the card. | number | `4.5` |
|
| `border_radius` | Corner rounding on the card. | number | `4.5` |
|
||||||
@@ -323,6 +323,7 @@ You can customize the appearance and behavior of the pinned repository card usin
|
|||||||
| Name | Description | Type | Default value |
|
| Name | Description | Type | Default value |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `show_owner` | Shows the repo's owner name. | boolean | `false` |
|
| `show_owner` | Shows the repo's owner name. | boolean | `false` |
|
||||||
|
| `browser_rendering` | Compute text wrapping of repository description natively in the browser, instead of computing it server-side. | boolean | `false` |
|
||||||
| `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` |
|
| `description_lines_count` | Manually set the number of lines for the description. Specified value will be clamped between 1 and 3. If this parameter is not specified, the number of lines will be automatically adjusted according to the actual length of the description. | number | `null` |
|
||||||
| `card_width` | Sets the card's width manually. | number | `400px (approx.)` |
|
| `card_width` | Sets the card's width manually. | number | `400px (approx.)` |
|
||||||
| `show_icons` | Shows icons near all stats enabled via `show`. | boolean | `true` |
|
| `show_icons` | Shows icons near all stats enabled via `show`. | boolean | `true` |
|
||||||
@@ -368,6 +369,7 @@ You can customize the appearance and behavior of the gist card using the [common
|
|||||||
| Name | Description | Type | Default value |
|
| Name | Description | Type | Default value |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `show_owner` | Shows the gist's owner name. | boolean | `false` |
|
| `show_owner` | Shows the gist's owner name. | boolean | `false` |
|
||||||
|
| `browser_rendering` | Compute text wrapping of gist description natively in the browser, instead of computing it server-side. | boolean | `false` |
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|
||||||
@@ -417,6 +419,7 @@ You can customize the appearance and behavior of the top languages card using th
|
|||||||
| `disable_animations` | Disables all animations in the card. | boolean | `false` |
|
| `disable_animations` | Disables all animations in the card. | boolean | `false` |
|
||||||
| `prog_bar_bg_color` | Background color of the bars. (Applies only to `normal` layout.) | string (hex color) | `#ddd` |
|
| `prog_bar_bg_color` | Background color of the bars. (Applies only to `normal` layout.) | string (hex color) | `#ddd` |
|
||||||
| `hide_progress` | Uses the compact layout option, hides percentages, and removes the bars. | boolean | `false` |
|
| `hide_progress` | Uses the compact layout option, hides percentages, and removes the bars. | boolean | `false` |
|
||||||
|
| `hide_values` | Hides language percentages or bytes while keeping the progress bars or chart. | boolean | `false` |
|
||||||
| `size_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `1` |
|
| `size_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `1` |
|
||||||
| `count_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `0` |
|
| `count_weight` | Configures language stats algorithm (see [Language stats algorithm](#language-stats-algorithm)). | integer | `0` |
|
||||||
| `stats_format` | Switches between two available formats for language's stats `percentages` and `bytes`. | enum | `percentages` |
|
| `stats_format` | Switches between two available formats for language's stats `percentages` and `bytes`. | enum | `percentages` |
|
||||||
|
|||||||
+3
-2
@@ -123,6 +123,7 @@ Click on the deploy button to get started!
|
|||||||
13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL`
|
13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL`
|
||||||
14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly
|
14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly
|
||||||
15. optional: in addition to the Vercel project based on the `apps/backend` folder, create a second project based on the `apps/frontend` folder. No environment variables needed.
|
15. optional: in addition to the Vercel project based on the `apps/backend` folder, create a second project based on the `apps/frontend` folder. No environment variables needed.
|
||||||
|
16. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -163,12 +164,12 @@ GitHub Stats Extended provides several environment variables that can be used to
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>UPDATE_AFTER_HOURS</code></td>
|
<td><code>UPDATE_AFTER_HOURS</code></td>
|
||||||
<td>Sets the duration in hours after which the server <a href="fork.md#improved-performance-and-latency">proactively regenerates</a> a previously requested card.</td>
|
<td>Sets the duration in hours after which the server <a href="fork.md#improved-performance-and-latency">proactively regenerates</a> a previously requested card. Defaults to 11 hours.</td>
|
||||||
<td>Any int or float</td>
|
<td>Any int or float</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DELETE_AFTER_HOURS</code></td>
|
<td><code>DELETE_AFTER_HOURS</code></td>
|
||||||
<td>Sets the duration in hours after which the server stops <a href="fork.md#improved-performance-and-latency">proactively regenerating</a> a previously requested card if it hasn't been requested again in the meantime.</td>
|
<td>Sets the duration in hours after which the server stops <a href="fork.md#improved-performance-and-latency">proactively regenerating</a> a previously requested card if it hasn't been requested again in the meantime. Defaults to 8 days, i.e. 192 hours.</td>
|
||||||
<td>Any int or float</td>
|
<td>Any int or float</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
+9
-8
@@ -1,6 +1,6 @@
|
|||||||
# Fork Information
|
# Fork Information
|
||||||
|
|
||||||
This project is an actively maintained fork of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
|
This project is an actively maintained fork and extension of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats).
|
||||||
|
|
||||||
## Key Differences
|
## Key Differences
|
||||||
|
|
||||||
@@ -25,6 +25,11 @@ The resolution of this most requested feature in github-readme-stats was [origin
|
|||||||
|
|
||||||
GitHub-Stats-Extended proactively precomputes and caches cards. This solves the problem where [cards wouldn't load on the first try](https://github.com/anuraghazra/github-readme-stats/issues/2603). It also gives GitHub-Stats-Extended more time while generating cards in the background, which allows it to fetch more repo data:
|
GitHub-Stats-Extended proactively precomputes and caches cards. This solves the problem where [cards wouldn't load on the first try](https://github.com/anuraghazra/github-readme-stats/issues/2603). It also gives GitHub-Stats-Extended more time while generating cards in the background, which allows it to fetch more repo data:
|
||||||
|
|
||||||
|
### Customization of top languages card
|
||||||
|
GitHub-Stats-Extended allows you to display your top languages without any numbers via the `hide_values` parameter. And the `prog_bar_bg_color` parameter allows you to customize the background color of the progress bars, e.g. by setting it to white:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Multi-page fetching for accurate star counts
|
### Multi-page fetching for accurate star counts
|
||||||
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
|
GitHub-Stats-Extended fetches up to 1000 of your starred repositories to accurately compute your stars count. In github-readme-stats, this is limited to 100 repos because github-readme-stats doesn't have the above-mentioned performance improvements.
|
||||||
|
|
||||||
@@ -67,16 +72,12 @@ One of the valued maintainers [wrote](https://github.com/anuraghazra/github-read
|
|||||||
>
|
>
|
||||||
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
|
> The volume is overwhelming for the small team, especially taking into account that right now I'm alone online and working only sometimes when I have a free hours, so it took some time to get to your PR.
|
||||||
|
|
||||||
So I decided to implement some of the highly requested features and make the enhanced project available to everyone.
|
So I decided to implement some of the highly requested features and make the enhanced project available to everyone. Since the initial release of this fork I have been in contact with the maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) and we decided to join forces. github-stats-extended is now in the process of becoming the successor of github-readme-stats.
|
||||||
|
|
||||||
## Upstream Sync Strategy
|
|
||||||
|
|
||||||
The maintainers of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) keep their project up to date with dependency upgrades and do occasionally merge some pull requests. I regularly merge the latest upstream changes into this fork to keep it in sync with the upstream repo.
|
|
||||||
|
|
||||||
I also try to structure my own changes in a way that minimizes merge conflicts with the upstream repo. So some of my changes might even be merged back into [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) if the upstream maintainers are interested.
|
|
||||||
|
|
||||||
## Compatibility Notes
|
## Compatibility Notes
|
||||||
|
|
||||||
|
If there should be any changes in github-readme-stats in the future, they will be synced into this fork.
|
||||||
|
|
||||||
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Generally, additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
GitHub-Stats-Extended aims to be fully compatible with [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). Generally, additional functionality introduced in this fork has to be explicitly enabled via some parameter.
|
||||||
|
|
||||||
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
|
So you can change an existing stats card url from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) to GitHub-Stats-Extended simply by changing the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`. The card will look the same.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { includeIgnoreFile } from "@eslint/compat";
|
import { includeIgnoreFile } from "@eslint/compat";
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import { defineConfig } from "eslint/config";
|
import { defineConfig } from "eslint/config";
|
||||||
|
import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript";
|
||||||
import { importX } from "eslint-plugin-import-x";
|
import { importX } from "eslint-plugin-import-x";
|
||||||
import { default as jsdoc } from "eslint-plugin-jsdoc";
|
import { default as jsdoc } from "eslint-plugin-jsdoc";
|
||||||
import react from "eslint-plugin-react";
|
import react from "eslint-plugin-react";
|
||||||
@@ -18,6 +19,25 @@ export default defineConfig(
|
|||||||
|
|
||||||
{
|
{
|
||||||
extends: [importX.flatConfigs.recommended, importX.flatConfigs.typescript],
|
extends: [importX.flatConfigs.recommended, importX.flatConfigs.typescript],
|
||||||
|
settings: {
|
||||||
|
"import-x/resolver-next": [
|
||||||
|
createTypeScriptImportResolver({
|
||||||
|
conditionNames: [
|
||||||
|
/** Keep in sync with `tsconfig.base.json#customConditions` */
|
||||||
|
"@stats/source",
|
||||||
|
|
||||||
|
"types",
|
||||||
|
"import",
|
||||||
|
|
||||||
|
"require",
|
||||||
|
"node",
|
||||||
|
"node-addons",
|
||||||
|
"browser",
|
||||||
|
"default",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"import-x/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
"import-x/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||||
"import-x/order": [
|
"import-x/order": [
|
||||||
|
|||||||
+2
-13
@@ -2,21 +2,10 @@
|
|||||||
"$schema": "./node_modules/knip/schema.json",
|
"$schema": "./node_modules/knip/schema.json",
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"apps/backend": {
|
"apps/backend": {
|
||||||
"entry": ["api-renamed/*.js", "express.js", "tests/bench/*.bench.js"],
|
"entry": ["api-renamed/*.js", "express.js", "tests/bench/*.bench.js"]
|
||||||
"ignoreFiles": [
|
|
||||||
"_dot_vercel_copy/**/*" // Hopefully by using a npm package this file will be removed
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"apps/frontend": {
|
"apps/frontend": {
|
||||||
"entry": ["src/wakatime-override.ts"],
|
"entry": ["src/wakatime-override.ts"]
|
||||||
|
|
||||||
"ignoreDependencies": [
|
|
||||||
// below dependencies are added because backend folder is copied inside frontend folder,
|
|
||||||
// so some of his dependencies must be present here
|
|
||||||
"github-username-regex",
|
|
||||||
"emoji-name-map",
|
|
||||||
"word-wrap"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-19
@@ -1,35 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "@stats-organization/root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "2.0.2",
|
"@eslint/compat": "2.0.5",
|
||||||
"@eslint/js": "9.39.3",
|
"@eslint/js": "10.0.1",
|
||||||
"@playwright/test": "1.58.2",
|
"@playwright/test": "1.59.1",
|
||||||
"@types/node": "24.10.13",
|
"@types/node": "24.12.2",
|
||||||
"eslint": "9.39.2",
|
"@vitest/coverage-v8": "catalog:default",
|
||||||
|
"eslint": "10.2.1",
|
||||||
"eslint-import-resolver-typescript": "4.4.4",
|
"eslint-import-resolver-typescript": "4.4.4",
|
||||||
"eslint-plugin-import-x": "4.16.1",
|
"eslint-plugin-import-x": "4.16.2",
|
||||||
"eslint-plugin-jsdoc": "62.7.1",
|
"eslint-plugin-jsdoc": "62.9.0",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-hooks": "7.0.1",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"globals": "17.3.0",
|
"globals": "17.5.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"knip": "5.85.0",
|
"knip": "6.7.0",
|
||||||
"lint-staged": "16.2.7",
|
"lint-staged": "16.4.0",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.3",
|
||||||
"typescript": "5.9.3",
|
"turbo": "2.9.6",
|
||||||
"typescript-eslint": "8.56.1"
|
"typescript": "6.0.3",
|
||||||
|
"typescript-eslint": "8.59.0",
|
||||||
|
"vitest": "catalog:default"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
"build:packages": "turbo run build --filter=./packages/*",
|
||||||
|
"build:frontend": "turbo run build --filter=./apps/frontend",
|
||||||
|
"dev:frontend": "pnpm run --filter=./apps/frontend dev",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:coverage": "vitest --config vitest.config.coverage.ts",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"lint": "eslint",
|
"lint": "turbo run lint",
|
||||||
"lint:fix": "eslint --fix",
|
"lint:eslint": "eslint",
|
||||||
|
"lint:eslint:fix": "eslint --fix",
|
||||||
"lint:knip": "knip",
|
"lint:knip": "knip",
|
||||||
"typecheck": "tsc --build --noEmit"
|
"typecheck": "turbo run typecheck"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx,css,json,jsonc,yaml,yml}": "prettier --write"
|
"*.{js,jsx,ts,tsx,css,json,jsonc,yaml,yml}": "prettier --write"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Core library powering [GitHub Stats Extended](https://github.com/stats-organization/github-stats-extended) and [GitHub Readme Stats Action](https://github.com/readme-tools/github-readme-stats-action).
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"name": "@stats-organization/github-readme-stats-core",
|
||||||
|
"version": "2.1.2",
|
||||||
|
"type": "module",
|
||||||
|
"homepage": "https://github-stats-extended.vercel.app/frontend",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/stats-organization/github-stats-extended/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stats-organization/github-stats-extended.git",
|
||||||
|
"directory": "packages/core"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Anurag Hazra",
|
||||||
|
"url": "https://github.com/anuraghazra/"
|
||||||
|
},
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Rick Staa",
|
||||||
|
"url": "https://github.com/rickstaa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alexandr Garbuzov",
|
||||||
|
"url": "https://github.com/qwerty541"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Abhijit Gupta",
|
||||||
|
"url": "https://github.com/avgupta456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Marco Pasqualetti",
|
||||||
|
"url": "https://github.com/marcalexiei"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "martin-mfg",
|
||||||
|
"url": "https://github.com/martin-mfg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "24.x"
|
||||||
|
},
|
||||||
|
"description": "Dynamically generate stats for your GitHub readme",
|
||||||
|
"keywords": [
|
||||||
|
"github-readme-stats",
|
||||||
|
"readme-stats",
|
||||||
|
"cards",
|
||||||
|
"card-generator",
|
||||||
|
"github-stats",
|
||||||
|
"github-stats-extended",
|
||||||
|
"github-readme-stats-extended"
|
||||||
|
],
|
||||||
|
"main": "./build/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"@stats/source": "./src/index.ts",
|
||||||
|
"default": "./build/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build",
|
||||||
|
"src",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"prepack": "pnpm run build",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:update:snapshot": "vitest -u",
|
||||||
|
"test:e2e": "vitest --config vitest.config.e2e.ts",
|
||||||
|
"bench": "vitest bench --run --config vitest.config.bench.ts",
|
||||||
|
"lint": "eslint",
|
||||||
|
"typecheck": "tsc -p tsconfig.typecheck.json",
|
||||||
|
"theme-readme-gen": "node scripts/generate-theme-doc",
|
||||||
|
"generate-langs-json": "node scripts/generate-langs-json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/dom": "10.4.1",
|
||||||
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
|
"@uppercod/css-to-object": "1.1.1",
|
||||||
|
"axios-mock-adapter": "2.1.0",
|
||||||
|
"js-yaml": "4.1.1",
|
||||||
|
"jsdom": "29.0.2",
|
||||||
|
"vitest": "catalog:default"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.15.2",
|
||||||
|
"emoji-name-map": "^2.0.3",
|
||||||
|
"github-username-regex": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
import { themes } from "../themes/index.js";
|
import { themes } from "../src/themes/index.js";
|
||||||
|
|
||||||
const TARGET_FILE = "./themes/README.md";
|
const TARGET_FILE = "./src/themes/README.md";
|
||||||
const REPO_CARD_LINKS_FLAG = "<!-- REPO_CARD_LINKS -->";
|
const REPO_CARD_LINKS_FLAG = "<!-- REPO_CARD_LINKS -->";
|
||||||
const STAT_CARD_LINKS_FLAG = "<!-- STATS_CARD_LINKS -->";
|
const STAT_CARD_LINKS_FLAG = "<!-- STATS_CARD_LINKS -->";
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user