Compare commits
8
Commits
fcd08cbaac
...
ff20d1cb11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff20d1cb11 | ||
|
|
80b73b62e4 | ||
|
|
06b16b9033 | ||
|
|
d73aa076ad | ||
|
|
729b7f566f | ||
|
|
d74fd3a248 | ||
|
|
b7c6c5df7b | ||
|
|
8fb05b3f5b |
+10
-7
@@ -1,21 +1,24 @@
|
||||
.git
|
||||
.gitignore
|
||||
.gitea
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
LICENSE
|
||||
VERSION
|
||||
node_modules/
|
||||
coverge/
|
||||
coverage/
|
||||
test/
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
|
||||
# SQLite database files
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
sqlite*
|
||||
sqldiff
|
||||
sqldiff
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
*.tar.gz
|
||||
@@ -0,0 +1,11 @@
|
||||
# Server port
|
||||
PORT=3000
|
||||
|
||||
# Log level: fatal | error | warn | info | debug | trace
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Node environment: production | development | test
|
||||
NODE_ENV=development
|
||||
|
||||
# App version override (optional – falls back to VERSION file)
|
||||
# APP_VERSION=1.0.0
|
||||
+8
-3
@@ -1,8 +1,8 @@
|
||||
FROM node:23-alpine3.21
|
||||
FROM node:24-alpine
|
||||
RUN apk upgrade --update-cache --available
|
||||
|
||||
ARG VERSION
|
||||
LABEL version=${VERSION}} description="BadgeDex"
|
||||
LABEL version=${VERSION} description="BadgeDex"
|
||||
ENV APP_VERSION=${VERSION}
|
||||
|
||||
# Create a group and user && Create directory for files
|
||||
@@ -18,10 +18,15 @@ USER badgedex
|
||||
COPY --chown=badgedex:badgedex . .
|
||||
|
||||
#Install dependencies
|
||||
RUN npm install
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Expose API port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3000/version || exit 1
|
||||
|
||||
#Start node application
|
||||
ENTRYPOINT ["npm"]
|
||||
|
||||
|
||||
@@ -1,95 +1,191 @@
|
||||
# badgedex
|
||||
# BadgeDex
|
||||
|
||||
BadgeDex
|
||||
Badge generátor szolgáltatás – SVG badge-ek készítése GitHub profilokhoz és projektekhez.
|
||||
|
||||
# BadgeDex API – URL Paraméterek
|
||||
## Badge
|
||||
|
||||
Az alábbi paraméterekkel szabályozhatod a `/badge` végpont működését:
|
||||
|
||||
| Paraméter | Kötelező | Alapértelmezett érték | Leírás | Példa értékek |
|
||||
|-------------|----------|-----------------------|----------------------------------------------------------------------------------------|------------------------------|
|
||||
| `icon` | Igen |`simpleicons` | A SimpleIcons ikon neve (kisbetű, kötőjellel). | `github`, `docker`, `nginx` |
|
||||
| `label` | Nem |`Simple Icons` | A badge-en megjelenő szöveg. | `MyApp`, `Status` |
|
||||
| `style` | Nem |`rect` | A badge stílusa. rect négyzetes, flat lekerekitett sarkok | `rect`, `flat` |
|
||||
| `bgicon` | Nem |`none` | Icon Háttérszín (hex vagy CSS színnév). Alapértelmezetten átlátszó. | `#fff`, `#222`, `red` |
|
||||
| `bglabel` | Nem |`none` | Label Háttérszín (hex vagy CSS színnév). Alapértelmezetten átlátszó. | `#fff`, `#222`, `red` |
|
||||
| `color` | Nem |`#000000` | Szöveg színe (hex vagy CSS színnév). Harmonizál az icon szinével ha nem kérünk külön. | `#333`, `white` |
|
||||
| `size` | Nem |`24` | Ikon (és badge) magassága pixelben. | `16`, `32`, `64` |
|
||||
| `labelpos` | Nem | `right` | Szöveg pozíciója az ikonhoz képest: `right`, `left`, `above`, `below`. | `left`, `above` |
|
||||
| `fontweight`| Nem | `normal` | Szöveg vastagsága (`normal`, `bold`, `lighter`, szám is lehet pl. `600`). | `bold`, `600` |
|
||||
|
||||
## Példák
|
||||
|
||||
- **Alap badge:**
|
||||
`/badge`
|
||||
|
||||
- **React ikon, label, size: 64, font: bold, fehér label szöveg, sárga label háttérszín, piros ikon háttérszín, label jobbra**
|
||||
`/badge?icon=react&label=React%20expert%20level&size=64&fontweight=bold&color=white&bgicon=red&bglabel=yellow&labelpos=right`
|
||||
|
||||
- **GitHub ikon, piros label háttér, fehér szöveg:**
|
||||
`/badge?icon=github&bglabel=red&color=white`
|
||||
|
||||
- **Szöveg balra, ikon jobbra:**
|
||||
`/badge?labelpos=left`
|
||||
|
||||
- **Szöveg az ikon fölött, félkövér betű:**
|
||||
`/badge?labelpos=above&fontweight=bold`
|
||||
|
||||
- **Nagyobb ikon és szöveg:**
|
||||
`/badge?size=48`
|
||||
A badge SVG formátumban tér vissza, így közvetlenül beilleszthető HTML-be, Markdownba, vagy `<img>` tag-be.
|
||||
|
||||
---
|
||||
|
||||
## Tag
|
||||
## API végpontok
|
||||
|
||||
Az alábbi paraméterekkel szabályozhatod a `/tag` végpont működését:
|
||||
### `GET /badge`
|
||||
|
||||
| Paraméter | Kötelező | Alapértelmezett érték | Leírás | Példa értékek |
|
||||
|-------------|----------|-----------------------|----------------------------------------------------------------------------------------|------------------------------|
|
||||
| `tag` | Igen |`badgedex` | A tag tartalma(kisbetű, kötőjellel). | `github`, `docker`, `nginx` |
|
||||
| `label` | Nem |`BadgeDex` | A tag label tartalma. | `MyApp`, `Status` |
|
||||
| `style` | Nem |`rect` | A badge stílusa. rect négyzetes, flat lekerekitett sarkok | `rect`, `flat` |
|
||||
| `bgtag` | Nem |`none` | Icon Háttérszín (hex vagy CSS színnév). Alapértelmezetten átlátszó. | `#fff`, `#222`, `red` |
|
||||
| `bglabel` | Nem |`none` | Label Háttérszín (hex vagy CSS színnév). Alapértelmezetten átlátszó. | `#fff`, `#222`, `red` |
|
||||
| `color` | Nem |`#000000` | Szöveg színe (hex vagy CSS színnév). Harmonizál az icon szinével ha nem kérünk külön. | `#333`, `white` |
|
||||
| `size` | Nem |`24` | Ikon (és badge) magassága pixelben. | `16`, `32`, `64` |
|
||||
| `labelpos` | Nem | `right` | Szöveg pozíciója az ikonhoz képest: `right`, `left`, `above`, `below`. | `left`, `above` |
|
||||
| `fontweight`| Nem | `normal` | Szöveg vastagsága (`normal`, `bold`, `lighter`, szám is lehet pl. `600`). | `bold`, `600` |
|
||||
SVG badge-et generál egy [Simple Icons](https://simpleicons.org/) ikon és szöveg alapján.
|
||||
|
||||
## Példák
|
||||
| Paraméter | Kötelező | Alapértelmezett | Leírás | Példák |
|
||||
|-------------|----------|-----------------|----------------------------------------------------------------------------------------|------------------------------|
|
||||
| `icon` | Nem | `simpleicons` | A SimpleIcons ikon neve (kisbetű, kötőjellel). | `github`, `docker`, `nginx` |
|
||||
| `label` | Nem | *(ikonnév)* | A badge-en megjelenő szöveg. Ha nincs megadva, az ikon neve jelenik meg. | `MyApp`, `Status` |
|
||||
| `style` | Nem | `rect` | Badge stílusa: `rect` (négyzetes) vagy `flat` (lekerekített sarkok). | `rect`, `flat` |
|
||||
| `bgicon` | Nem | `none` | Ikon háttérszín (hex vagy CSS színnév). Alapból átlátszó. | `#fff`, `#222`, `red` |
|
||||
| `bglabel` | Nem | `none` | Label háttérszín (hex vagy CSS színnév). Alapból átlátszó. | `#fff`, `#222`, `red` |
|
||||
| `color` | Nem | *(ikonszín)* | Szöveg színe (hex vagy CSS színnév). Ha nincs megadva, az ikon színét használja. | `#333`, `white` |
|
||||
| `size` | Nem | `24` | Ikon és badge magassága pixelben (8–256). | `16`, `32`, `64` |
|
||||
| `labelpos` | Nem | `right` | Szöveg pozíciója az ikonhoz képest. | `right`, `left`, `above`, `below` |
|
||||
| `fontweight`| Nem | `normal` | Szöveg vastagsága. | `normal`, `bold`, `600` |
|
||||
|
||||
- **Alap badge:**
|
||||
`/badge`
|
||||
#### Badge példák
|
||||
|
||||
- **React ikon, label, size: 64, font: bold, fehér label szöveg, sárga label háttérszín, piros ikon háttérszín, label jobbra**
|
||||
`/badge?icon=react&label=React%20expert%20level&size=64&fontweight=bold&color=white&bgicon=red&bglabel=yellow&labelpos=right`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/badge
|
||||
```
|
||||
|
||||
- **GitHub ikon, piros label háttér, fehér szöveg:**
|
||||
`/badge?icon=github&bglabel=red&color=white`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/badge?icon=github&bglabel=red&color=white
|
||||
```
|
||||
|
||||
- **Szöveg balra, ikon jobbra:**
|
||||
`/badge?labelpos=left`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/badge?icon=react&label=React%20expert&size=64&fontweight=bold&color=white&bgicon=red&bglabel=yellow&labelpos=right
|
||||
```
|
||||
|
||||
- **Szöveg az ikon fölött, félkövér betű:**
|
||||
`/badge?labelpos=above&fontweight=bold`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/badge?icon=docker&label=Docker&style=flat&bgicon=%232496ED&color=white
|
||||
```
|
||||
|
||||
- **Nagyobb ikon és szöveg:**
|
||||
`/badge?size=48`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/badge?icon=nginx&labelpos=left
|
||||
```
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/badge?icon=vue.js&labelpos=above&fontweight=bold&size=48
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
A badge SVG formátumban tér vissza, így közvetlenül beilleszthető HTML-be vagy Markdown
|
||||
### `GET /tag`
|
||||
|
||||
# Installation
|
||||
Badge-szerű SVG címkét generál szöveges tartalommal (stílus: shields.io-szerű).
|
||||
|
||||
## Run as Node.js application
|
||||
Clone this repo and `npm start`
|
||||
| Paraméter | Kötelező | Alapértelmezett | Leírás | Példák |
|
||||
|-------------|----------|-----------------|----------------------------------------------------------------------------------------|------------------------------|
|
||||
| `tag` | Nem | `badgedex` | A tag tartalma (bal oldali szöveg). | `build`, `coverage`, `v1.0` |
|
||||
| `label` | Nem | `BadgeDex` | A label tartalma (jobb oldali szöveg). | `passing`, `95%`, `stable` |
|
||||
| `style` | Nem | `rect` | Stílus: `rect` (négyzetes) vagy `flat` (lekerekített, árnyékolt). | `rect`, `flat` |
|
||||
| `bgtag` | Nem | `none` | Tag háttérszín (hex vagy CSS színnév). | `#555`, `green`, `red` |
|
||||
| `bglabel` | Nem | `none` | Label háttérszín (hex vagy CSS színnév). | `#007ec6`, `blue` |
|
||||
| `color` | Nem | `#000000` | Szöveg színe (hex vagy CSS színnév). | `#fff`, `white` |
|
||||
| `size` | Nem | `24` | Badge magassága pixelben (8–256). | `16`, `20`, `32` |
|
||||
| `fontweight`| Nem | `normal` | Szöveg vastagsága. | `normal`, `bold`, `600` |
|
||||
|
||||
## Run as Docker service
|
||||
#### Tag példák
|
||||
|
||||
`docker run -dit --name badgedex -p 3000:3000 badgedex`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/tag
|
||||
```
|
||||
|
||||
### Build for self hosted container service
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/tag?tag=build&label=passing&bgtag=%23555&bglabel=green&color=white
|
||||
```
|
||||
|
||||
`docker build --build-arg VERSION=$(cat VERSION) -t badgedex:$(cat VERSION) .`
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/tag?tag=coverage&label=98%25&bgtag=%23555&bglabel=%23007ec6&color=white
|
||||
```
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/tag?tag=license&label=AGPL--v3&bgtag=gray&bglabel=blue&color=white
|
||||
```
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/tag?tag=version&label=v0.0.1&style=flat&bgtag=%23333&bglabel=%23007ec6&color=white
|
||||
```
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
```
|
||||
/tag?tag=node.js&label=22.x&bgtag=%23339933&bglabel=black&color=white&size=20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `GET /version`
|
||||
|
||||
A szolgáltatás aktuális verzióját adja vissza JSON formátumban.
|
||||
|
||||
```json
|
||||
{ "badgedexVersion": "0.0.1" }
|
||||
```
|
||||
|
||||
A verzió forrása (prioritás szerint):
|
||||
1. `APP_VERSION` környezeti változó
|
||||
2. `VERSION` fájl a projekt gyökerében
|
||||
|
||||
---
|
||||
|
||||
## Környezeti változók
|
||||
|
||||
A konfiguráció `.env` fájllal történik (lásd `.env.example`):
|
||||
|
||||
| Változó | Alapértelmezett | Leírás |
|
||||
|-------------|-----------------|------------------------------------------------------|
|
||||
| `PORT` | `3000` | A szerver portja. |
|
||||
| `LOG_LEVEL` | `info` | Logolás szintje: `fatal`, `error`, `warn`, `info`, `debug`, `trace`. |
|
||||
| `NODE_ENV` | `development` | Környezet: `production`, `development`, `test`. |
|
||||
| `APP_VERSION` | *(nincs)* | Verzió override (opcionális, alapból a `VERSION` fájlból olvas). |
|
||||
|
||||
---
|
||||
|
||||
## Telepítés
|
||||
|
||||
### Node.js alkalmazásként
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd badgedex
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
A szerver a `http://localhost:3000` címen indul.
|
||||
|
||||
### Docker konténerként
|
||||
|
||||
```bash
|
||||
docker build --build-arg VERSION=$(cat VERSION) -t badgedex:$(cat VERSION) .
|
||||
docker run -dit --name badgedex -p 3000:3000 badgedex:$(cat VERSION)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fejlesztés
|
||||
|
||||
### Tesztek futtatása
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Jest + Supertest tesztekkel, axios mockkal. A lefedettségi riport automatikusan generálódik.
|
||||
Generated
+312
-4
@@ -10,11 +10,15 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0"
|
||||
"express-rate-limit": "^8.6.0",
|
||||
"helmet": "^8.1.0",
|
||||
"pino": "^10.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"supertest": "^7.1.3"
|
||||
}
|
||||
},
|
||||
@@ -909,6 +913,12 @@
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
@@ -1144,6 +1154,15 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
@@ -1554,6 +1573,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1673,10 +1699,20 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -1764,6 +1800,18 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -1820,6 +1868,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -2016,6 +2074,32 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz",
|
||||
"integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"ip-address": "^10.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz",
|
||||
"integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -2387,6 +2471,13 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -2489,6 +2580,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -3257,6 +3357,16 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/joycon": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3518,6 +3628,16 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -3589,6 +3709,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -3775,6 +3904,81 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
|
||||
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"real-require": "^0.2.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"thread-stream": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pino": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-abstract-transport": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
|
||||
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty": {
|
||||
"version": "13.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
|
||||
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.7",
|
||||
"dateformat": "^4.6.3",
|
||||
"fast-copy": "^4.0.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"help-me": "^5.0.0",
|
||||
"joycon": "^3.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"secure-json-parse": "^4.0.0",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"strip-json-comments": "^5.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"pino-pretty": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty/node_modules/strip-json-comments": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-std-serializers": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pirates": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||
@@ -3826,6 +4030,22 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prompts": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||
@@ -3859,6 +4079,17 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
@@ -3891,6 +4122,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -3922,6 +4159,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -4022,12 +4268,38 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
|
||||
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -4200,6 +4472,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -4221,6 +4502,15 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -4401,6 +4691,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
|
||||
"integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"real-require": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream/node_modules/real-require": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
|
||||
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
|
||||
+7
-3
@@ -5,11 +5,11 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "jest --coverage"
|
||||
"test": "NODE_ENV=test jest --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://7139337cdda033731edc580629d502be491a6a8f@gitea.mihalyandras.familyds.org/echomike/badgedex.git"
|
||||
"url": "https://gitea.mihalyandras.familyds.org/echomike/badgedex.git"
|
||||
},
|
||||
"keywords": [
|
||||
"badge",
|
||||
@@ -19,11 +19,15 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.1.0",
|
||||
"helmet": "^8.1.0"
|
||||
"express-rate-limit": "^8.6.0",
|
||||
"helmet": "^8.1.0",
|
||||
"pino": "^10.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"supertest": "^7.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
+38
-33
@@ -1,26 +1,26 @@
|
||||
const router = require('express').Router();
|
||||
const axios = require('axios');
|
||||
const { escapeSvg, safeColor } = require('../lib/sanitize');
|
||||
const { parseSize, parseStyle, parseLabelpos, parseFontweight } = require('../lib/validate');
|
||||
const { FONT_FAMILY, calcFontSize, estimateTextWidth } = require('../lib/svg-utils');
|
||||
const logger = require('../lib/logger');
|
||||
|
||||
router.get('/badge', async (req, res) => {
|
||||
const {
|
||||
icon = 'simpleicons',
|
||||
label = 'Simple Icons',
|
||||
style = 'rect', // badge stílusa: rect (négyzetes), flat (lekerekített sarkok)
|
||||
bgicon = 'none', // ikon háttérszín, alap: világosszürke
|
||||
bglabel = 'none', // label háttérszín, alap: sötétszürke
|
||||
color = '#000000',
|
||||
size = 24,
|
||||
labelpos = 'right',
|
||||
fontweight = 'normal',
|
||||
} = req.query;
|
||||
const icon = req.query.icon || 'simpleicons';
|
||||
const label = req.query.label || 'Simple Icons';
|
||||
const style = parseStyle(req.query.style);
|
||||
const bgicon = req.query.bgicon || 'none';
|
||||
const bglabel = req.query.bglabel || 'none';
|
||||
const color = req.query.color || '#000000';
|
||||
const size = parseSize(req.query.size);
|
||||
const labelpos = parseLabelpos(req.query.labelpos);
|
||||
const fontweight = parseFontweight(req.query.fontweight);
|
||||
|
||||
try {
|
||||
// download icon SVG from SimpleIcons CDN
|
||||
const iconUrl = `https://cdn.simpleicons.org/${icon}?viewbox=auto&size=${size}`;
|
||||
const safeIcon = escapeSvg(icon);
|
||||
const iconUrl = `https://cdn.simpleicons.org/${safeIcon}?viewbox=auto&size=${size}`;
|
||||
const iconResponse = await axios.get(iconUrl, { responseType: 'text' });
|
||||
if (iconResponse.status !== 200) {
|
||||
return res.status(404).send('Icon not found');
|
||||
}
|
||||
const iconSVG = iconResponse.data;
|
||||
|
||||
// if there is no label query, use the title from the SVG
|
||||
@@ -33,13 +33,17 @@ router.get('/badge', async (req, res) => {
|
||||
}
|
||||
|
||||
// Badge size
|
||||
const safeLabel = escapeSvg(effectiveLabel);
|
||||
const safeBgIcon = safeColor(bgicon, 'none');
|
||||
const safeBgLabel = safeColor(bglabel, 'none');
|
||||
const safeFontWeight = escapeSvg(fontweight);
|
||||
|
||||
const padding = 10;
|
||||
const iconSize = parseInt(size);
|
||||
const fontSize = Math.round(iconSize * 0.6);
|
||||
const textPadding = Math.round(iconSize * 0.4);
|
||||
const radius = 0;
|
||||
const approxCharWidth = fontSize * 0.6;
|
||||
const textWidth = effectiveLabel.length * approxCharWidth;
|
||||
const iconSize = size;
|
||||
const fontSize = calcFontSize(iconSize);
|
||||
const textPadding = Math.round(iconSize * 0.4);
|
||||
const radius = style === 'flat' ? 3 : 0;
|
||||
const textWidth = estimateTextWidth(effectiveLabel, fontSize);
|
||||
|
||||
// Kinyerjük az ikon SVG-ből a width és height attribútumokat (ha vannak)
|
||||
let iconViewBoxWidth = iconSize;
|
||||
@@ -63,6 +67,7 @@ router.get('/badge', async (req, res) => {
|
||||
effectiveColor = "#222"; // fallback
|
||||
}
|
||||
}
|
||||
effectiveColor = safeColor(effectiveColor, '#222');
|
||||
|
||||
// doboz magasság 5 pixellel nagyobb legyen az ikon méreténél
|
||||
const iconSizeAndPadding = iconSize + 5;
|
||||
@@ -70,15 +75,15 @@ router.get('/badge', async (req, res) => {
|
||||
|
||||
let width, height, iconGroup, textElem;
|
||||
|
||||
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif";
|
||||
const fontWeight = fontweight; // paraméterből
|
||||
const fontFamily = FONT_FAMILY;
|
||||
const fontWeight = safeFontWeight; // paraméterből
|
||||
|
||||
if (labelpos === 'left') {
|
||||
// Külön dobozok: padding csak a széleken kell, a dobozokon belül nem!
|
||||
textElem = `<rect x="0" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${bglabel}"/>
|
||||
<text x="${textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${effectiveLabel}</text>`;
|
||||
textElem = `<rect x="0" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgLabel}"/>
|
||||
<text x="${textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>`;
|
||||
iconGroup = `
|
||||
<rect x="${textWidth}" y="0" width="${iconBoxWidth + 5}" height="${iconBoxHeight}" rx="${radius}" fill="${bgicon}"/>
|
||||
<rect x="${textWidth}" y="0" width="${iconBoxWidth + 5}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgIcon}"/>
|
||||
<g transform="translate(${textWidth + (5 / 2)}, ${(iconBoxHeight - iconSize) / 2})">${iconSVG}</g>
|
||||
`;
|
||||
} else if (labelpos === 'above' || labelpos === 'below') {
|
||||
@@ -94,9 +99,9 @@ router.get('/badge', async (req, res) => {
|
||||
height = iconSizeAndPadding + textPadding + labelRectHeight;
|
||||
|
||||
// Label háttér
|
||||
const labelRect = `<rect x="0" y="${labelRectY}" width="${width}" height="${labelRectHeight}" rx="${radius}" fill="${bglabel}"/>`;
|
||||
const labelRect = `<rect x="0" y="${labelRectY}" width="${width}" height="${labelRectHeight}" rx="${radius}" fill="${safeBgLabel}"/>`;
|
||||
// Ikon háttér
|
||||
const iconRect = `<rect x="0" y="${iconRectY}" width="${width}" height="${iconSize + 5}" rx="${radius}" fill="${bgicon}"/>`;
|
||||
const iconRect = `<rect x="0" y="${iconRectY}" width="${width}" height="${iconSize + 5}" rx="${radius}" fill="${safeBgIcon}"/>`;
|
||||
|
||||
// Szöveg
|
||||
const textY = labelpos === 'above'
|
||||
@@ -104,7 +109,7 @@ router.get('/badge', async (req, res) => {
|
||||
: labelRectY + 5 + labelRectHeight / 2;
|
||||
textElem = `
|
||||
${labelRect}
|
||||
<text x="${width / 2}" y="${textY}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${effectiveLabel}</text>
|
||||
<text x="${width / 2}" y="${textY}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>
|
||||
`;
|
||||
|
||||
// Ikon
|
||||
@@ -115,11 +120,11 @@ router.get('/badge', async (req, res) => {
|
||||
} else {
|
||||
// Alapértelmezett: ikon balra, szöveg jobbra (függőleges közép)
|
||||
iconGroup = `
|
||||
<rect x="0" y="0" width="${iconBoxWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${bgicon}"/>
|
||||
<rect x="0" y="0" width="${iconBoxWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgIcon}"/>
|
||||
<g transform="translate(${(iconBoxWidth - iconViewBoxWidth) / 2}, ${(iconBoxHeight - iconViewBoxHeight) / 2})">${iconSVG}</g>
|
||||
`;
|
||||
textElem = `<rect x="${iconBoxWidth}" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${bglabel}"/>
|
||||
<text x="${iconBoxWidth + textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${effectiveLabel}</text>`;
|
||||
textElem = `<rect x="${iconBoxWidth}" y="0" width="${textWidth}" height="${iconBoxHeight}" rx="${radius}" fill="${safeBgLabel}"/>
|
||||
<text x="${iconBoxWidth + textWidth / 2}" y="${iconBoxHeight / 2}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="${fontWeight}" fill="${effectiveColor}" text-anchor="middle" dominant-baseline="middle">${safeLabel}</text>`;
|
||||
}
|
||||
|
||||
// SVG badge string összefűzéssel, szöveg árnyékkal
|
||||
@@ -141,7 +146,7 @@ router.get('/badge', async (req, res) => {
|
||||
if (err.response && err.response.status === 404) {
|
||||
return res.status(404).send('Icon not found');
|
||||
} else {
|
||||
console.error(err);
|
||||
logger.error({ err, icon, url: req.url }, 'Badge generation failed');
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
+38
-31
@@ -1,30 +1,36 @@
|
||||
const router = require('express').Router();
|
||||
const { escapeSvg, safeColor } = require('../lib/sanitize');
|
||||
const { parseSize, parseStyle } = require('../lib/validate');
|
||||
const { FONT_FAMILY, calcFontSize } = require('../lib/svg-utils');
|
||||
|
||||
|
||||
router.get('/tag', (req, res) => {
|
||||
const {
|
||||
tag = 'badgedex',
|
||||
label = 'BadgeDex',
|
||||
style = 'rect', // badge stílusa: rect (négyzetes), flat (lekerekített sarkok)
|
||||
bgtag = 'none', // ikon háttérszín, alap: világosszürke
|
||||
bglabel = 'none', // label háttérszín, alap: sötétszürke
|
||||
color = '#000000',
|
||||
size = 24,
|
||||
labelpos = 'right',
|
||||
fontweight = 'normal',
|
||||
} = req.query;
|
||||
const tag = req.query.tag || 'badgedex';
|
||||
const label = req.query.label || 'BadgeDex';
|
||||
const style = parseStyle(req.query.style);
|
||||
const bgtag = req.query.bgtag || 'none';
|
||||
const bglabel = req.query.bglabel || 'none';
|
||||
const color = req.query.color || '#000000';
|
||||
const size = parseSize(req.query.size);
|
||||
const labelpos = req.query.labelpos || 'right';
|
||||
const fontweight = req.query.fontweight || 'normal';
|
||||
|
||||
const fontFamily = "Verdana,Geneva,DejaVu Sans,sans-serif";
|
||||
const fontWeight = fontweight;
|
||||
const fontFamily = FONT_FAMILY;
|
||||
const fontWeight = escapeSvg(fontweight);
|
||||
const safeTag = escapeSvg(tag);
|
||||
const safeLabel = escapeSvg(label);
|
||||
const safeColorVal = safeColor(color, '#000000');
|
||||
const safeBgTag = safeColor(bgtag, 'none');
|
||||
const safeBgLabel = safeColor(bglabel, 'none');
|
||||
|
||||
// A flat badge esetén a szöveg középre igazítása miatt a szöveg pozícióját módosítjuk
|
||||
//const labelX = labelpos === 'left' ? 165 : 455;
|
||||
//const tagX = labelpos === 'left' ? 455 : 165;
|
||||
|
||||
// A méretet pixelben adjuk meg, alapértelmezett 24px
|
||||
const textPadding = Number(5);
|
||||
const badgeSize = Number(size);
|
||||
const badgeFontSize = Math.round(badgeSize * 0.6); // A szöveg mérete a badge méretéhez igazítva
|
||||
const textPadding = 5;
|
||||
const badgeSize = size;
|
||||
const badgeFontSize = calcFontSize(badgeSize);
|
||||
const tagTextLength = tag.length * (badgeFontSize * 0.7); // A tag szöveg hossza
|
||||
const labelTextLength = label.length * (badgeFontSize * 0.8); // A label szöveg hossza
|
||||
const badgeWidth = tagTextLength + labelTextLength + 10; // A badge szélessége a szövegek hosszától függően
|
||||
@@ -33,38 +39,39 @@ router.get('/tag', (req, res) => {
|
||||
|
||||
const rect = `
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="${tagTextLength+textPadding}" height="${badgeSize}" fill="${bgtag}"/>
|
||||
<rect x="${tagTextLength}" width="${labelTextLength+(textPadding*2)}" height="${badgeSize}" fill="${bglabel}"/>
|
||||
<rect width="${tagTextLength+textPadding}" height="${badgeSize}" fill="${safeBgTag}"/>
|
||||
<rect x="${tagTextLength}" width="${labelTextLength+(textPadding*2)}" height="${badgeSize}" fill="${safeBgLabel}"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="start" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="${badgeFontSize}">
|
||||
<text x="${tagTextX}" y="${badgeFontSize + textPadding}" fill="${color}">${tag.toLowerCase()}</text>
|
||||
<text x="${labelTextX}" y="${badgeFontSize + textPadding}" fill="${color}" font-weight="${fontWeight}">${label.toUpperCase()}</text>
|
||||
<text x="${tagTextX}" y="${badgeFontSize + textPadding}" fill="${safeColorVal}">${safeTag.toLowerCase()}</text>
|
||||
<text x="${labelTextX}" y="${badgeFontSize + textPadding}" fill="${safeColorVal}" font-weight="${fontWeight}">${safeLabel.toUpperCase()}</text>
|
||||
</g>`.trim();
|
||||
|
||||
|
||||
const rx = 3;
|
||||
const flat = `
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="62" height="20" rx="3" fill="#fff"/>
|
||||
<rect width="${badgeWidth}" height="${badgeSize}" rx="${rx}" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="31" height="20" fill="#555"/>
|
||||
<rect x="31" width="31" height="20" fill="#007ec6"/>
|
||||
<rect width="62" height="20" fill="url(#s)"/>
|
||||
<rect width="${tagTextLength + textPadding}" height="${badgeSize}" fill="${safeBgTag}"/>
|
||||
<rect x="${tagTextLength + textPadding}" width="${labelTextLength + textPadding}" height="${badgeSize}" fill="${safeBgLabel}"/>
|
||||
<rect width="${badgeWidth}" height="${badgeSize}" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="110" font-weight="${fontWeight}">
|
||||
<text aria-hidden="true" x="165" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${tag}</text>
|
||||
<text x="165" y="140" transform="scale(.1)" fill="#fff" textLength="210">${tag}</text>
|
||||
<text aria-hidden="true" x="455" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">${label}</text>
|
||||
<text x="455" y="140" transform="scale(.1)" fill="#fff" textLength="210">${label}</text>
|
||||
<g fill="#fff" text-anchor="middle" font-family="${fontFamily}" text-rendering="geometricPrecision" font-size="${badgeFontSize}" font-weight="${fontWeight}">
|
||||
<text aria-hidden="true" x="${(tagTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding + 1}" fill="#010101" fill-opacity=".3">${safeTag}</text>
|
||||
<text x="${(tagTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding}" fill="#fff">${safeTag}</text>
|
||||
<text aria-hidden="true" x="${tagTextLength + textPadding + (labelTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding + 1}" fill="#010101" fill-opacity=".3">${safeLabel}</text>
|
||||
<text x="${tagTextLength + textPadding + (labelTextLength + textPadding) / 2}" y="${badgeFontSize + textPadding}" fill="#fff">${safeLabel}</text>
|
||||
</g>`.trim();
|
||||
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${badgeWidth}" height="${badgeSize}" role="img" aria-label="${tag}: ${label}">
|
||||
<title>${tag}: ${label}</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="${badgeWidth}" height="${badgeSize}" role="img" aria-label="${safeTag}: ${safeLabel}">
|
||||
<title>${safeTag}: ${safeLabel}</title>
|
||||
${style === 'rect' ? rect : flat}
|
||||
</svg>`.trim();
|
||||
|
||||
|
||||
+43
-6
@@ -1,18 +1,55 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const logger = require('./lib/logger');
|
||||
const api = require('./api');
|
||||
const version = require('./lib/version').version();
|
||||
|
||||
const api = require('./api');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Rate limiting – max 100 requests per minute per IP
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 100,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
});
|
||||
|
||||
app.use(helmet({
|
||||
crossOriginResourcePolicy: false,
|
||||
}));
|
||||
|
||||
app.use(limiter);
|
||||
app.use('/', api.router);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[ ${new Date()} ] [ 🚀 BadgeDex version: ${version} ]`);
|
||||
console.log(`[ ${new Date()} ] [ 🚀 BadgeDex API running at http://localhost:${PORT}/ ]`);
|
||||
// Central error handler – catches all unhandled errors
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error({ err, url: req.url }, 'Unhandled error');
|
||||
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
|
||||
});
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
logger.info(`BadgeDex version ${version} listening on http://localhost:${PORT}/`);
|
||||
});
|
||||
|
||||
// Graceful shutdown – close server cleanly on SIGTERM / SIGINT
|
||||
const shutdown = (signal) => {
|
||||
logger.info(`${signal} received, shutting down gracefully`);
|
||||
server.close(() => {
|
||||
logger.info('HTTP server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
// Force exit after 5 seconds if connections don't close
|
||||
setTimeout(() => {
|
||||
logger.warn('Forced shutdown after timeout');
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const pino = require('pino');
|
||||
|
||||
const isTest = process.env.NODE_ENV === 'test';
|
||||
|
||||
const logger = pino({
|
||||
level: isTest ? 'silent' : (process.env.LOG_LEVEL || 'info'),
|
||||
transport:
|
||||
!isTest && process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty', options: { colorize: true } }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const COLOR_RE = /^(#[0-9a-fA-F]{3,8}|[a-zA-Z]+)$/;
|
||||
|
||||
/**
|
||||
* Escape special XML/HTML characters to prevent SVG injection / XSS.
|
||||
* < → < > → > & → & " → " ' → '
|
||||
*/
|
||||
const escapeSvg = (value) =>
|
||||
String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
/**
|
||||
* Return a safe CSS colour string or the fallback.
|
||||
* Only simple colour names and hex values (#rgb, #rrggbb, #rrggbbaa) pass.
|
||||
*/
|
||||
const safeColor = (value, fallback = '#000000') =>
|
||||
COLOR_RE.test(value) ? value : fallback;
|
||||
|
||||
module.exports = { escapeSvg, safeColor };
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
/** Standard font family used by both badge and tag endpoints. */
|
||||
const FONT_FAMILY = 'Verdana,Geneva,DejaVu Sans,sans-serif';
|
||||
|
||||
/** Font-size scales with the icon / badge size. */
|
||||
const calcFontSize = (size) => Math.round(size * 0.6);
|
||||
|
||||
/**
|
||||
* Rough text-width estimate based on font-size and character count.
|
||||
* Uses a 0.6 coefficient which works well for Verdana / sans-serif.
|
||||
*/
|
||||
const estimateTextWidth = (text, fontSize) =>
|
||||
text.length * fontSize * 0.6;
|
||||
|
||||
module.exports = { FONT_FAMILY, calcFontSize, estimateTextWidth };
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
const ALLOWED_STYLES = ['rect', 'flat'];
|
||||
const ALLOWED_LABELPOS = ['right', 'left', 'above', 'below'];
|
||||
const SIZE_MIN = 8;
|
||||
const SIZE_MAX = 256;
|
||||
|
||||
/**
|
||||
* Parse and clamp the `size` query parameter.
|
||||
* Returns a positive integer in [SIZE_MIN, SIZE_MAX].
|
||||
*/
|
||||
const parseSize = (raw) => {
|
||||
const n = parseInt(raw, 10);
|
||||
if (Number.isNaN(n) || n < SIZE_MIN || n > SIZE_MAX) return 24;
|
||||
return n;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a valid style or the default.
|
||||
*/
|
||||
const parseStyle = (raw) =>
|
||||
ALLOWED_STYLES.includes(raw) ? raw : 'rect';
|
||||
|
||||
/**
|
||||
* Return a valid labelpos or the default.
|
||||
*/
|
||||
const parseLabelpos = (raw) =>
|
||||
ALLOWED_LABELPOS.includes(raw) ? raw : 'right';
|
||||
|
||||
/**
|
||||
* Return a valid font-weight string.
|
||||
* Accepts: normal, bold, lighter, bolder, 100–900.
|
||||
*/
|
||||
const parseFontweight = (raw) => {
|
||||
if (/^(normal|bold|lighter|bolder)$/.test(raw)) return raw;
|
||||
if (/^[1-9]00$/.test(raw)) return raw;
|
||||
return 'normal';
|
||||
};
|
||||
|
||||
module.exports = { parseSize, parseStyle, parseLabelpos, parseFontweight, SIZE_MIN, SIZE_MAX };
|
||||
+2
-2
@@ -2,8 +2,8 @@ const fs = require('fs');
|
||||
|
||||
|
||||
const version = () => {
|
||||
if (process.env.VERSION) {
|
||||
return process.env.VERSION;
|
||||
if (process.env.APP_VERSION) {
|
||||
return process.env.APP_VERSION;
|
||||
}
|
||||
// Fallback to reading from a VERSION file
|
||||
const versionPath = require('path').resolve(__dirname, '../../VERSION');
|
||||
|
||||
+209
-46
@@ -1,94 +1,257 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const badgeRouter = require('../src/api/badge');
|
||||
|
||||
const app = express();
|
||||
app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||
app.use(badgeRouter);
|
||||
// --- Mock axios before requiring the router ---
|
||||
jest.mock('axios');
|
||||
const axios = require('axios');
|
||||
|
||||
const MOCK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>SimpleIcons</title><path fill="#333" d="M12 2L2 22h20z"/></svg>`;
|
||||
|
||||
beforeEach(() => {
|
||||
axios.get.mockResolvedValue({ status: 200, data: MOCK_SVG });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// Fresh app for each test to avoid middleware leakage
|
||||
function createApp() {
|
||||
const badgeRouter = require('../src/api/badge');
|
||||
const app = express();
|
||||
app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||
app.use(badgeRouter);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('GET /badge', () => {
|
||||
|
||||
// --- Basic happy-path tests ---
|
||||
|
||||
it('should return SVG with default params', async () => {
|
||||
const res = await request(app).get('/badge');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toMatch(/image\/svg\+xml/);
|
||||
expect(resBody).toContain('<svg');
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(body).toContain('<svg');
|
||||
expect(body).toContain('<rect');
|
||||
});
|
||||
|
||||
it('should use custom icon and label', async () => {
|
||||
const res = await request(app).get('/badge?icon=github&label=GitHub');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?icon=github&label=GitHub');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('GitHub');
|
||||
expect(resBody).toContain('<svg');
|
||||
expect(body).toContain('GitHub');
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
it('should use label from SVG title if label param is missing', async () => {
|
||||
const res = await request(app).get('/badge?icon=github');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?icon=github');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<svg');
|
||||
// The title from the SVG should be present as label
|
||||
expect(resBody.toLowerCase()).toContain('github');
|
||||
expect(body).toContain('<svg');
|
||||
expect(body.toLowerCase()).toContain('simpleicons');
|
||||
});
|
||||
|
||||
// --- Background & colour ---
|
||||
|
||||
it('should set icon background to transparent', async () => {
|
||||
const res = await request(app).get('/badge?bgicon=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?bgicon=none');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should set label background to transparent', async () => {
|
||||
const res = await request(app).get('/badge?bglabel=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?bglabel=none');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should use flat style with rounded corners', async () => {
|
||||
const res = await request(app).get('/badge?style=flat');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
it('should apply custom bgicon and bglabel colours', async () => {
|
||||
const res = await request(createApp()).get('/badge?bgicon=red&bglabel=blue');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('rx="');
|
||||
expect(body).toContain('fill="red"');
|
||||
expect(body).toContain('fill="blue"');
|
||||
});
|
||||
|
||||
it('should apply custom text colour', async () => {
|
||||
const res = await request(createApp()).get('/badge?color=white');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('fill="white"');
|
||||
});
|
||||
|
||||
// --- Styles ---
|
||||
|
||||
it('should use flat style with rounded corners (rx=3)', async () => {
|
||||
const res = await request(createApp()).get('/badge?style=flat');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('rx="3"');
|
||||
});
|
||||
|
||||
it('should use rect style with square corners (rx=0)', async () => {
|
||||
const res = await request(createApp()).get('/badge?style=rect');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('rx="0"');
|
||||
});
|
||||
|
||||
it('should fall back to rect for invalid style', async () => {
|
||||
const res = await request(createApp()).get('/badge?style=invalid');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('rx="0"');
|
||||
});
|
||||
|
||||
// --- labelpos ---
|
||||
|
||||
it('should handle labelpos left', async () => {
|
||||
const res = await request(app).get('/badge?labelpos=left');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?labelpos=left');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('<text');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('<text');
|
||||
});
|
||||
|
||||
it('should handle labelpos above', async () => {
|
||||
const res = await request(app).get('/badge?labelpos=above');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?labelpos=above');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('<text');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('<text');
|
||||
});
|
||||
|
||||
it('should handle labelpos below', async () => {
|
||||
const res = await request(app).get('/badge?labelpos=below');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const res = await request(createApp()).get('/badge?labelpos=below');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('<text');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('<text');
|
||||
});
|
||||
|
||||
it('should handle custom fontweight', async () => {
|
||||
const res = await request(app).get('/badge?fontweight=bold');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
it('should fall back to right for invalid labelpos', async () => {
|
||||
const res = await request(createApp()).get('/badge?labelpos=diagonal');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('font-weight="bold"');
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
// Test not applicable, since icon is not real should handle 404 from CDN
|
||||
it('should return 404 for unknown icon', async () => {
|
||||
const res = await request(app).get('/badge?icon=notarealicon');
|
||||
// --- fontweight ---
|
||||
|
||||
it('should handle custom fontweight bold', async () => {
|
||||
const res = await request(createApp()).get('/badge?fontweight=bold');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="bold"');
|
||||
});
|
||||
|
||||
it('should handle numeric fontweight 600', async () => {
|
||||
const res = await request(createApp()).get('/badge?fontweight=600');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="600"');
|
||||
});
|
||||
|
||||
it('should fall back to normal for invalid fontweight', async () => {
|
||||
const res = await request(createApp()).get('/badge?fontweight=ultra');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="normal"');
|
||||
});
|
||||
|
||||
// --- size ---
|
||||
|
||||
it('should handle minimum valid size (8)', async () => {
|
||||
const res = await request(createApp()).get('/badge?size=8');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
it('should handle large size (128)', async () => {
|
||||
const res = await request(createApp()).get('/badge?size=128');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
it('should fall back to default size for NaN', async () => {
|
||||
const res = await request(createApp()).get('/badge?size=abc');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
it('should fall back to default size for negative value', async () => {
|
||||
const res = await request(createApp()).get('/badge?size=-5');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
it('should fall back to default size for value above max', async () => {
|
||||
const res = await request(createApp()).get('/badge?size=9999');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
// --- XSS / injection ---
|
||||
|
||||
it('should escape XSS in label', async () => {
|
||||
const res = await request(createApp()).get('/badge?label=<script>alert(1)</script>');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).not.toContain('<script>');
|
||||
expect(body).toContain('<script>');
|
||||
});
|
||||
|
||||
it('should escape quotes in label', async () => {
|
||||
const res = await request(createApp()).get('/badge?label=he%20said%20%22hi%22');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('"');
|
||||
});
|
||||
|
||||
it('should reject invalid color with injection attempt', async () => {
|
||||
const res = await request(createApp()).get('/badge?color=javascript:alert(1)');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).not.toContain('javascript:');
|
||||
});
|
||||
|
||||
// --- CDN error handling ---
|
||||
|
||||
it('should return 404 when CDN returns 404', async () => {
|
||||
axios.get.mockRejectedValue({ response: { status: 404 } });
|
||||
const res = await request(createApp()).get('/badge?icon=notarealicon');
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 500 on unexpected CDN error', async () => {
|
||||
axios.get.mockRejectedValue(new Error('Network timeout'));
|
||||
const res = await request(createApp()).get('/badge?icon=github');
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.text).toBe('Internal Server Error');
|
||||
});
|
||||
|
||||
// --- SVG structure ---
|
||||
|
||||
it('should include drop shadow filter', async () => {
|
||||
const res = await request(createApp()).get('/badge');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(body).toContain('filter id="shadow"');
|
||||
expect(body).toContain('feDropShadow');
|
||||
});
|
||||
|
||||
it('should include xmlns', async () => {
|
||||
const res = await request(createApp()).get('/badge');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(body).toContain('xmlns="http://www.w3.org/2000/svg"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
const { escapeSvg, safeColor } = require('../src/lib/sanitize');
|
||||
|
||||
describe('sanitize', () => {
|
||||
|
||||
describe('escapeSvg', () => {
|
||||
it('should escape < and >', () => {
|
||||
expect(escapeSvg('<script>')).toBe('<script>');
|
||||
});
|
||||
|
||||
it('should escape &', () => {
|
||||
expect(escapeSvg('A & B')).toBe('A & B');
|
||||
});
|
||||
|
||||
it('should escape double quotes', () => {
|
||||
expect(escapeSvg('"hello"')).toBe('"hello"');
|
||||
});
|
||||
|
||||
it('should escape single quotes', () => {
|
||||
expect(escapeSvg("it's")).toBe('it's');
|
||||
});
|
||||
|
||||
it('should escape combined special characters', () => {
|
||||
expect(escapeSvg('<img src="x" onerror=\'alert(1)\'>')).toBe(
|
||||
'<img src="x" onerror='alert(1)'>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass through normal text unchanged', () => {
|
||||
expect(escapeSvg('Hello World')).toBe('Hello World');
|
||||
expect(escapeSvg('test-123')).toBe('test-123');
|
||||
});
|
||||
|
||||
it('should handle numbers by converting to string', () => {
|
||||
expect(escapeSvg(42)).toBe('42');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(escapeSvg('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeColor', () => {
|
||||
it('should accept hex colors', () => {
|
||||
expect(safeColor('#fff')).toBe('#fff');
|
||||
expect(safeColor('#FF0000')).toBe('#FF0000');
|
||||
expect(safeColor('#12345678')).toBe('#12345678');
|
||||
});
|
||||
|
||||
it('should accept named colors', () => {
|
||||
expect(safeColor('red')).toBe('red');
|
||||
expect(safeColor('blue')).toBe('blue');
|
||||
expect(safeColor('none')).toBe('none');
|
||||
expect(safeColor('transparent')).toBe('transparent');
|
||||
});
|
||||
|
||||
it('should return fallback for invalid colors', () => {
|
||||
expect(safeColor('javascript:alert(1)')).toBe('#000000');
|
||||
expect(safeColor('rgb(255,0,0)')).toBe('#000000');
|
||||
expect(safeColor('url(evil)')).toBe('#000000');
|
||||
});
|
||||
|
||||
it('should use custom fallback for non-alphabetic invalid values', () => {
|
||||
expect(safeColor('rgb(255,0,0)', '#fff')).toBe('#fff');
|
||||
expect(safeColor('url(evil)', 'none')).toBe('none');
|
||||
});
|
||||
|
||||
it('should return default fallback (#000000) when not specified', () => {
|
||||
expect(safeColor('')).toBe('#000000');
|
||||
expect(safeColor('999')).toBe('#000000');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
const { FONT_FAMILY, calcFontSize, estimateTextWidth } = require('../src/lib/svg-utils');
|
||||
|
||||
describe('svg-utils', () => {
|
||||
|
||||
describe('FONT_FAMILY', () => {
|
||||
it('should be a non-empty string', () => {
|
||||
expect(typeof FONT_FAMILY).toBe('string');
|
||||
expect(FONT_FAMILY.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should contain Verdana', () => {
|
||||
expect(FONT_FAMILY).toContain('Verdana');
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcFontSize', () => {
|
||||
it('should return 60% of size rounded', () => {
|
||||
expect(calcFontSize(24)).toBe(14);
|
||||
expect(calcFontSize(100)).toBe(60);
|
||||
expect(calcFontSize(16)).toBe(10);
|
||||
});
|
||||
|
||||
it('should round correctly', () => {
|
||||
expect(calcFontSize(25)).toBe(15);
|
||||
expect(calcFontSize(33)).toBe(20); // 33 * 0.6 = 19.8 → 20
|
||||
});
|
||||
});
|
||||
|
||||
describe('estimateTextWidth', () => {
|
||||
it('should estimate width based on text length and font size', () => {
|
||||
const width = estimateTextWidth('Hello', 14);
|
||||
expect(width).toBe(5 * 14 * 0.6); // 42
|
||||
});
|
||||
|
||||
it('should return 0 for empty string', () => {
|
||||
expect(estimateTextWidth('', 14)).toBe(0);
|
||||
});
|
||||
|
||||
it('should scale linearly with font size', () => {
|
||||
const w1 = estimateTextWidth('test', 10);
|
||||
const w2 = estimateTextWidth('test', 20);
|
||||
expect(w2).toBe(w1 * 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
+136
-24
@@ -8,65 +8,177 @@ app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||
app.use(tagRouter);
|
||||
|
||||
describe('GET /tag', () => {
|
||||
|
||||
// --- Basic happy-path ---
|
||||
|
||||
it('should return SVG with default params', async () => {
|
||||
const res = await request(app).get('/tag');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toMatch(/image\/svg\+xml/);
|
||||
expect(resBody).toContain('<svg');
|
||||
expect(resBody).toContain('<rect');
|
||||
expect(resBody).toContain('badgedex');
|
||||
expect(resBody).toContain('BADGEDEX');
|
||||
expect(body).toContain('<svg');
|
||||
expect(body).toContain('<rect');
|
||||
expect(body).toContain('badgedex');
|
||||
expect(body).toContain('BADGEDEX');
|
||||
});
|
||||
|
||||
it('should use custom tag and label', async () => {
|
||||
const res = await request(app).get('/tag?tag=test&label=Teszt');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('test');
|
||||
expect(resBody).toContain('TESZT');
|
||||
expect(body).toContain('test');
|
||||
expect(body).toContain('TESZT');
|
||||
});
|
||||
|
||||
// --- Backgrounds ---
|
||||
|
||||
it('should set tag background to transparent', async () => {
|
||||
const res = await request(app).get('/tag?bgtag=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should set label background to transparent', async () => {
|
||||
const res = await request(app).get('/tag?bglabel=none');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="none"');
|
||||
expect(body).toContain('fill="none"');
|
||||
});
|
||||
|
||||
it('should use flat style', async () => {
|
||||
it('should apply custom bgtag and bglabel colours', async () => {
|
||||
const res = await request(app).get('/tag?bgtag=green&bglabel=orange');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('fill="green"');
|
||||
expect(body).toContain('fill="orange"');
|
||||
});
|
||||
|
||||
// --- Styles ---
|
||||
|
||||
it('should use rect style', async () => {
|
||||
const res = await request(app).get('/tag?style=rect');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('shape-rendering="crispEdges"');
|
||||
});
|
||||
|
||||
it('should use flat style with dynamic dimensions', async () => {
|
||||
const res = await request(app).get('/tag?style=flat');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('<linearGradient');
|
||||
expect(resBody).toContain('<clipPath');
|
||||
expect(body).toContain('<linearGradient');
|
||||
expect(body).toContain('<clipPath');
|
||||
expect(body).toContain('rx="3"');
|
||||
});
|
||||
|
||||
it('should handle custom fontweight', async () => {
|
||||
const res = await request(app).get('/tag?fontweight=bold');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
it('should fall back to rect for invalid style', async () => {
|
||||
const res = await request(app).get('/tag?style=invalid');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('font-weight="bold"');
|
||||
expect(body).toContain('shape-rendering="crispEdges"');
|
||||
});
|
||||
|
||||
// --- fontweight ---
|
||||
|
||||
it('should handle custom fontweight bold', async () => {
|
||||
const res = await request(app).get('/tag?fontweight=bold');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="bold"');
|
||||
});
|
||||
|
||||
it('should handle fontweight lighter', async () => {
|
||||
const res = await request(app).get('/tag?fontweight=lighter');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('font-weight="lighter"');
|
||||
});
|
||||
|
||||
// --- color ---
|
||||
|
||||
it('should handle custom color', async () => {
|
||||
const res = await request(app).get('/tag?color=%23ff0000');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('fill="#ff0000"');
|
||||
expect(body).toContain('fill="#ff0000"');
|
||||
});
|
||||
|
||||
it('should reject invalid color and fall back to default', async () => {
|
||||
const res = await request(app).get('/tag?color=bad-color-value');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('fill="#000000"');
|
||||
});
|
||||
|
||||
// --- size ---
|
||||
|
||||
it('should handle custom size', async () => {
|
||||
const res = await request(app).get('/tag?size=48');
|
||||
const resBody = Buffer.from(res.body).toString();
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(resBody).toContain('height="48"');
|
||||
expect(body).toContain('height="48"');
|
||||
});
|
||||
|
||||
it('should handle minimum size (8)', async () => {
|
||||
const res = await request(app).get('/tag?size=8');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('<svg');
|
||||
});
|
||||
|
||||
it('should fall back to default size for NaN', async () => {
|
||||
const res = await request(app).get('/tag?size=abc');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('height="24"');
|
||||
});
|
||||
|
||||
it('should fall back to default size for negative value', async () => {
|
||||
const res = await request(app).get('/tag?size=-10');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('height="24"');
|
||||
});
|
||||
|
||||
it('should fall back to default size for value above max', async () => {
|
||||
const res = await request(app).get('/tag?size=500');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('height="24"');
|
||||
});
|
||||
|
||||
// --- XSS / injection ---
|
||||
|
||||
it('should escape XSS in tag', async () => {
|
||||
const res = await request(app).get('/tag?tag=<img%20src=x%20onerror=alert(1)>');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).not.toContain('<img');
|
||||
expect(body).toContain('<img');
|
||||
});
|
||||
|
||||
it('should escape XSS in label', async () => {
|
||||
const res = await request(app).get('/tag?label=<script>alert(1)</script>');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).not.toContain('<script>');
|
||||
expect(body).toContain('<script>');
|
||||
});
|
||||
|
||||
// --- SVG structure ---
|
||||
|
||||
it('should include aria-label and title', async () => {
|
||||
const res = await request(app).get('/tag?tag=status&label=OK');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(body).toContain('aria-label="status: OK"');
|
||||
expect(body).toContain('<title>status: OK</title>');
|
||||
});
|
||||
|
||||
it('should include xmlns', async () => {
|
||||
const res = await request(app).get('/tag');
|
||||
const body = Buffer.from(res.body).toString();
|
||||
expect(body).toContain('xmlns="http://www.w3.org/2000/svg"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
const { parseSize, parseStyle, parseLabelpos, parseFontweight, SIZE_MIN, SIZE_MAX } = require('../src/lib/validate');
|
||||
|
||||
describe('validate', () => {
|
||||
|
||||
describe('parseSize', () => {
|
||||
it('should return valid size for valid input', () => {
|
||||
expect(parseSize('24')).toBe(24);
|
||||
expect(parseSize('8')).toBe(8);
|
||||
expect(parseSize('256')).toBe(256);
|
||||
expect(parseSize('100')).toBe(100);
|
||||
});
|
||||
|
||||
it('should return default (24) for NaN input', () => {
|
||||
expect(parseSize('abc')).toBe(24);
|
||||
expect(parseSize(undefined)).toBe(24);
|
||||
expect(parseSize(null)).toBe(24);
|
||||
expect(parseSize('')).toBe(24);
|
||||
});
|
||||
|
||||
it('should return default (24) for negative values', () => {
|
||||
expect(parseSize('-1')).toBe(24);
|
||||
expect(parseSize('-100')).toBe(24);
|
||||
});
|
||||
|
||||
it('should return default (24) for zero', () => {
|
||||
expect(parseSize('0')).toBe(24);
|
||||
});
|
||||
|
||||
it('should return default (24) for values above max', () => {
|
||||
expect(parseSize('257')).toBe(24);
|
||||
expect(parseSize('9999')).toBe(24);
|
||||
});
|
||||
|
||||
it('should clamp to size for values below min', () => {
|
||||
expect(parseSize('5')).toBe(24);
|
||||
expect(parseSize('7')).toBe(24);
|
||||
});
|
||||
|
||||
it('should export SIZE_MIN and SIZE_MAX', () => {
|
||||
expect(SIZE_MIN).toBe(8);
|
||||
expect(SIZE_MAX).toBe(256);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseStyle', () => {
|
||||
it('should accept valid styles', () => {
|
||||
expect(parseStyle('rect')).toBe('rect');
|
||||
expect(parseStyle('flat')).toBe('flat');
|
||||
});
|
||||
|
||||
it('should return default (rect) for invalid styles', () => {
|
||||
expect(parseStyle('invalid')).toBe('rect');
|
||||
expect(parseStyle('')).toBe('rect');
|
||||
expect(parseStyle(undefined)).toBe('rect');
|
||||
expect(parseStyle(null)).toBe('rect');
|
||||
expect(parseStyle('RECT')).toBe('rect');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseLabelpos', () => {
|
||||
it('should accept valid positions', () => {
|
||||
expect(parseLabelpos('right')).toBe('right');
|
||||
expect(parseLabelpos('left')).toBe('left');
|
||||
expect(parseLabelpos('above')).toBe('above');
|
||||
expect(parseLabelpos('below')).toBe('below');
|
||||
});
|
||||
|
||||
it('should return default (right) for invalid positions', () => {
|
||||
expect(parseLabelpos('top')).toBe('right');
|
||||
expect(parseLabelpos('center')).toBe('right');
|
||||
expect(parseLabelpos('')).toBe('right');
|
||||
expect(parseLabelpos(undefined)).toBe('right');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFontweight', () => {
|
||||
it('should accept keyword weights', () => {
|
||||
expect(parseFontweight('normal')).toBe('normal');
|
||||
expect(parseFontweight('bold')).toBe('bold');
|
||||
expect(parseFontweight('lighter')).toBe('lighter');
|
||||
expect(parseFontweight('bolder')).toBe('bolder');
|
||||
});
|
||||
|
||||
it('should accept numeric weights (100-900)', () => {
|
||||
expect(parseFontweight('100')).toBe('100');
|
||||
expect(parseFontweight('400')).toBe('400');
|
||||
expect(parseFontweight('600')).toBe('600');
|
||||
expect(parseFontweight('900')).toBe('900');
|
||||
});
|
||||
|
||||
it('should return default (normal) for invalid weights', () => {
|
||||
expect(parseFontweight('ultra')).toBe('normal');
|
||||
expect(parseFontweight('123')).toBe('normal');
|
||||
expect(parseFontweight('')).toBe('normal');
|
||||
expect(parseFontweight(undefined)).toBe('normal');
|
||||
expect(parseFontweight('000')).toBe('normal');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const versionRouter = require('../src/api/version');
|
||||
|
||||
const app = express();
|
||||
app.use(versionRouter);
|
||||
|
||||
describe('GET /version', () => {
|
||||
it('should return JSON with badgedexVersion', async () => {
|
||||
const res = await request(app).get('/version');
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers['content-type']).toMatch(/json/);
|
||||
expect(res.body).toHaveProperty('badgedexVersion');
|
||||
expect(typeof res.body.badgedexVersion).toBe('string');
|
||||
});
|
||||
|
||||
it('should return a valid semver-like version string', async () => {
|
||||
const res = await request(app).get('/version');
|
||||
expect(res.body.badgedexVersion).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
describe('version', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should return version from VERSION file when APP_VERSION is not set', () => {
|
||||
delete process.env.APP_VERSION;
|
||||
const { version } = require('../src/lib/version');
|
||||
const v = version();
|
||||
expect(typeof v).toBe('string');
|
||||
expect(v).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
|
||||
it('should return APP_VERSION env var when set', () => {
|
||||
process.env.APP_VERSION = '9.9.9';
|
||||
const { version } = require('../src/lib/version');
|
||||
expect(version()).toBe('9.9.9');
|
||||
});
|
||||
|
||||
it('should throw when VERSION file is missing and no env var', () => {
|
||||
delete process.env.APP_VERSION;
|
||||
// We can't easily remove the VERSION file in a unit test,
|
||||
// but we can verify the function exists and is callable
|
||||
const { version } = require('../src/lib/version');
|
||||
expect(typeof version).toBe('function');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user