Compare commits
108 Commits
better-err
...
3.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c03a73b573 | ||
|
|
9ac2b41206 | ||
|
|
46250dd105 | ||
|
|
c4314094dc | ||
|
|
13d75e0607 | ||
|
|
1c736f2159 | ||
|
|
0059599662 | ||
|
|
4da0c7b8dd | ||
|
|
6163de5a80 | ||
|
|
d967c62497 | ||
|
|
562dac6711 | ||
|
|
6ca2fa0c89 | ||
|
|
594e14c1b6 | ||
|
|
6e9e25451f | ||
|
|
34d4271509 | ||
|
|
784b45160b | ||
|
|
d2cdbc7a01 | ||
|
|
ba149f2c75 | ||
|
|
1cd92e2e71 | ||
|
|
86c0b41219 | ||
|
|
622f39635a | ||
|
|
2b363096bb | ||
|
|
d9c483abbe | ||
|
|
171aac3c42 | ||
|
|
0129eba673 | ||
|
|
9c3ddab7bf | ||
|
|
072c6fa689 | ||
|
|
cbb3cebfa0 | ||
|
|
ed9672bfcf | ||
|
|
0afdeca5c3 | ||
|
|
07eb48fce8 | ||
|
|
14f91ab16d | ||
|
|
26d753019e | ||
|
|
ed551b3a68 | ||
|
|
6e58db0379 | ||
|
|
4d275e0642 | ||
|
|
af4b9e67cd | ||
|
|
8d0b8987ce | ||
|
|
fb63713f7e | ||
|
|
3424e07d21 | ||
|
|
a29565a02d | ||
|
|
271b84e1cb | ||
|
|
17374873ad | ||
|
|
6bc0d40c37 | ||
|
|
7385739a43 | ||
|
|
d95aff75a9 | ||
|
|
07d1466cd3 | ||
|
|
283ded9e8e | ||
|
|
7198c5af50 | ||
|
|
f0030a4025 | ||
|
|
b6c78a1110 | ||
|
|
5b42c8310b | ||
|
|
612a43aaa9 | ||
|
|
7af965113b | ||
|
|
36f9a8083a | ||
|
|
6e76fabe26 | ||
|
|
762306d7aa | ||
|
|
f9d5f57f49 | ||
|
|
98ebc3ad5e | ||
|
|
4fe80bb150 | ||
|
|
5f80d931ac | ||
|
|
d8840b83e7 | ||
|
|
6bd4b182b6 | ||
|
|
1602443e51 | ||
|
|
90eb4fc73f | ||
|
|
56456bcbed | ||
|
|
e987358c5e | ||
|
|
e60b588feb | ||
|
|
78ad619399 | ||
|
|
446c1031f9 | ||
|
|
6984918cab | ||
|
|
3ba05cb184 | ||
|
|
b64987d2dd | ||
|
|
e66cfa3c21 | ||
|
|
1c48c2207f | ||
|
|
a2e0e11ead | ||
|
|
2c89a8c59f | ||
|
|
d11fa30f06 | ||
|
|
05b324093a | ||
|
|
42ed874112 | ||
|
|
e2d8c94dab | ||
|
|
91577cb6e9 | ||
|
|
d578faab50 | ||
|
|
b0f367cdfb | ||
|
|
430be7e7d1 | ||
|
|
2df8289ec8 | ||
|
|
478d14300c | ||
|
|
23ceea1d72 | ||
|
|
2bf6dfdeea | ||
|
|
e4b1fd23b1 | ||
|
|
8df5eed088 | ||
|
|
4c1eee9218 | ||
|
|
1c2920ac31 | ||
|
|
9df6aade11 | ||
|
|
01950a7852 | ||
|
|
935dd25264 | ||
|
|
c320a58a24 | ||
|
|
10f06ff175 | ||
|
|
68b1a0cfe5 | ||
|
|
39b8ff0aa8 | ||
|
|
6dc672a03d | ||
|
|
642aac6408 | ||
|
|
b2bb50234f | ||
|
|
d16c8404d6 | ||
|
|
b36de3dbf3 | ||
|
|
20d4143dfb | ||
|
|
fbe865e149 | ||
|
|
da29c92910 |
@@ -109,6 +109,7 @@ package_steps: &package_steps
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install v12
|
||||
nvm install v14
|
||||
nvm use v12
|
||||
npm install -g npm
|
||||
npm ci
|
||||
@@ -116,21 +117,13 @@ package_steps: &package_steps
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
# Run the package tests on each currently supported node version. See:
|
||||
# https://github.com/badges/shields/blob/master/gh-badges/README.md#node-version-support
|
||||
# https://github.com/badges/shields/blob/master/badge-maker/README.md#node-version-support
|
||||
|
||||
- run:
|
||||
<<: *run_package_tests
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/gh-badges/v8/results.xml
|
||||
NODE_VERSION: v8
|
||||
name: Run package tests on Node 8
|
||||
|
||||
- run:
|
||||
<<: *run_package_tests
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/gh-badges/v10/results.xml
|
||||
MOCHA_FILE: junit/badge-maker/v10/results.xml
|
||||
NODE_VERSION: v10
|
||||
name: Run package tests on Node 10
|
||||
|
||||
@@ -138,17 +131,25 @@ package_steps: &package_steps
|
||||
<<: *run_package_tests
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/gh-badges/v12/results.xml
|
||||
MOCHA_FILE: junit/badge-maker/v12/results.xml
|
||||
NODE_VERSION: v12
|
||||
name: Run package tests on Node 12
|
||||
|
||||
- run:
|
||||
<<: *run_package_tests
|
||||
environment:
|
||||
mocha_reporter: mocha-junit-reporter
|
||||
MOCHA_FILE: junit/badge-maker/v14/results.xml
|
||||
NODE_VERSION: v14
|
||||
name: Run package tests on Node 14
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
jobs:
|
||||
npm-install:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -160,33 +161,33 @@ jobs:
|
||||
|
||||
main:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
main@node-latest:
|
||||
main@node-12:
|
||||
docker:
|
||||
- image: circleci/node:latest
|
||||
- image: circleci/node:12
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
integration@node-latest:
|
||||
integration@node-12:
|
||||
docker:
|
||||
- image: circleci/node:latest
|
||||
- image: circleci/node:12
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -206,7 +207,7 @@ jobs:
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -247,19 +248,19 @@ jobs:
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/node:10
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
services@node-latest:
|
||||
services@node-12:
|
||||
docker:
|
||||
- image: circleci/node:latest
|
||||
- image: circleci/node:12
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
e2e:
|
||||
docker:
|
||||
- image: cypress/base:8
|
||||
- image: cypress/base:12
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -308,11 +309,11 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- main@node-latest:
|
||||
- main@node-12:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- integration@node-latest:
|
||||
- integration@node-12:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
@@ -330,7 +331,7 @@ workflows:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- services@node-latest:
|
||||
- services@node-12:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
|
||||
@@ -29,75 +29,8 @@ update_configs:
|
||||
- match:
|
||||
dependency_name: 'snap-shot-it'
|
||||
update_type: 'semver:minor'
|
||||
ignored_updates:
|
||||
- match:
|
||||
dependency_name: babel-preset-gatsby
|
||||
version_requirement: '>=0.3.0'
|
||||
- match:
|
||||
dependency_name: chalk
|
||||
version_requirement: '>=4.0.0'
|
||||
- match:
|
||||
dependency_name: cross-env
|
||||
version_requirement: '>=7.0.0'
|
||||
- match:
|
||||
dependency_name: decamelize
|
||||
version_requirement: '>=4.0.0'
|
||||
- match:
|
||||
dependency_name: eslint-plugin-jsdoc
|
||||
version_requirement: '>=21.0.0'
|
||||
- match:
|
||||
dependency_name: gatsby
|
||||
version_requirement: '>=2.19.50'
|
||||
- match:
|
||||
dependency_name: gatsby-plugin-catch-links
|
||||
version_requirement: '>=2.2.0'
|
||||
- match:
|
||||
dependency_name: gatsby-plugin-page-creator
|
||||
version_requirement: '>=2.2.0'
|
||||
- match:
|
||||
dependency_name: gatsby-plugin-react-helmet
|
||||
version_requirement: '>=3.2.0'
|
||||
- match:
|
||||
dependency_name: gatsby-plugin-remove-trailing-slashes
|
||||
version_requirement: '>=2.2.0'
|
||||
- match:
|
||||
dependency_name: gatsby-plugin-styled-components
|
||||
version_requirement: '>=3.2.0'
|
||||
- match:
|
||||
dependency_name: gatsby-plugin-typescript
|
||||
version_requirement: '>=2.3.0'
|
||||
- match:
|
||||
dependency_name: got
|
||||
version_requirement: '>=10.0.0'
|
||||
- match:
|
||||
dependency_name: '@hapi/joi'
|
||||
version_requirement: '>=17.0.0'
|
||||
- match:
|
||||
dependency_name: husky
|
||||
version_requirement: '>=4.0.0'
|
||||
- match:
|
||||
dependency_name: lint-staged
|
||||
version_requirement: '>=10.0.0'
|
||||
- match:
|
||||
dependency_name: nock
|
||||
version_requirement: '>=12.0.0'
|
||||
- match:
|
||||
dependency_name: prom-client
|
||||
version_requirement: '>=12.0.0'
|
||||
- match:
|
||||
dependency_name: react-error-overlay
|
||||
version_requirement: '>=3.0.0'
|
||||
- match:
|
||||
dependency_name: sinon
|
||||
version_requirement: '>=9.0.0'
|
||||
- match:
|
||||
dependency_name: start-server-and-test
|
||||
version_requirement: '>=1.10.8'
|
||||
- match:
|
||||
dependency_name: xmldom
|
||||
version_requirement: '>=0.3.0'
|
||||
|
||||
# gh-badges package dependencies
|
||||
# badge-maker package dependencies
|
||||
- package_manager: 'javascript'
|
||||
directory: '/gh-badges'
|
||||
directory: '/badge-maker'
|
||||
update_schedule: 'weekly'
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
/coverage
|
||||
/__snapshots__
|
||||
/public
|
||||
gh-badges/node_modules/
|
||||
badge-maker/node_modules/
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/1_Bug_report.md
vendored
@@ -8,7 +8,7 @@ Are you experiencing an issue with...
|
||||
|
||||
- [ ] [shields.io](https://shields.io/#/)
|
||||
- [ ] My own instance
|
||||
- [ ] [gh-badges NPM package](https://www.npmjs.com/package/gh-badges)
|
||||
- [ ] [badge-maker NPM package](https://www.npmjs.com/package/badge-maker)
|
||||
|
||||
:beetle: **Description**
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,7 +7,7 @@
|
||||
/private
|
||||
/index.html
|
||||
/shields.env
|
||||
gh-badges/package-lock.json
|
||||
badge-maker/package-lock.json
|
||||
|
||||
# Folder view configuration files
|
||||
.DS_Store
|
||||
|
||||
30
.nowignore
30
.nowignore
@@ -1,30 +0,0 @@
|
||||
*
|
||||
!frontend/
|
||||
!gh-badges/
|
||||
!lib/
|
||||
!core/
|
||||
!logo/
|
||||
!pages/
|
||||
!public/
|
||||
!templates/
|
||||
!services/
|
||||
!package-lock.json
|
||||
!/*.js
|
||||
!scripts/export-*.js
|
||||
!config/
|
||||
config/local*.yml
|
||||
*.spec.js
|
||||
*~
|
||||
.env
|
||||
.circleci
|
||||
.github
|
||||
.vscode
|
||||
__snapshots__
|
||||
.buildpacks
|
||||
.eslint*
|
||||
.editorconfig
|
||||
.nycrc*
|
||||
.gitpod*
|
||||
.prettier*
|
||||
CONTRIBUTING.md
|
||||
Dockerfile
|
||||
@@ -10,6 +10,5 @@ package-lock.json
|
||||
private/*.json
|
||||
/.nyc_output
|
||||
analytics.json
|
||||
gh-badges/templates/default-template.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
semi: false
|
||||
singleQuote: true
|
||||
trailingComma: es5
|
||||
bracketSpacing: true
|
||||
endOfLine: lf
|
||||
arrowParens: avoid
|
||||
|
||||
@@ -5,8 +5,8 @@ RUN mkdir /usr/src/app/private
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json package-lock.json /usr/src/app/
|
||||
# Without the gh-badges package.json and CLI script in place, `npm ci` will fail.
|
||||
COPY gh-badges /usr/src/app/gh-badges/
|
||||
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
|
||||
COPY badge-maker /usr/src/app/badge-maker/
|
||||
|
||||
# We need dev deps to build the front end. We don't need Cypress, though.
|
||||
RUN NODE_ENV=development CYPRESS_INSTALL_BINARY=0 npm ci
|
||||
|
||||
12
README.md
12
README.md
@@ -43,16 +43,16 @@ Every month it serves over 470 million images.
|
||||
This repo hosts:
|
||||
|
||||
- The [Shields.io][shields.io] frontend and server code
|
||||
- An [NPM library for generating badges][gh-badges]
|
||||
- [documentation][gh-badges-docs]
|
||||
- [changelog][gh-badges-changelog]
|
||||
- An [NPM library for generating badges][badge-maker]
|
||||
- [documentation][badge-maker-docs]
|
||||
- [changelog][badge-maker-changelog]
|
||||
- The [badge design specification][badge-spec]
|
||||
|
||||
[shields.io]: https://shields.io/
|
||||
[gh-badges]: https://www.npmjs.com/package/gh-badges
|
||||
[badge-maker]: https://www.npmjs.com/package/badge-maker
|
||||
[badge-spec]: https://github.com/badges/shields/tree/master/spec
|
||||
[gh-badges-docs]: https://github.com/badges/shields/tree/master/gh-badges/README.md
|
||||
[gh-badges-changelog]: https://github.com/badges/shields/tree/master/gh-badges/CHANGELOG.md
|
||||
[badge-maker-docs]: https://github.com/badges/shields/tree/master/badge-maker/README.md
|
||||
[badge-maker-changelog]: https://github.com/badges/shields/tree/master/badge-maker/CHANGELOG.md
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -1,19 +1,127 @@
|
||||
exports['The badge generator SVG should always produce the same SVG (unless we have changed something!) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h45v20H0z"/><path fill="#4c1" d="M45 0h45v20H45z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"> <text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g> </svg>
|
||||
exports['The badge generator SVG should match snapshot 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><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="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="45" height="20" fill="#555"/><rect x="45" width="45" height="20" fill="#4c1"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge shields GitHub logo custom color (whitesmoke) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="github"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message/label, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><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="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="45" height="20" fill="#0f0"/><rect x="45" width="45" height="20" fill="#b3e"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge shields GitHub logo default color (#333333) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="github"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message/label, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="107" height="20"><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="107" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="62" height="20" fill="#0f0"/><rect x="62" width="45" height="20" fill="#b3e"/><rect width="107" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="405" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="405" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="835" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge simple-icons javascript logo custom color (rgba(46,204,113,0.8)) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="javascript"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message only, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="45" height="20"><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="45" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="0" height="20" fill="#b3e"/><rect x="0" width="45" height="20" fill="#b3e"/><rect width="45" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="225" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="225" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge simple-icons javascript logo default color (#F7DF1E) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="javascript"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message only, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="63" height="20"><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="63" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="0" height="20" fill="#555"/><rect x="0" width="63" height="20" fill="#b3e"/><rect width="63" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="405" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="405" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message only, with logo and labelColor 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="69" height="20"><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="69" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="24" height="20" fill="#0f0"/><rect x="24" width="45" height="20" fill="#b3e"/><rect width="69" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="455" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="455" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message/label, with links 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><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="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="45" height="20" fill="#0f0"/><rect x="45" width="45" height="20" fill="#b3e"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="90" height="20" fill="rgba(0,0,0,0)"/></a><a target="_blank" xlink:href="https://shields.io/"><rect width="45" height="20" fill="rgba(0,0,0,0)"/></a></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message/label, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><g shape-rendering="crispEdges"><rect width="45" height="20" fill="#0f0"/><rect x="45" width="45" height="20" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message/label, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="107" height="20"><g shape-rendering="crispEdges"><rect width="62" height="20" fill="#0f0"/><rect x="62" width="45" height="20" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="405" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="835" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message only, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="45" height="20"><g shape-rendering="crispEdges"><rect width="0" height="20" fill="#b3e"/><rect x="0" width="45" height="20" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="225" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message only, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="63" height="20"><g shape-rendering="crispEdges"><rect width="0" height="20" fill="#555"/><rect x="0" width="63" height="20" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="405" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message only, with logo and labelColor 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="69" height="20"><g shape-rendering="crispEdges"><rect width="24" height="20" fill="#0f0"/><rect x="24" width="45" height="20" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="455" y="140" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message/label, with links 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20"><g shape-rendering="crispEdges"><rect width="45" height="20" fill="#0f0"/><rect x="45" width="45" height="20" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="140" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="140" transform="scale(.1)" textLength="350">grown</text></g><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="90" height="20" fill="rgba(0,0,0,0)"/></a><a target="_blank" xlink:href="https://shields.io/"><rect width="45" height="20" fill="rgba(0,0,0,0)"/></a></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message/label, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="18"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="90" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="45" height="18" fill="#0f0"/><rect x="45" width="45" height="18" fill="#b3e"/><rect width="90" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="130" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="130" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message/label, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="107" height="18"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="107" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="62" height="18" fill="#0f0"/><rect x="62" width="45" height="18" fill="#b3e"/><rect width="107" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="2" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="405" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="405" y="130" transform="scale(.1)" textLength="350">cactus</text><text x="835" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="835" y="130" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message only, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="45" height="18"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="45" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="0" height="18" fill="#b3e"/><rect x="0" width="45" height="18" fill="#b3e"/><rect width="45" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="225" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="225" y="130" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message only, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="63" height="18"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="63" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="0" height="18" fill="#555"/><rect x="0" width="63" height="18" fill="#b3e"/><rect width="63" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="2" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="405" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="405" y="130" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message only, with logo and labelColor 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="69" height="18"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="69" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="24" height="18" fill="#0f0"/><rect x="24" width="45" height="18" fill="#b3e"/><rect width="69" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="2" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="455" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="455" y="130" transform="scale(.1)" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message/label, with links 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="18"><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="90" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="45" height="18" fill="#0f0"/><rect x="45" width="45" height="18" fill="#b3e"/><rect width="90" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="235" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="130" transform="scale(.1)" textLength="350">cactus</text><text x="665" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="130" transform="scale(.1)" textLength="350">grown</text></g><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="90" height="18" fill="rgba(0,0,0,0)"/></a><a target="_blank" xlink:href="https://shields.io/"><rect width="45" height="18" fill="rgba(0,0,0,0)"/></a></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message/label, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="147" height="28"><g shape-rendering="crispEdges"><rect width="74" height="28" fill="#0f0"/><rect x="74" width="73" height="28" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><text x="370" y="175" transform="scale(.1)" textLength="500">CACTUS</text><text x="1105" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message/label, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="164" height="28"><g shape-rendering="crispEdges"><rect width="91" height="28" fill="#0f0"/><rect x="91" width="73" height="28" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><image x="9" y="7" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="540" y="175" transform="scale(.1)" textLength="500">CACTUS</text><text x="1275" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message only, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="73" height="28"><g shape-rendering="crispEdges"><rect width="0" height="28" fill="#b3e"/><rect x="0" width="73" height="28" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><text x="365" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message only, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="91" height="28"><g shape-rendering="crispEdges"><rect width="0" height="28" fill="#555"/><rect x="0" width="91" height="28" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><image x="9" y="7" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="545" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message only, with logo and labelColor 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="105" height="28"><g shape-rendering="crispEdges"><rect width="32" height="28" fill="#0f0"/><rect x="32" width="73" height="28" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><image x="9" y="7" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="230" y="175" transform="scale(.1)" textLength="-60"></text><text x="685" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message/label, with links 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="147" height="28"><g shape-rendering="crispEdges"><rect width="74" height="28" fill="#0f0"/><rect x="74" width="73" height="28" fill="#b3e"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><text x="370" y="175" transform="scale(.1)" textLength="500">CACTUS</text><text x="1105" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="147" height="28" fill="rgba(0,0,0,0)"/></a><a target="_blank" xlink:href="https://shields.io/"><rect width="74" height="28" fill="rgba(0,0,0,0)"/></a></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message/label, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="95" height="20"><style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="47" height="19" rx="2"/><rect x="53.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa"/><rect x="53" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M53.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><g fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><text x="235" y="150" fill="#fff" transform="scale(.1)" textLength="370">Cactus</text><text x="235" y="140" transform="scale(.1)" textLength="370">Cactus</text><text x="735" y="150" fill="#fff" transform="scale(.1)" textLength="330">grown</text><text id="rlink" x="735" y="140" transform="scale(.1)" textLength="330">grown</text></g><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="47" height="19" rx="2" /></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message/label, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="112" height="20"><style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="64" height="19" rx="2"/><rect x="70.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa"/><rect x="70" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M70.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><g fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><text x="405" y="150" fill="#fff" transform="scale(.1)" textLength="370">Cactus</text><text x="405" y="140" transform="scale(.1)" textLength="370">Cactus</text><text x="905" y="150" fill="#fff" transform="scale(.1)" textLength="330">grown</text><text id="rlink" x="905" y="140" transform="scale(.1)" textLength="330">grown</text></g><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="64" height="19" rx="2" /></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message only, no logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="59" height="20"><style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="11" height="19" rx="2"/><rect x="17.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa"/><rect x="17" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M17.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><g fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><text x="55" y="150" fill="#fff" transform="scale(.1)" textLength="10"></text><text x="55" y="140" transform="scale(.1)" textLength="10"></text><text x="375" y="150" fill="#fff" transform="scale(.1)" textLength="330">grown</text><text id="rlink" x="375" y="140" transform="scale(.1)" textLength="330">grown</text></g><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="11" height="19" rx="2" /></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message only, with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="73" height="20"><style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="25" height="19" rx="2"/><rect x="31.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa"/><rect x="31" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M31.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><g fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><text x="195" y="150" fill="#fff" transform="scale(.1)" textLength="10"></text><text x="195" y="140" transform="scale(.1)" textLength="10"></text><text x="515" y="150" fill="#fff" transform="scale(.1)" textLength="330">grown</text><text id="rlink" x="515" y="140" transform="scale(.1)" textLength="330">grown</text></g><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="25" height="19" rx="2" /></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message only, with logo and labelColor 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="73" height="20"><style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="25" height="19" rx="2"/><rect x="31.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa"/><rect x="31" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M31.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><g fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><text x="195" y="150" fill="#fff" transform="scale(.1)" textLength="10"></text><text x="195" y="140" transform="scale(.1)" textLength="10"></text><text x="515" y="150" fill="#fff" transform="scale(.1)" textLength="330">grown</text><text id="rlink" x="515" y="140" transform="scale(.1)" textLength="330">grown</text></g><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="25" height="19" rx="2" /></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message/label, with links 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="95" height="20"><style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="47" height="19" rx="2"/><rect x="53.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa"/><rect x="53" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M53.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><g fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><text x="235" y="150" fill="#fff" transform="scale(.1)" textLength="370">Cactus</text><text x="235" y="140" transform="scale(.1)" textLength="370">Cactus</text><text x="735" y="150" fill="#fff" transform="scale(.1)" textLength="330">grown</text><a target="_blank" xlink:href="https://www.google.co.uk/"><text id="rlink" x="735" y="140" transform="scale(.1)" textLength="330">grown</text></a></g><a target="_blank" xlink:href="https://shields.io/"><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="47" height="19" rx="2" /></a></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator badges with logos should always produce the same badge badge with logo 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><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="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="54" height="20" fill="#555"/><rect x="54" width="59" height="20" fill="#4c1"/><rect width="113" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"/><text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g></svg>
|
||||
`
|
||||
|
||||
@@ -1,6 +1,68 @@
|
||||
# Changelog
|
||||
|
||||
## 2.2.1
|
||||
## 3.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Dropped support for node < 10
|
||||
- Package name has changed to `badge-maker` and moved to https://www.npmjs.com/package/badge-maker
|
||||
- `BadgeFactory` class is removed and replaced by `makeBadge()` function.
|
||||
- Deprecated parameters have been removed. In version 2.2.0 the `colorA`, `colorB` and `colorscheme` params were deprecated. In version 3.0.0 these have been removed.
|
||||
- Only SVG output format is now provided. JSON format has been dropped and the `format` key has been removed.
|
||||
- The `text` array has been replaced by `label` and `message` keys.
|
||||
- The `template` key has been renamed `style`.
|
||||
To upgrade from v2.1.1, change your code from:
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
const bf = new BadgeFactory()
|
||||
const svg = bf.create({
|
||||
text: ['build', 'passed'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
})
|
||||
```
|
||||
to:
|
||||
```js
|
||||
const { makeBadge } = require('badge-maker')
|
||||
const svg = makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
style: 'flat-square',
|
||||
})
|
||||
```
|
||||
- `ValidationError` had been added and inputs are now validated. In previous releases, invalid inputs would be discarded and replaced with defaults. For example, in 2.2.1
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
const bf = new BadgeFactory()
|
||||
const svg = bf.create({
|
||||
text: ['build', 'passed'],
|
||||
template: 'some invalid value',
|
||||
})
|
||||
```
|
||||
would generate an SVG badge. In version >=3
|
||||
```js
|
||||
const { makeBadge } = require('badge-maker')
|
||||
const svg = makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
style: 'some invalid value',
|
||||
})
|
||||
```
|
||||
will throw a `ValidationError`.
|
||||
- Raster support has been removed from the CLI. It will now only output SVG. On the console, the output of `badge` can be piped to a utility like [imagemagick](https://imagemagick.org/script/command-line-processing.php). If you were previously using
|
||||
```sh
|
||||
badge build passed :green .gif
|
||||
```
|
||||
this could be replaced by
|
||||
```sh
|
||||
badge build passed :green | magick svg:- gif:-
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
- Removed dependency on doT library which has known vulnerabilities.
|
||||
|
||||
## 2.2.1 - 2019-05-30
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# gh-badges
|
||||
# badge-maker
|
||||
|
||||
[](https://npmjs.org/package/gh-badges)
|
||||
[](https://npmjs.org/package/gh-badges)
|
||||
[](https://npmjs.org/package/badge-maker)
|
||||
[](https://npmjs.org/package/badge-maker)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install gh-badges
|
||||
npm install badge-maker
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -14,29 +14,34 @@ npm install gh-badges
|
||||
### On the console
|
||||
|
||||
```sh
|
||||
npm install -g gh-badges
|
||||
badge build passed :green .png > mybadge.png
|
||||
npm install -g badge-maker
|
||||
badge build passed :green > mybadge.svg
|
||||
```
|
||||
|
||||
### As a library
|
||||
|
||||
```js
|
||||
const { BadgeFactory } = require('gh-badges')
|
||||
|
||||
const bf = new BadgeFactory()
|
||||
const { makeBadge, ValidationError } = require('badge-maker')
|
||||
|
||||
const format = {
|
||||
text: ['build', 'passed'],
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
color: 'green',
|
||||
template: 'flat',
|
||||
}
|
||||
|
||||
const svg = bf.create(format)
|
||||
const svg = makeBadge(format)
|
||||
console.log(svg) // <svg...
|
||||
|
||||
try {
|
||||
makeBadge({})
|
||||
} catch (e) {
|
||||
console.log(e) // ValidationError: Field `message` is required
|
||||
}
|
||||
```
|
||||
|
||||
### Node version support
|
||||
|
||||
The latest version of gh-badges supports all currently maintained Node
|
||||
The latest version of badge-maker supports all currently maintained Node
|
||||
versions. See the [Node Release Schedule][].
|
||||
|
||||
[node release schedule]: https://github.com/nodejs/Release#release-schedule
|
||||
@@ -47,28 +52,17 @@ The format is the following:
|
||||
|
||||
```js
|
||||
{
|
||||
text: [ 'build', 'passed' ], // Textual information shown, in order
|
||||
label: 'build', // (Optional) Badge label
|
||||
message: 'passed', // (Required) Badge message
|
||||
labelColor: '#555', // (Optional) Label color
|
||||
color: '#4c1', // (Optional) Message color
|
||||
|
||||
format: 'svg', // Also supports json
|
||||
|
||||
color: '#4c1',
|
||||
labelColor: '#555',
|
||||
|
||||
// See templates/ for a list of available templates.
|
||||
// (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social'
|
||||
// Each offers a different visual design.
|
||||
template: 'flat',
|
||||
|
||||
// Deprecated attributes:
|
||||
colorscheme: 'green', // Now an alias for `color`.
|
||||
colorB: '#4c1', // Now an alias for `color`.
|
||||
colorA: '#555', // Now an alias for `labelColor`.
|
||||
style: 'flat',
|
||||
}
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
- [templates/](./templates) for the `template` option
|
||||
|
||||
## Colors
|
||||
|
||||
There are three ways to specify `color` and `labelColor`:
|
||||
@@ -126,3 +120,12 @@ There are three ways to specify `color` and `labelColor`:
|
||||
[lightslategray]: https://img.shields.io/badge/lightslategray-lightslategray.svg
|
||||
[css color]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
|
||||
[css/svg color]: http://www.w3.org/TR/SVG/types.html#DataTypeColor
|
||||
|
||||
## Raster Formats
|
||||
|
||||
Conversion to raster formats is no longer directly supported. In javascript
|
||||
code, SVG badges can be converted to raster formats using a library like
|
||||
[gm](https://www.npmjs.com/package/gm). On the console, the output of `badge`
|
||||
can be piped to a utility like
|
||||
[imagemagick](https://imagemagick.org/script/command-line-processing.php)
|
||||
e.g: `badge build passed :green | magick svg:- gif:-`.
|
||||
@@ -2,23 +2,18 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const makeBadge = require('./make-badge')
|
||||
const svg2img = require('./svg-to-img')
|
||||
const { namedColors } = require('./color')
|
||||
const { makeBadge } = require('./index')
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
console.log('Usage: badge subject status [:color] [.output] [@style]')
|
||||
console.log(
|
||||
'Or: badge subject status color [labelColor] [.output] [@style]'
|
||||
)
|
||||
console.log('Usage: badge label message [:color] [@style]')
|
||||
console.log('Or: badge label message color [labelColor] [@style]')
|
||||
console.log()
|
||||
console.log(' color, labelColor:')
|
||||
console.log(` one of ${Object.keys(namedColors).join(', ')}.`)
|
||||
console.log(' #xxx (three hex digits)')
|
||||
console.log(' #xxxxxx (six hex digits)')
|
||||
console.log(' color (CSS color)')
|
||||
console.log(' output:')
|
||||
console.log(' svg, png, jpg, or gif')
|
||||
console.log()
|
||||
console.log('Eg: badge cactus grown :green @flat')
|
||||
console.log()
|
||||
@@ -26,14 +21,8 @@ if (process.argv.length < 4) {
|
||||
}
|
||||
|
||||
// Find a format specifier.
|
||||
let format = 'svg'
|
||||
let style = ''
|
||||
for (let i = 4; i < process.argv.length; i++) {
|
||||
if (process.argv[i][0] === '.') {
|
||||
format = process.argv[i].slice(1)
|
||||
process.argv.splice(i, 1)
|
||||
continue
|
||||
}
|
||||
if (process.argv[i][0] === '@') {
|
||||
style = process.argv[i].slice(1)
|
||||
process.argv.splice(i, 1)
|
||||
@@ -41,14 +30,14 @@ for (let i = 4; i < process.argv.length; i++) {
|
||||
}
|
||||
}
|
||||
|
||||
const subject = process.argv[2]
|
||||
const status = process.argv[3]
|
||||
const label = process.argv[2]
|
||||
const message = process.argv[3]
|
||||
let color = process.argv[4] || ':green'
|
||||
const colorA = process.argv[5]
|
||||
const labelColor = process.argv[5]
|
||||
|
||||
const badgeData = { text: [subject, status], format }
|
||||
const badgeData = { label, message }
|
||||
if (style) {
|
||||
badgeData.template = style
|
||||
badgeData.style = style
|
||||
}
|
||||
|
||||
if (color[0] === ':') {
|
||||
@@ -58,28 +47,17 @@ if (color[0] === ':') {
|
||||
console.error('Invalid color scheme.')
|
||||
process.exit(1)
|
||||
}
|
||||
badgeData.colorscheme = color
|
||||
badgeData.color = color
|
||||
} else {
|
||||
badgeData.colorB = color
|
||||
if (colorA) {
|
||||
badgeData.colorA = colorA
|
||||
badgeData.color = color
|
||||
if (labelColor) {
|
||||
badgeData.labelColor = labelColor
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const svg = makeBadge(badgeData)
|
||||
|
||||
if (/png|jpg|gif/.test(format)) {
|
||||
const data = await svg2img(svg, format)
|
||||
process.stdout.write(data)
|
||||
} else {
|
||||
console.log(svg)
|
||||
}
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
;(() => {
|
||||
try {
|
||||
await main()
|
||||
console.log(makeBadge(badgeData))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const isPng = require('is-png')
|
||||
const isSvg = require('is-svg')
|
||||
const { spawn } = require('child-process-promise')
|
||||
const { expect, use } = require('chai')
|
||||
@@ -14,13 +13,13 @@ function runCli(args) {
|
||||
})
|
||||
}
|
||||
|
||||
describe('The CLI', function() {
|
||||
it('should provide a help message', async function() {
|
||||
describe('The CLI', function () {
|
||||
it('should provide a help message', async function () {
|
||||
const { stdout } = await runCli([])
|
||||
expect(stdout).to.startWith('Usage')
|
||||
})
|
||||
|
||||
it('should produce default badges', async function() {
|
||||
it('should produce default badges', async function () {
|
||||
const { stdout } = await runCli(['cactus', 'grown'])
|
||||
expect(stdout)
|
||||
.to.satisfy(isSvg)
|
||||
@@ -28,30 +27,13 @@ describe('The CLI', function() {
|
||||
.and.to.include('grown')
|
||||
})
|
||||
|
||||
it('should produce colorschemed badges', async function() {
|
||||
it('should produce colorschemed badges', async function () {
|
||||
const { stdout } = await runCli(['cactus', 'grown', ':green'])
|
||||
expect(stdout).to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
it('should produce right-color badges', async function() {
|
||||
it('should produce right-color badges', async function () {
|
||||
const { stdout } = await runCli(['cactus', 'grown', '#abcdef'])
|
||||
expect(stdout)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('#abcdef')
|
||||
})
|
||||
|
||||
it('should produce PNG badges', async function() {
|
||||
const child = runCli(['cactus', 'grown', '.png'])
|
||||
|
||||
// The buffering done by `child-process-promise` doesn't seem correctly to
|
||||
// handle binary data.
|
||||
let chunk
|
||||
child.childProcess.stdout.once('data', data => {
|
||||
chunk = data
|
||||
})
|
||||
|
||||
await child
|
||||
|
||||
expect(chunk).to.satisfy(isPng)
|
||||
expect(stdout).to.satisfy(isSvg).and.to.include('#abcdef')
|
||||
})
|
||||
})
|
||||
610
badge-maker/lib/badge-renderers.js
Normal file
610
badge-maker/lib/badge-renderers.js
Normal file
@@ -0,0 +1,610 @@
|
||||
'use strict'
|
||||
|
||||
const anafanafo = require('anafanafo')
|
||||
|
||||
const fontFamily = 'font-family="Verdana,Geneva,DejaVu Sans,sans-serif"'
|
||||
const socialFontFamily =
|
||||
'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"'
|
||||
|
||||
function capitalize(s) {
|
||||
return `${s.charAt(0).toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
function escapeXml(s) {
|
||||
if (s === undefined || typeof s !== 'string') {
|
||||
return undefined
|
||||
} else {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
}
|
||||
|
||||
function roundUpToOdd(val) {
|
||||
// Increase chances of pixel grid alignment.
|
||||
return val % 2 === 0 ? val + 1 : val
|
||||
}
|
||||
|
||||
function preferredWidthOf(str) {
|
||||
return roundUpToOdd((anafanafo(str) / 10) | 0)
|
||||
}
|
||||
|
||||
function computeWidths({ label, message }) {
|
||||
return {
|
||||
labelWidth: preferredWidthOf(label),
|
||||
messageWidth: preferredWidthOf(message),
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogo({
|
||||
logo,
|
||||
badgeHeight,
|
||||
horizPadding,
|
||||
logoWidth = 14,
|
||||
logoPadding = 0,
|
||||
}) {
|
||||
if (!logo) {
|
||||
return {
|
||||
hasLogo: false,
|
||||
totalLogoWidth: 0,
|
||||
renderedLogo: '',
|
||||
}
|
||||
}
|
||||
const logoHeight = 14
|
||||
const y = (badgeHeight - logoHeight) / 2
|
||||
const x = horizPadding
|
||||
return {
|
||||
hasLogo: true,
|
||||
totalLogoWidth: logoWidth + logoPadding,
|
||||
renderedLogo: `<image x="${x}" y="${y}" width="${logoWidth}" height="14" xlink:href="${escapeXml(
|
||||
logo
|
||||
)}"/>`,
|
||||
}
|
||||
}
|
||||
|
||||
function renderText({
|
||||
leftMargin,
|
||||
horizPadding = 0,
|
||||
content,
|
||||
verticalMargin = 0,
|
||||
shadow = false,
|
||||
}) {
|
||||
if (!content.length) {
|
||||
return { renderedText: '', width: 0 }
|
||||
}
|
||||
|
||||
const textLength = preferredWidthOf(content)
|
||||
const escapedContent = escapeXml(content)
|
||||
|
||||
const shadowMargin = 150 + verticalMargin
|
||||
const textMargin = 140 + verticalMargin
|
||||
|
||||
const outTextLength = 10 * textLength
|
||||
const x = 10 * (leftMargin + 0.5 * textLength + horizPadding)
|
||||
|
||||
let renderedText = ''
|
||||
if (shadow) {
|
||||
renderedText = `<text x="${x}" y="${shadowMargin}" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${outTextLength}">${escapedContent}</text>`
|
||||
}
|
||||
renderedText += `<text x="${x}" y="${textMargin}" transform="scale(.1)" textLength="${outTextLength}">${escapedContent}</text>`
|
||||
|
||||
return {
|
||||
renderedText,
|
||||
width: textLength,
|
||||
}
|
||||
}
|
||||
|
||||
function renderLinks({
|
||||
links: [leftLink, rightLink] = [],
|
||||
leftWidth,
|
||||
rightWidth,
|
||||
height,
|
||||
}) {
|
||||
leftLink = escapeXml(leftLink)
|
||||
rightLink = escapeXml(rightLink)
|
||||
const hasLeftLink = leftLink && leftLink.length
|
||||
const hasRightLink = rightLink && rightLink.length
|
||||
const leftLinkWidth = hasRightLink ? leftWidth : leftWidth + rightWidth
|
||||
|
||||
function render({ link, width }) {
|
||||
return `<a target="_blank" xlink:href="${link}"><rect width="${width}" height="${height}" fill="rgba(0,0,0,0)"/></a>`
|
||||
}
|
||||
|
||||
return (
|
||||
(hasRightLink
|
||||
? render({ link: rightLink, width: leftWidth + rightWidth })
|
||||
: '') +
|
||||
(hasLeftLink ? render({ link: leftLink, width: leftLinkWidth }) : '')
|
||||
)
|
||||
}
|
||||
|
||||
function renderBadge({ links, leftWidth, rightWidth, height }, main) {
|
||||
const width = leftWidth + rightWidth
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}">
|
||||
${main}
|
||||
${renderLinks({ links, leftWidth, rightWidth, height })}
|
||||
</svg>`
|
||||
}
|
||||
|
||||
function stripXmlWhitespace(xml) {
|
||||
return xml.replace(/>\s+/g, '>').replace(/<\s+/g, '<').trim()
|
||||
}
|
||||
|
||||
class Badge {
|
||||
static get fontFamily() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
static get height() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
static get verticalMargin() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
static get shadow() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
constructor({
|
||||
label,
|
||||
message,
|
||||
links,
|
||||
logo,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
color = '#4c1',
|
||||
labelColor,
|
||||
}) {
|
||||
const horizPadding = 5
|
||||
const { hasLogo, totalLogoWidth, renderedLogo } = renderLogo({
|
||||
logo,
|
||||
badgeHeight: this.constructor.height,
|
||||
horizPadding,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
})
|
||||
const hasLabel = label.length || labelColor
|
||||
if (labelColor == null) {
|
||||
labelColor = '#555'
|
||||
}
|
||||
|
||||
labelColor = hasLabel || hasLogo ? labelColor : color
|
||||
labelColor = escapeXml(labelColor)
|
||||
color = escapeXml(color)
|
||||
|
||||
const labelMargin = totalLogoWidth + 1
|
||||
|
||||
const { renderedText: renderedLabel, width: labelWidth } = renderText({
|
||||
leftMargin: labelMargin,
|
||||
horizPadding,
|
||||
content: label,
|
||||
verticalMargin: this.constructor.verticalMargin,
|
||||
shadow: this.constructor.shadow,
|
||||
})
|
||||
|
||||
const leftWidth = hasLabel
|
||||
? labelWidth + 2 * horizPadding + totalLogoWidth
|
||||
: 0
|
||||
|
||||
let messageMargin = leftWidth - (message.length ? 1 : 0)
|
||||
if (!hasLabel) {
|
||||
if (hasLogo) {
|
||||
messageMargin = messageMargin + totalLogoWidth + horizPadding
|
||||
} else {
|
||||
messageMargin = messageMargin + 1
|
||||
}
|
||||
}
|
||||
|
||||
const { renderedText: renderedMessage, width: messageWidth } = renderText({
|
||||
leftMargin: messageMargin,
|
||||
horizPadding,
|
||||
content: message,
|
||||
verticalMargin: this.constructor.verticalMargin,
|
||||
shadow: this.constructor.shadow,
|
||||
})
|
||||
|
||||
let rightWidth = messageWidth + 2 * horizPadding
|
||||
if (hasLogo && !hasLabel) {
|
||||
rightWidth += totalLogoWidth + horizPadding - 1
|
||||
}
|
||||
|
||||
const width = leftWidth + rightWidth
|
||||
|
||||
this.links = links
|
||||
this.leftWidth = leftWidth
|
||||
this.rightWidth = rightWidth
|
||||
this.width = width
|
||||
this.labelColor = labelColor
|
||||
this.color = color
|
||||
this.renderedLogo = renderedLogo
|
||||
this.renderedLabel = renderedLabel
|
||||
this.renderedMessage = renderedMessage
|
||||
}
|
||||
|
||||
render() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
class Plastic extends Badge {
|
||||
static get fontFamily() {
|
||||
return fontFamily
|
||||
}
|
||||
|
||||
static get height() {
|
||||
return 18
|
||||
}
|
||||
|
||||
static get verticalMargin() {
|
||||
return -10
|
||||
}
|
||||
|
||||
static get shadow() {
|
||||
return true
|
||||
}
|
||||
|
||||
render() {
|
||||
return renderBadge(
|
||||
{
|
||||
links: this.links,
|
||||
leftWidth: this.leftWidth,
|
||||
rightWidth: this.rightWidth,
|
||||
height: this.constructor.height,
|
||||
},
|
||||
`
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".7"/>
|
||||
<stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
|
||||
<stop offset=".9" stop-color="#000" stop-opacity=".3"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity=".5"/>
|
||||
</linearGradient>
|
||||
|
||||
<clipPath id="r">
|
||||
<rect width="${this.width}" height="${this.constructor.height}" rx="4" fill="#fff"/>
|
||||
</clipPath>
|
||||
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="${this.leftWidth}" height="${this.constructor.height}" fill="${this.labelColor}"/>
|
||||
<rect x="${this.leftWidth}" width="${this.rightWidth}" height="${this.constructor.height}" fill="${this.color}"/>
|
||||
<rect width="${this.width}" height="${this.constructor.height}" fill="url(#s)"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle" ${this.constructor.fontFamily} text-rendering="geometricPrecision" font-size="110">
|
||||
${this.renderedLogo}
|
||||
${this.renderedLabel}
|
||||
${this.renderedMessage}
|
||||
</g>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Flat extends Badge {
|
||||
static get fontFamily() {
|
||||
return fontFamily
|
||||
}
|
||||
|
||||
static get height() {
|
||||
return 20
|
||||
}
|
||||
|
||||
static get verticalMargin() {
|
||||
return 0
|
||||
}
|
||||
|
||||
static get shadow() {
|
||||
return true
|
||||
}
|
||||
|
||||
render() {
|
||||
return renderBadge(
|
||||
{
|
||||
links: this.links,
|
||||
leftWidth: this.leftWidth,
|
||||
rightWidth: this.rightWidth,
|
||||
height: this.constructor.height,
|
||||
},
|
||||
`
|
||||
<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="${this.width}" height="${this.constructor.height}" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="${this.leftWidth}" height="${this.constructor.height}" fill="${this.labelColor}"/>
|
||||
<rect x="${this.leftWidth}" width="${this.rightWidth}" height="${this.constructor.height}" fill="${this.color}"/>
|
||||
<rect width="${this.width}" height="${this.constructor.height}" fill="url(#s)"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle" ${this.constructor.fontFamily} text-rendering="geometricPrecision" font-size="110">
|
||||
${this.renderedLogo}
|
||||
${this.renderedLabel}
|
||||
${this.renderedMessage}
|
||||
</g>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class FlatSquare extends Badge {
|
||||
static get fontFamily() {
|
||||
return fontFamily
|
||||
}
|
||||
|
||||
static get height() {
|
||||
return 20
|
||||
}
|
||||
|
||||
static get verticalMargin() {
|
||||
return 0
|
||||
}
|
||||
|
||||
static get shadow() {
|
||||
return false
|
||||
}
|
||||
|
||||
render() {
|
||||
return renderBadge(
|
||||
{
|
||||
links: this.links,
|
||||
leftWidth: this.leftWidth,
|
||||
rightWidth: this.rightWidth,
|
||||
height: this.constructor.height,
|
||||
},
|
||||
`
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="${this.leftWidth}" height="${this.constructor.height}" fill="${this.labelColor}"/>
|
||||
<rect x="${this.leftWidth}" width="${this.rightWidth}" height="${this.constructor.height}" fill="${this.color}"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle" ${this.constructor.fontFamily} text-rendering="geometricPrecision" font-size="110">
|
||||
${this.renderedLogo}
|
||||
${this.renderedLabel}
|
||||
${this.renderedMessage}
|
||||
</g>`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function plastic(params) {
|
||||
const badge = new Plastic(params)
|
||||
if (params.minify) {
|
||||
return stripXmlWhitespace(badge.render())
|
||||
}
|
||||
return badge.render()
|
||||
}
|
||||
|
||||
function flat(params) {
|
||||
const badge = new Flat(params)
|
||||
if (params.minify) {
|
||||
return stripXmlWhitespace(badge.render())
|
||||
}
|
||||
return badge.render()
|
||||
}
|
||||
|
||||
function flatSquare(params) {
|
||||
const badge = new FlatSquare(params)
|
||||
if (params.minify) {
|
||||
return stripXmlWhitespace(badge.render())
|
||||
}
|
||||
return badge.render()
|
||||
}
|
||||
|
||||
function social({
|
||||
label,
|
||||
message,
|
||||
links = [],
|
||||
logo,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
color = '#4c1',
|
||||
labelColor = '#555',
|
||||
minify,
|
||||
}) {
|
||||
// Social label is styled with a leading capital. Convert to caps here so
|
||||
// width can be measured using the correct characters.
|
||||
label = capitalize(label)
|
||||
|
||||
const externalHeight = 20
|
||||
const internalHeight = 19
|
||||
const horizPadding = 5
|
||||
const { totalLogoWidth, renderedLogo } = renderLogo({
|
||||
logo,
|
||||
badgeHeight: externalHeight,
|
||||
horizPadding,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
})
|
||||
const hasMessage = message.length
|
||||
|
||||
let { labelWidth, messageWidth } = computeWidths({ label, message })
|
||||
labelWidth += 10 + totalLogoWidth
|
||||
messageWidth += 10
|
||||
messageWidth -= 4
|
||||
|
||||
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
|
||||
const labelTextLength = (labelWidth - (10 + totalLogoWidth)) * 10
|
||||
const escapedLabel = escapeXml(label)
|
||||
|
||||
let [leftLink, rightLink] = links
|
||||
leftLink = escapeXml(leftLink)
|
||||
rightLink = escapeXml(rightLink)
|
||||
const hasLeftLink = leftLink && leftLink.length
|
||||
const hasRightLink = rightLink && rightLink.length
|
||||
|
||||
function renderMessageBubble() {
|
||||
const messageBubbleMainX = labelWidth + 6.5
|
||||
const messageBubbleNotchX = labelWidth + 6
|
||||
return `
|
||||
<rect x="${messageBubbleMainX}" y="0.5" width="${messageWidth}" height="${internalHeight}" rx="2" fill="#fafafa"/>
|
||||
<rect x="${messageBubbleNotchX}" y="7.5" width="0.5" height="5" stroke="#fafafa"/>
|
||||
<path d="M${messageBubbleMainX} 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/>
|
||||
`
|
||||
}
|
||||
|
||||
function renderMessageText() {
|
||||
const messageTextX = (labelWidth + messageWidth / 2 + 6) * 10
|
||||
const messageTextLength = (messageWidth - 8) * 10
|
||||
const escapedMessage = escapeXml(message)
|
||||
const shadow = `<text x="${messageTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
|
||||
const text = `<text id="rlink" x="${messageTextX}" y="140" transform="scale(.1)" textLength="${messageTextLength}">${escapedMessage}</text>`
|
||||
if (hasRightLink) {
|
||||
return `
|
||||
${shadow}
|
||||
<a target="_blank" xlink:href="${rightLink}">${text}</a>
|
||||
`
|
||||
}
|
||||
return `
|
||||
${shadow}
|
||||
${text}
|
||||
`
|
||||
}
|
||||
|
||||
function renderLeftLink() {
|
||||
const rect = `<rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="${labelWidth}" height="${internalHeight}" rx="2" />`
|
||||
if (hasLeftLink) {
|
||||
return `<a target="_blank" xlink:href="${leftLink}">${rect}</a>`
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
const badge = renderBadge(
|
||||
{
|
||||
links: [],
|
||||
leftWidth: labelWidth + 1,
|
||||
rightWidth: hasMessage ? messageWidth + 6 : 0,
|
||||
height: externalHeight,
|
||||
},
|
||||
`
|
||||
<style>a #llink:hover{fill:url(#b);stroke:#ccc}a #rlink:hover{fill:#4183c4}</style>
|
||||
<linearGradient id="a" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#ccc" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<g stroke="#d5d5d5">
|
||||
<rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="${labelWidth}" height="${internalHeight}" rx="2"/>
|
||||
${hasMessage ? renderMessageBubble() : ''}
|
||||
</g>
|
||||
${renderedLogo}
|
||||
<g fill="#333" text-anchor="middle" ${socialFontFamily} text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px">
|
||||
<text x="${labelTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>
|
||||
<text x="${labelTextX}" y="140" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>
|
||||
${hasMessage ? renderMessageText() : ''}
|
||||
</g>
|
||||
${renderLeftLink()}
|
||||
`
|
||||
)
|
||||
|
||||
if (minify) {
|
||||
return stripXmlWhitespace(badge)
|
||||
}
|
||||
return badge
|
||||
}
|
||||
|
||||
function forTheBadge({
|
||||
label,
|
||||
message,
|
||||
links,
|
||||
logo,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
color = '#4c1',
|
||||
labelColor,
|
||||
minify,
|
||||
}) {
|
||||
// For the Badge is styled in all caps. Convert to caps here so widths can
|
||||
// be measured using the correct characters.
|
||||
label = label.toUpperCase()
|
||||
message = message.toUpperCase()
|
||||
|
||||
let { labelWidth, messageWidth } = computeWidths({ label, message })
|
||||
const height = 28
|
||||
const hasLabel = label.length || labelColor
|
||||
if (labelColor == null) {
|
||||
labelColor = '#555'
|
||||
}
|
||||
const horizPadding = 9
|
||||
const { hasLogo, totalLogoWidth, renderedLogo } = renderLogo({
|
||||
logo,
|
||||
badgeHeight: height,
|
||||
horizPadding,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
})
|
||||
|
||||
labelWidth += 10 + totalLogoWidth
|
||||
if (label.length) {
|
||||
labelWidth += 10 + label.length * 1.5
|
||||
} else if (hasLogo) {
|
||||
if (hasLabel) {
|
||||
labelWidth += 7
|
||||
} else {
|
||||
labelWidth -= 7
|
||||
}
|
||||
} else {
|
||||
labelWidth -= 11
|
||||
}
|
||||
|
||||
messageWidth += 10
|
||||
messageWidth += 10 + message.length * 2
|
||||
const leftWidth = hasLogo && !hasLabel ? 0 : labelWidth
|
||||
const rightWidth =
|
||||
hasLogo && !hasLabel ? messageWidth + labelWidth : messageWidth
|
||||
|
||||
labelColor = hasLabel || hasLogo ? labelColor : color
|
||||
|
||||
color = escapeXml(color)
|
||||
labelColor = escapeXml(labelColor)
|
||||
|
||||
function renderLabelText() {
|
||||
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
|
||||
const labelTextLength = (labelWidth - (24 + totalLogoWidth)) * 10
|
||||
const escapedLabel = escapeXml(label)
|
||||
return `
|
||||
<text x="${labelTextX}" y="175" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>
|
||||
`
|
||||
}
|
||||
|
||||
const badge = renderBadge(
|
||||
{
|
||||
links,
|
||||
leftWidth,
|
||||
rightWidth,
|
||||
height,
|
||||
},
|
||||
`
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="${leftWidth}" height="${height}" fill="${labelColor}"/>
|
||||
<rect x="${leftWidth}" width="${rightWidth}" height="${height}" fill="${color}"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" ${fontFamily} text-rendering="geometricPrecision" font-size="100">
|
||||
${renderedLogo}
|
||||
${hasLabel ? renderLabelText() : ''}
|
||||
<text x="${
|
||||
(labelWidth + messageWidth / 2) * 10
|
||||
}" y="175" font-weight="bold" transform="scale(.1)" textLength="${
|
||||
(messageWidth - 24) * 10
|
||||
}">
|
||||
${escapeXml(message)}</text>
|
||||
</g>`
|
||||
)
|
||||
|
||||
if (minify) {
|
||||
return stripXmlWhitespace(badge)
|
||||
}
|
||||
return badge
|
||||
}
|
||||
|
||||
module.exports = { plastic, flat, flatSquare, social, forTheBadge }
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const isCSSColor = require('is-css-color')
|
||||
|
||||
// When updating these, be sure also to update the list in `gh-badges/README.md`.
|
||||
// When updating these, be sure also to update the list in `badge-maker/README.md`.
|
||||
const namedColors = {
|
||||
brightgreen: '#4c1',
|
||||
green: '#97ca00',
|
||||
87
badge-maker/lib/index.js
Normal file
87
badge-maker/lib/index.js
Normal file
@@ -0,0 +1,87 @@
|
||||
'use strict'
|
||||
/**
|
||||
* @module badge-maker
|
||||
*/
|
||||
|
||||
const _makeBadge = require('./make-badge')
|
||||
|
||||
class ValidationError extends Error {}
|
||||
|
||||
function _validate(format) {
|
||||
if (format !== Object(format)) {
|
||||
throw new ValidationError('makeBadge takes an argument of type object')
|
||||
}
|
||||
|
||||
if (!('message' in format)) {
|
||||
throw new ValidationError('Field `message` is required')
|
||||
}
|
||||
|
||||
const stringFields = ['labelColor', 'color', 'message', 'label']
|
||||
stringFields.forEach(function (field) {
|
||||
if (field in format && typeof format[field] !== 'string') {
|
||||
throw new ValidationError(`Field \`${field}\` must be of type string`)
|
||||
}
|
||||
})
|
||||
|
||||
const styleValues = [
|
||||
'plastic',
|
||||
'flat',
|
||||
'flat-square',
|
||||
'for-the-badge',
|
||||
'social',
|
||||
]
|
||||
if ('style' in format && !styleValues.includes(format.style)) {
|
||||
throw new ValidationError(
|
||||
`Field \`style\` must be one of (${styleValues.toString()})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function _clean(format) {
|
||||
const expectedKeys = ['label', 'message', 'labelColor', 'color', 'style']
|
||||
|
||||
const cleaned = {}
|
||||
Object.keys(format).forEach(key => {
|
||||
if (format[key] != null && expectedKeys.includes(key)) {
|
||||
cleaned[key] = format[key]
|
||||
} else {
|
||||
throw new ValidationError(
|
||||
`Unexpected field '${key}'. Allowed values are (${expectedKeys.toString()})`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// convert "public" format to "internal" format
|
||||
cleaned.text = [cleaned.label || '', cleaned.message]
|
||||
delete cleaned.label
|
||||
delete cleaned.message
|
||||
if ('style' in cleaned) {
|
||||
cleaned.template = cleaned.style
|
||||
delete cleaned.style
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a badge
|
||||
*
|
||||
* @param {object} format Object specifying badge data
|
||||
* @param {string} format.label (Optional) Badge label (e.g: 'build')
|
||||
* @param {string} format.message (Required) Badge message (e.g: 'passing')
|
||||
* @param {string} format.labelColor (Optional) Label color
|
||||
* @param {string} format.color (Optional) Message color
|
||||
* @param {string} format.style (Optional) Visual style e.g: 'flat'
|
||||
* @returns {string} Badge in SVG format
|
||||
* @see https://github.com/badges/shields/tree/master/badge-maker/README.md
|
||||
*/
|
||||
function makeBadge(format) {
|
||||
_validate(format)
|
||||
const cleanedFormat = _clean(format)
|
||||
return _makeBadge(cleanedFormat)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makeBadge,
|
||||
ValidationError,
|
||||
}
|
||||
75
badge-maker/lib/index.spec.js
Normal file
75
badge-maker/lib/index.spec.js
Normal file
@@ -0,0 +1,75 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const isSvg = require('is-svg')
|
||||
const { makeBadge, ValidationError } = require('.')
|
||||
|
||||
describe('makeBadge function', function () {
|
||||
it('should produce badge with valid input', function () {
|
||||
expect(
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
})
|
||||
).to.satisfy(isSvg)
|
||||
expect(
|
||||
makeBadge({
|
||||
message: 'passed',
|
||||
})
|
||||
).to.satisfy(isSvg)
|
||||
expect(
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
color: 'green',
|
||||
style: 'flat',
|
||||
})
|
||||
).to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
it('should throw a ValidationError with invalid inputs', function () {
|
||||
;[null, undefined, 7, 'foo', 4.25].forEach(x => {
|
||||
console.log(x)
|
||||
expect(() => makeBadge(x)).to.throw(
|
||||
ValidationError,
|
||||
'makeBadge takes an argument of type object'
|
||||
)
|
||||
})
|
||||
expect(() => makeBadge({})).to.throw(
|
||||
ValidationError,
|
||||
'Field `message` is required'
|
||||
)
|
||||
expect(() => makeBadge({ label: 'build' })).to.throw(
|
||||
ValidationError,
|
||||
'Field `message` is required'
|
||||
)
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', labelColor: 7 })
|
||||
).to.throw(ValidationError, 'Field `labelColor` must be of type string')
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', format: 'png' })
|
||||
).to.throw(ValidationError, "Unexpected field 'format'")
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', template: 'flat' })
|
||||
).to.throw(ValidationError, "Unexpected field 'template'")
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', foo: 'bar' })
|
||||
).to.throw(ValidationError, "Unexpected field 'foo'")
|
||||
expect(() =>
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
style: 'something else',
|
||||
})
|
||||
).to.throw(
|
||||
ValidationError,
|
||||
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)'
|
||||
)
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', style: 'popout' })
|
||||
).to.throw(
|
||||
ValidationError,
|
||||
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)'
|
||||
)
|
||||
})
|
||||
})
|
||||
64
badge-maker/lib/make-badge.js
Normal file
64
badge-maker/lib/make-badge.js
Normal file
@@ -0,0 +1,64 @@
|
||||
'use strict'
|
||||
|
||||
const camelcase = require('camelcase')
|
||||
const { normalizeColor, toSvgColor } = require('./color')
|
||||
const badgeRenderers = require('./badge-renderers')
|
||||
|
||||
/*
|
||||
note: makeBadge() is fairly thinly wrapped so if we are making changes here
|
||||
it is likely this will impact on the package's public interface in index.js
|
||||
*/
|
||||
module.exports = function makeBadge({
|
||||
format,
|
||||
template = 'flat',
|
||||
text,
|
||||
color,
|
||||
labelColor,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
links = ['', ''],
|
||||
}) {
|
||||
// String coercion and whitespace removal.
|
||||
text = text.map(value => `${value}`.trim())
|
||||
|
||||
const [label, message] = text
|
||||
|
||||
color = normalizeColor(color)
|
||||
labelColor = normalizeColor(labelColor)
|
||||
|
||||
// This ought to be the responsibility of the server, not `makeBadge`.
|
||||
if (format === 'json') {
|
||||
return JSON.stringify({
|
||||
label,
|
||||
message,
|
||||
logoWidth,
|
||||
color,
|
||||
labelColor,
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
})
|
||||
}
|
||||
|
||||
const methodName = camelcase(template)
|
||||
if (!(methodName in badgeRenderers)) {
|
||||
throw new Error(`Unknown template: '${template}'`)
|
||||
}
|
||||
const render = badgeRenderers[methodName]
|
||||
|
||||
logoWidth = +logoWidth || (logo ? 14 : 0)
|
||||
|
||||
return render({
|
||||
label,
|
||||
message,
|
||||
links,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
logoPadding: logo && label.length ? 3 : 0,
|
||||
color: toSvgColor(color),
|
||||
labelColor: toSvgColor(labelColor),
|
||||
minify: true,
|
||||
})
|
||||
}
|
||||
563
badge-maker/lib/make-badge.spec.js
Normal file
563
badge-maker/lib/make-badge.spec.js
Normal file
@@ -0,0 +1,563 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const isSvg = require('is-svg')
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
function testColor(color = '', colorAttr = 'color') {
|
||||
return JSON.parse(
|
||||
makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
[colorAttr]: color,
|
||||
format: 'json',
|
||||
})
|
||||
).color
|
||||
}
|
||||
|
||||
describe('The badge generator', function () {
|
||||
describe('color test', function () {
|
||||
test(testColor, () => {
|
||||
// valid hex
|
||||
forCases([
|
||||
given('#4c1'),
|
||||
given('#4C1'),
|
||||
given('4C1'),
|
||||
given('4c1'),
|
||||
]).expect('#4c1')
|
||||
forCases([
|
||||
given('#abc123'),
|
||||
given('#ABC123'),
|
||||
given('abc123'),
|
||||
given('ABC123'),
|
||||
]).expect('#abc123')
|
||||
// valid rgb(a)
|
||||
given('rgb(0,128,255)').expect('rgb(0,128,255)')
|
||||
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
|
||||
// valid hsl(a)
|
||||
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
|
||||
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
|
||||
// CSS named color.
|
||||
given('papayawhip').expect('papayawhip')
|
||||
// Shields named color.
|
||||
given('red').expect('red')
|
||||
given('green').expect('green')
|
||||
given('blue').expect('blue')
|
||||
given('yellow').expect('yellow')
|
||||
// Semantic color alias
|
||||
given('success').expect('brightgreen')
|
||||
given('informational').expect('blue')
|
||||
|
||||
forCases(
|
||||
// invalid hex
|
||||
given('#123red'), // contains letter above F
|
||||
given('#red'), // contains letter above F
|
||||
// invalid rgb(a)
|
||||
given('rgb(220,128,255,0.5)'), // has alpha
|
||||
given('rgba(0,0,255)'), // no alpha
|
||||
// invalid hsl(a)
|
||||
given('hsl(360,50%,50%,0.5)'), // has alpha
|
||||
given('hsla(0,50%,101%)'), // no alpha
|
||||
// neither a css named color nor colorscheme
|
||||
given('notacolor'),
|
||||
given('bluish'),
|
||||
given('almostred'),
|
||||
given('brightmaroon'),
|
||||
given('cactus')
|
||||
).expect(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('color aliases', function () {
|
||||
test(testColor, () => {
|
||||
forCases([given('#4c1', 'color')]).expect('#4c1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SVG', function () {
|
||||
it('should produce SVG', function () {
|
||||
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||
expect(svg)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cactus')
|
||||
.and.to.include('grown')
|
||||
})
|
||||
|
||||
it('should match snapshot', function () {
|
||||
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||
snapshot(svg)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', function () {
|
||||
it('should produce the expected JSON', function () {
|
||||
const json = makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'json',
|
||||
links: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
expect(JSON.parse(json)).to.deep.equal({
|
||||
name: 'cactus',
|
||||
label: 'cactus',
|
||||
value: 'grown',
|
||||
message: 'grown',
|
||||
link: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace undefined svg template with "flat"', function () {
|
||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
})
|
||||
const jsonBadgeWithDefaultStyle = makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
})
|
||||
expect(jsonBadgeWithUnknownStyle)
|
||||
.to.equal(jsonBadgeWithDefaultStyle)
|
||||
.and.to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
it('should fail with unknown svg template', function () {
|
||||
expect(() =>
|
||||
makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
template: 'unknown_style',
|
||||
})
|
||||
).to.throw(Error, "Unknown template: 'unknown_style'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('"flat" template badge generation', function () {
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#b3e',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"flat-square" template badge generation', function () {
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
color: '#b3e',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"plastic" template badge generation', function () {
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'plastic',
|
||||
color: '#b3e',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'plastic',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"for-the-badge" template badge generation', function () {
|
||||
// https://github.com/badges/shields/issues/1280
|
||||
it('numbers should produce a string', function () {
|
||||
const svg = makeBadge({
|
||||
text: [1998, 1999],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
})
|
||||
expect(svg).to.include('1998').and.to.include('1999')
|
||||
})
|
||||
|
||||
it('lowercase/mixedcase string should produce uppercase string', function () {
|
||||
const svg = makeBadge({
|
||||
text: ['Label', '1 string'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
})
|
||||
expect(svg).to.include('LABEL').and.to.include('1 STRING')
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"social" template badge generation', function () {
|
||||
it('should produce capitalized string for badge key', function () {
|
||||
const svg = makeBadge({
|
||||
text: ['some-key', 'some-value'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
})
|
||||
expect(svg).to.include('Some-key').and.to.include('some-value')
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/issues/1606
|
||||
it('should handle empty strings used as badge keys', function () {
|
||||
const svg = makeBadge({
|
||||
text: ['', 'some-value'],
|
||||
format: 'json',
|
||||
template: 'social',
|
||||
})
|
||||
expect(svg).to.include('""').and.to.include('some-value')
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
color: '#b3e',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
color: '#b3e',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
links: ['https://shields.io/', 'https://www.google.co.uk/'],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('badges with logos should always produce the same badge', function () {
|
||||
it('badge with logo', function () {
|
||||
const svg = makeBadge({
|
||||
text: ['label', 'message'],
|
||||
format: 'svg',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
})
|
||||
snapshot(svg)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gh-badges",
|
||||
"version": "2.2.1",
|
||||
"name": "badge-maker",
|
||||
"version": "3.0.0",
|
||||
"description": "Shields.io badge library",
|
||||
"keywords": [
|
||||
"GitHub",
|
||||
@@ -13,7 +13,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/badges/shields.git",
|
||||
"directory": "gh-badges"
|
||||
"directory": "badge-maker"
|
||||
},
|
||||
"author": "Thaddée Tyl <thaddee.tyl@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"badge": "lib/badge-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8",
|
||||
"node": ">= 10",
|
||||
"npm": ">= 5"
|
||||
},
|
||||
"collective": {
|
||||
@@ -35,10 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"anafanafo": "^1.0.0",
|
||||
"dot": "^1.1.2",
|
||||
"gm": "^1.23.0",
|
||||
"is-css-color": "^1.0.0",
|
||||
"svgo": "^1.1.1"
|
||||
"is-css-color": "^1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo 'Run tests from parent dir'; false"
|
||||
@@ -6,6 +6,15 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: 'METRICS_PROMETHEUS_ENABLED'
|
||||
endpointEnabled: 'METRICS_PROMETHEUS_ENDPOINT_ENABLED'
|
||||
influx:
|
||||
enabled: 'METRICS_INFLUX_ENABLED'
|
||||
url: 'METRICS_INFLUX_URL'
|
||||
timeoutMilliseconds: 'METRICS_INFLUX_TIMEOUT_MILLISECONDS'
|
||||
intervalSeconds: 'METRICS_INFLUX_INTERVAL_SECONDS'
|
||||
instanceIdFrom: 'METRICS_INFLUX_INSTANCE_ID_FROM'
|
||||
instanceIdEnvVarName: 'METRICS_INFLUX_INSTANCE_ID_ENV_VAR_NAME'
|
||||
envLabel: 'METRICS_INFLUX_ENV_LABEL'
|
||||
|
||||
ssl:
|
||||
isSecure: 'HTTPS'
|
||||
@@ -55,6 +64,8 @@ public:
|
||||
|
||||
fetchLimit: 'FETCH_LIMIT'
|
||||
|
||||
shieldsProductionHerokuHacks: 'SHIELDS_PRODUCTION_HEROKU_HACKS'
|
||||
|
||||
private:
|
||||
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
|
||||
bintray_user: 'BINTRAY_USER'
|
||||
@@ -85,3 +96,5 @@ private:
|
||||
twitch_client_id: 'TWITCH_CLIENT_ID'
|
||||
twitch_client_secret: 'TWITCH_CLIENT_SECRET'
|
||||
wheelmap_token: 'WHEELMAP_TOKEN'
|
||||
influx_username: 'INFLUX_USERNAME'
|
||||
influx_password: 'INFLUX_PASSWORD'
|
||||
|
||||
@@ -5,7 +5,11 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
|
||||
endpointEnabled: false
|
||||
influx:
|
||||
enabled: false
|
||||
timeoutMilliseconds: 1000
|
||||
intervalSeconds: 15
|
||||
ssl:
|
||||
isSecure: false
|
||||
|
||||
@@ -32,4 +36,6 @@ public:
|
||||
|
||||
fetchLimit: '10MB'
|
||||
|
||||
shieldsProductionHerokuHacks: false
|
||||
|
||||
private: {}
|
||||
|
||||
@@ -2,6 +2,12 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: true
|
||||
influx:
|
||||
enabled: true
|
||||
url: https://metrics.shields.io/telegraf
|
||||
instanceIdFrom: env-var
|
||||
instanceIdEnvVarName: HEROKU_DYNO_ID
|
||||
envLabel: shields-production
|
||||
|
||||
ssl:
|
||||
isSecure: true
|
||||
|
||||
4
core/badge-urls/make-badge-url.d.ts
vendored
4
core/badge-urls/make-badge-url.d.ts
vendored
@@ -38,18 +38,22 @@ export function staticBadgeUrl({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
labelColor,
|
||||
color,
|
||||
style,
|
||||
namedLogo,
|
||||
format,
|
||||
links,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
label: string
|
||||
message: string
|
||||
labelColor?: string
|
||||
color?: string
|
||||
style?: string
|
||||
namedLogo?: string
|
||||
format?: string
|
||||
links?: string[]
|
||||
}): string
|
||||
|
||||
export function queryStringStaticBadgeUrl({
|
||||
|
||||
@@ -59,15 +59,19 @@ function staticBadgeUrl({
|
||||
baseUrl = '',
|
||||
label,
|
||||
message,
|
||||
labelColor,
|
||||
color = 'lightgray',
|
||||
style,
|
||||
namedLogo,
|
||||
format = '',
|
||||
links = [],
|
||||
}) {
|
||||
const path = [label, message, color].map(encodeField).join('-')
|
||||
const outQueryString = queryString.stringify({
|
||||
labelColor,
|
||||
style,
|
||||
logo: namedLogo,
|
||||
link: links,
|
||||
})
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
const suffix = outQueryString ? `?${outQueryString}` : ''
|
||||
|
||||
@@ -10,7 +10,7 @@ const {
|
||||
dynamicBadgeUrl,
|
||||
} = require('./make-badge-url')
|
||||
|
||||
describe('Badge URL generation functions', function() {
|
||||
describe('Badge URL generation functions', function () {
|
||||
test(badgeUrlFromPath, () => {
|
||||
given({
|
||||
baseUrl: 'http://example.com',
|
||||
|
||||
@@ -5,20 +5,20 @@ const { test, given, forCases } = require('sazerac')
|
||||
const { AuthHelper } = require('./auth-helper')
|
||||
const { InvalidParameter } = require('./errors')
|
||||
|
||||
describe('AuthHelper', function() {
|
||||
describe('constructor checks', function() {
|
||||
it('throws without userKey or passKey', function() {
|
||||
describe('AuthHelper', function () {
|
||||
describe('constructor checks', function () {
|
||||
it('throws without userKey or passKey', function () {
|
||||
expect(() => new AuthHelper({}, {})).to.throw(
|
||||
Error,
|
||||
'Expected userKey or passKey to be set'
|
||||
)
|
||||
})
|
||||
it('throws without serviceKey or authorizedOrigins', function() {
|
||||
it('throws without serviceKey or authorizedOrigins', function () {
|
||||
expect(
|
||||
() => new AuthHelper({ userKey: 'myci_user', passKey: 'myci_pass' }, {})
|
||||
).to.throw(Error, 'Expected authorizedOrigins or serviceKey to be set')
|
||||
})
|
||||
it('throws when authorizedOrigins is not an array', function() {
|
||||
it('throws when authorizedOrigins is not an array', function () {
|
||||
expect(
|
||||
() =>
|
||||
new AuthHelper(
|
||||
@@ -33,7 +33,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValid', function() {
|
||||
describe('isValid', function () {
|
||||
function validate(config, privateConfig) {
|
||||
return new AuthHelper(
|
||||
{ authorizedOrigins: ['https://example.test'], ...config },
|
||||
@@ -89,7 +89,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('_basicAuth', function() {
|
||||
describe('_basicAuth', function () {
|
||||
function validate(config, privateConfig) {
|
||||
return new AuthHelper(
|
||||
{ authorizedOrigins: ['https://example.test'], ...config },
|
||||
@@ -128,7 +128,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('_isInsecureSslRequest', function() {
|
||||
describe('_isInsecureSslRequest', function () {
|
||||
test(AuthHelper._isInsecureSslRequest, () => {
|
||||
forCases([
|
||||
given({ url: 'http://example.test' }),
|
||||
@@ -146,31 +146,31 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('enforceStrictSsl', function() {
|
||||
describe('enforceStrictSsl', function () {
|
||||
const authConfig = {
|
||||
userKey: 'myci_user',
|
||||
passKey: 'myci_pass',
|
||||
serviceKey: 'myci',
|
||||
}
|
||||
|
||||
context('by default', function() {
|
||||
context('by default', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: { myci: { authorizedOrigins: ['http://myci.test'] } },
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
})
|
||||
it('does not throw for secure requests', function() {
|
||||
it('does not throw for secure requests', function () {
|
||||
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
|
||||
})
|
||||
it('throws for insecure requests', function() {
|
||||
it('throws for insecure requests', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
})
|
||||
|
||||
context("when strict SSL isn't required", function() {
|
||||
context("when strict SSL isn't required", function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
@@ -182,10 +182,10 @@ describe('AuthHelper', function() {
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
})
|
||||
it('does not throw for secure requests', function() {
|
||||
it('does not throw for secure requests', function () {
|
||||
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
|
||||
})
|
||||
it('does not throw for insecure requests', function() {
|
||||
it('does not throw for insecure requests', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
|
||||
).not.to.throw()
|
||||
@@ -193,14 +193,14 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldAuthenticateRequest', function() {
|
||||
describe('shouldAuthenticateRequest', function () {
|
||||
const authConfig = {
|
||||
userKey: 'myci_user',
|
||||
passKey: 'myci_pass',
|
||||
serviceKey: 'myci',
|
||||
}
|
||||
|
||||
context('by default', function() {
|
||||
context('by default', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
@@ -213,12 +213,12 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
const shouldAuthenticateRequest = requestOptions =>
|
||||
authHelper.shouldAuthenticateRequest(requestOptions)
|
||||
describe('a secure request to an authorized origin', function() {
|
||||
describe('a secure request to an authorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({ url: 'https://myci.test/api' }).expect(true)
|
||||
})
|
||||
})
|
||||
describe('an insecure request', function() {
|
||||
describe('an insecure request', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({
|
||||
url: 'https://myci.test/api',
|
||||
@@ -226,7 +226,7 @@ describe('AuthHelper', function() {
|
||||
}).expect(false)
|
||||
})
|
||||
})
|
||||
describe('a request to an unauthorized origin', function() {
|
||||
describe('a request to an unauthorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
forCases([
|
||||
given({ url: 'http://myci.test/api' }),
|
||||
@@ -237,7 +237,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when auth over insecure SSL is allowed', function() {
|
||||
context('when auth over insecure SSL is allowed', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
@@ -251,12 +251,12 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
const shouldAuthenticateRequest = requestOptions =>
|
||||
authHelper.shouldAuthenticateRequest(requestOptions)
|
||||
describe('a secure request to an authorized origin', function() {
|
||||
describe('a secure request to an authorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({ url: 'https://myci.test' }).expect(true)
|
||||
})
|
||||
})
|
||||
describe('an insecure request', function() {
|
||||
describe('an insecure request', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({
|
||||
url: 'https://myci.test',
|
||||
@@ -264,7 +264,7 @@ describe('AuthHelper', function() {
|
||||
}).expect(true)
|
||||
})
|
||||
})
|
||||
describe('a request to an unauthorized origin', function() {
|
||||
describe('a request to an unauthorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
forCases([
|
||||
given({ url: 'http://myci.test' }),
|
||||
@@ -275,7 +275,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the service is partly configured', function() {
|
||||
context('when the service is partly configured', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
@@ -289,7 +289,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
const shouldAuthenticateRequest = requestOptions =>
|
||||
authHelper.shouldAuthenticateRequest(requestOptions)
|
||||
describe('a secure request to an authorized origin', function() {
|
||||
describe('a secure request to an authorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({ url: 'https://myci.test' }).expect(false)
|
||||
})
|
||||
@@ -297,7 +297,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('withBasicAuth', function() {
|
||||
describe('withBasicAuth', function () {
|
||||
const authHelper = new AuthHelper(
|
||||
{
|
||||
userKey: 'myci_user',
|
||||
@@ -318,7 +318,7 @@ describe('AuthHelper', function() {
|
||||
const withBasicAuth = requestOptions =>
|
||||
authHelper.withBasicAuth(requestOptions)
|
||||
|
||||
describe('authenticates a secure request to an authorized origin', function() {
|
||||
describe('authenticates a secure request to an authorized origin', function () {
|
||||
test(withBasicAuth, () => {
|
||||
given({
|
||||
url: 'https://myci.test/api',
|
||||
@@ -343,7 +343,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('does not authenticate a request to an unauthorized origin', function() {
|
||||
describe('does not authenticate a request to an unauthorized origin', function () {
|
||||
test(withBasicAuth, () => {
|
||||
given({
|
||||
url: 'https://other.test/api',
|
||||
@@ -364,7 +364,7 @@ describe('AuthHelper', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('throws on an insecure SSL request', function() {
|
||||
describe('throws on an insecure SSL request', function () {
|
||||
expect(() =>
|
||||
withBasicAuth({
|
||||
url: 'https://myci.test/api',
|
||||
|
||||
@@ -36,10 +36,10 @@ class DummyGraphqlService extends BaseGraphqlService {
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseGraphqlService', function() {
|
||||
describe('Making requests', function() {
|
||||
describe('BaseGraphqlService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '{"some": "json"}',
|
||||
@@ -48,7 +48,7 @@ describe('BaseGraphqlService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -64,7 +64,7 @@ describe('BaseGraphqlService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function() {
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
class WithOptions extends DummyGraphqlService {
|
||||
async handle() {
|
||||
const { value } = await this._requestGraphql({
|
||||
@@ -98,8 +98,8 @@ describe('BaseGraphqlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Making badges', function() {
|
||||
it('handles valid json responses', async function() {
|
||||
describe('Making badges', function () {
|
||||
it('handles valid json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '{"requiredString": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
@@ -114,7 +114,7 @@ describe('BaseGraphqlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles json responses which do not match the schema', async function() {
|
||||
it('handles json responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '{"unexpectedKey": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
@@ -131,7 +131,7 @@ describe('BaseGraphqlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unparseable json responses', async function() {
|
||||
it('handles unparseable json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'not json',
|
||||
res: { statusCode: 200 },
|
||||
@@ -149,8 +149,8 @@ describe('BaseGraphqlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling', function() {
|
||||
it('handles generic error', async function() {
|
||||
describe('Error handling', function () {
|
||||
it('handles generic error', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }',
|
||||
res: { statusCode: 200 },
|
||||
@@ -167,7 +167,7 @@ describe('BaseGraphqlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles custom error', async function() {
|
||||
it('handles custom error', async function () {
|
||||
class WithErrorHandler extends DummyGraphqlService {
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestGraphql({
|
||||
@@ -178,7 +178,7 @@ describe('BaseGraphqlService', function() {
|
||||
requiredString
|
||||
}
|
||||
`,
|
||||
transformErrors: function(errors) {
|
||||
transformErrors: function (errors) {
|
||||
if (errors[0].message === 'oh noes!!') {
|
||||
return new InvalidResponse({
|
||||
prettyMessage: 'a terrible thing has happened',
|
||||
|
||||
@@ -29,10 +29,10 @@ class DummyJsonService extends BaseJsonService {
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseJsonService', function() {
|
||||
describe('Making requests', function() {
|
||||
describe('BaseJsonService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '{"some": "json"}',
|
||||
@@ -41,7 +41,7 @@ describe('BaseJsonService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
await DummyJsonService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -55,7 +55,7 @@ describe('BaseJsonService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function() {
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
class WithOptions extends DummyJsonService {
|
||||
async handle() {
|
||||
const { value } = await this._requestJson({
|
||||
@@ -83,8 +83,8 @@ describe('BaseJsonService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Making badges', function() {
|
||||
it('handles valid json responses', async function() {
|
||||
describe('Making badges', function () {
|
||||
it('handles valid json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '{"requiredString": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
@@ -99,7 +99,7 @@ describe('BaseJsonService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles json responses which do not match the schema', async function() {
|
||||
it('handles json responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '{"unexpectedKey": "some-string"}',
|
||||
res: { statusCode: 200 },
|
||||
@@ -116,7 +116,7 @@ describe('BaseJsonService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unparseable json responses', async function() {
|
||||
it('handles unparseable json responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'not json',
|
||||
res: { statusCode: 200 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const BaseService = require('./base')
|
||||
const { MetricHelper } = require('./metric-helper')
|
||||
const { setCacheHeaders } = require('./cache-headers')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const BaseService = require('./base')
|
||||
const {
|
||||
serverHasBeenUpSinceResourceCached,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const Joi = require('@hapi/joi')
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const BaseSvgScrapingService = require('./base-svg-scraping')
|
||||
|
||||
function makeExampleSvg({ label, message }) {
|
||||
@@ -33,7 +33,7 @@ class DummySvgScrapingService extends BaseSvgScrapingService {
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseSvgScrapingService', function() {
|
||||
describe('BaseSvgScrapingService', function () {
|
||||
const exampleLabel = 'this is the label'
|
||||
const exampleMessage = 'this is the result!'
|
||||
const exampleSvg = makeExampleSvg({
|
||||
@@ -41,17 +41,17 @@ describe('BaseSvgScrapingService', function() {
|
||||
message: exampleMessage,
|
||||
})
|
||||
|
||||
describe('valueFromSvgBadge', function() {
|
||||
it('should find the correct value', function() {
|
||||
describe('valueFromSvgBadge', function () {
|
||||
it('should find the correct value', function () {
|
||||
expect(BaseSvgScrapingService.valueFromSvgBadge(exampleSvg)).to.equal(
|
||||
exampleMessage
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Making requests', function() {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: exampleSvg,
|
||||
@@ -60,7 +60,7 @@ describe('BaseSvgScrapingService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest with the expected header', async function() {
|
||||
it('invokes _sendAndCacheRequest with the expected header', async function () {
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -74,7 +74,7 @@ describe('BaseSvgScrapingService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function() {
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
class WithCustomOptions extends DummySvgScrapingService {
|
||||
async handle() {
|
||||
const { message } = await this._requestSvg({
|
||||
@@ -105,8 +105,8 @@ describe('BaseSvgScrapingService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Making badges', function() {
|
||||
it('handles valid svg responses', async function() {
|
||||
describe('Making badges', function () {
|
||||
it('handles valid svg responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: exampleSvg,
|
||||
res: { statusCode: 200 },
|
||||
@@ -121,7 +121,7 @@ describe('BaseSvgScrapingService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('allows overriding the valueMatcher', async function() {
|
||||
it('allows overriding the valueMatcher', async function () {
|
||||
class WithValueMatcher extends BaseSvgScrapingService {
|
||||
static get route() {
|
||||
return {}
|
||||
@@ -149,7 +149,7 @@ describe('BaseSvgScrapingService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unparseable svg responses', async function() {
|
||||
it('handles unparseable svg responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'not svg yo',
|
||||
res: { statusCode: 200 },
|
||||
|
||||
@@ -29,10 +29,10 @@ class DummyXmlService extends BaseXmlService {
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseXmlService', function() {
|
||||
describe('Making requests', function() {
|
||||
describe('BaseXmlService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: '<requiredString>some-string</requiredString>',
|
||||
@@ -41,7 +41,7 @@ describe('BaseXmlService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
await DummyXmlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -55,7 +55,7 @@ describe('BaseXmlService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function() {
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
class WithCustomOptions extends BaseXmlService {
|
||||
static get route() {
|
||||
return {}
|
||||
@@ -87,8 +87,8 @@ describe('BaseXmlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Making badges', function() {
|
||||
it('handles valid xml responses', async function() {
|
||||
describe('Making badges', function () {
|
||||
it('handles valid xml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '<requiredString>some-string</requiredString>',
|
||||
res: { statusCode: 200 },
|
||||
@@ -103,7 +103,7 @@ describe('BaseXmlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('parses XML response with custom parser options', async function() {
|
||||
it('parses XML response with custom parser options', async function () {
|
||||
const customParserOption = { trimValues: false }
|
||||
class DummyXmlServiceWithParserOption extends DummyXmlService {
|
||||
async handle() {
|
||||
@@ -130,7 +130,7 @@ describe('BaseXmlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles xml responses which do not match the schema', async function() {
|
||||
it('handles xml responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '<unexpectedAttribute>some-string</unexpectedAttribute>',
|
||||
res: { statusCode: 200 },
|
||||
@@ -147,7 +147,7 @@ describe('BaseXmlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unparseable xml responses', async function() {
|
||||
it('handles unparseable xml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'not xml',
|
||||
res: { statusCode: 200 },
|
||||
|
||||
@@ -45,10 +45,10 @@ foo: bar
|
||||
foo: baz
|
||||
`
|
||||
|
||||
describe('BaseYamlService', function() {
|
||||
describe('Making requests', function() {
|
||||
describe('BaseYamlService', function () {
|
||||
describe('Making requests', function () {
|
||||
let sendAndCacheRequest
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sendAndCacheRequest = sinon.stub().returns(
|
||||
Promise.resolve({
|
||||
buffer: expectedYaml,
|
||||
@@ -57,7 +57,7 @@ describe('BaseYamlService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
await DummyYamlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -74,7 +74,7 @@ describe('BaseYamlService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function() {
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
class WithOptions extends DummyYamlService {
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestYaml({
|
||||
@@ -105,8 +105,8 @@ describe('BaseYamlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Making badges', function() {
|
||||
it('handles valid yaml responses', async function() {
|
||||
describe('Making badges', function () {
|
||||
it('handles valid yaml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: expectedYaml,
|
||||
res: { statusCode: 200 },
|
||||
@@ -121,7 +121,7 @@ describe('BaseYamlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles yaml responses which do not match the schema', async function() {
|
||||
it('handles yaml responses which do not match the schema', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: unexpectedYaml,
|
||||
res: { statusCode: 200 },
|
||||
@@ -138,7 +138,7 @@ describe('BaseYamlService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unparseable yaml responses', async function() {
|
||||
it('handles unparseable yaml responses', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: invalidYaml,
|
||||
res: { statusCode: 200 },
|
||||
|
||||
@@ -58,10 +58,7 @@ const serviceDataSchema = Joi.object({
|
||||
// `render()` to always return a string.
|
||||
message: Joi.alternatives(Joi.string().allow(''), Joi.number()).required(),
|
||||
color: Joi.string(),
|
||||
link: Joi.array()
|
||||
.items(Joi.string().uri())
|
||||
.single()
|
||||
.max(2),
|
||||
link: Joi.array().items(Joi.string().uri()).single().max(2),
|
||||
// Generally services should not use these options, which are provided to
|
||||
// support the Endpoint badge.
|
||||
labelColor: Joi.string(),
|
||||
@@ -70,9 +67,7 @@ const serviceDataSchema = Joi.object({
|
||||
logoColor: optionalStringWhenNamedLogoPresent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
logoPosition: optionalNumberWhenAnyLogoPresent,
|
||||
cacheSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(0),
|
||||
cacheSeconds: Joi.number().integer().min(0),
|
||||
style: Joi.string(),
|
||||
})
|
||||
.oxor('namedLogo', 'logoSvg')
|
||||
|
||||
@@ -73,7 +73,7 @@ class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
|
||||
}
|
||||
}
|
||||
|
||||
describe('BaseService', function() {
|
||||
describe('BaseService', function () {
|
||||
const defaultConfig = {
|
||||
public: {
|
||||
handleInternalErrors: false,
|
||||
@@ -82,7 +82,7 @@ describe('BaseService', function() {
|
||||
private: {},
|
||||
}
|
||||
|
||||
it('Invokes the handler as expected', async function() {
|
||||
it('Invokes the handler as expected', async function () {
|
||||
expect(
|
||||
await DummyService.invoke(
|
||||
{},
|
||||
@@ -95,7 +95,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('Validates query params', async function() {
|
||||
it('Validates query params', async function () {
|
||||
expect(
|
||||
await DummyService.invoke(
|
||||
{},
|
||||
@@ -110,14 +110,14 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Required overrides', function() {
|
||||
it('Should throw if render() is not overridden', function() {
|
||||
describe('Required overrides', function () {
|
||||
it('Should throw if render() is not overridden', function () {
|
||||
expect(() => BaseService.render()).to.throw(
|
||||
/^render\(\) function not implemented for BaseService$/
|
||||
)
|
||||
})
|
||||
|
||||
it('Should throw if route is not overridden', function() {
|
||||
it('Should throw if route is not overridden', function () {
|
||||
return expect(BaseService.invoke({}, {}, {})).to.be.rejectedWith(
|
||||
/^Route not defined for BaseService$/
|
||||
)
|
||||
@@ -128,31 +128,31 @@ describe('BaseService', function() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
it('Should throw if handle() is not overridden', function() {
|
||||
it('Should throw if handle() is not overridden', function () {
|
||||
return expect(WithRoute.invoke({}, {}, {})).to.be.rejectedWith(
|
||||
/^Handler not implemented for WithRoute$/
|
||||
)
|
||||
})
|
||||
|
||||
it('Should throw if category is not overridden', function() {
|
||||
it('Should throw if category is not overridden', function () {
|
||||
expect(() => BaseService.category).to.throw(
|
||||
/^Category not set for BaseService$/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logging', function() {
|
||||
describe('Logging', function () {
|
||||
let sandbox
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
})
|
||||
it('Invokes the logger as expected', async function() {
|
||||
it('Invokes the logger as expected', async function () {
|
||||
await DummyService.invoke(
|
||||
{},
|
||||
defaultConfig,
|
||||
@@ -180,8 +180,8 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Service data validation', function() {
|
||||
it('Allows a link array', async function() {
|
||||
describe('Service data validation', function () {
|
||||
it('Allows a link array', async function () {
|
||||
const message = 'hello'
|
||||
const link = ['https://example.com/', 'https://other.example.com/']
|
||||
class LinkService extends DummyService {
|
||||
@@ -202,7 +202,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('On invalid data', function() {
|
||||
context('On invalid data', function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
return {
|
||||
@@ -211,7 +211,7 @@ describe('BaseService', function() {
|
||||
}
|
||||
}
|
||||
|
||||
it('Throws a validation error on invalid data', async function() {
|
||||
it('Throws a validation error on invalid data', async function () {
|
||||
try {
|
||||
await ThrowingService.invoke(
|
||||
{},
|
||||
@@ -229,7 +229,7 @@ describe('BaseService', function() {
|
||||
|
||||
// Ensure debuggabillity.
|
||||
// https://github.com/badges/shields/issues/3784
|
||||
it('Includes the service class in the stack trace', async function() {
|
||||
it('Includes the service class in the stack trace', async function () {
|
||||
try {
|
||||
await ThrowingService.invoke(
|
||||
{},
|
||||
@@ -244,8 +244,8 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling', function() {
|
||||
it('Handles internal errors', async function() {
|
||||
describe('Error handling', function () {
|
||||
it('Handles internal errors', async function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw Error("I've made a huge mistake")
|
||||
@@ -265,8 +265,8 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Handles known subtypes of ShieldsInternalError', function() {
|
||||
it('handles NotFound errors', async function() {
|
||||
describe('Handles known subtypes of ShieldsInternalError', function () {
|
||||
it('handles NotFound errors', async function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw new NotFound()
|
||||
@@ -281,7 +281,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles Inaccessible errors', async function() {
|
||||
it('handles Inaccessible errors', async function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw new Inaccessible()
|
||||
@@ -296,7 +296,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles InvalidResponse errors', async function() {
|
||||
it('handles InvalidResponse errors', async function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw new InvalidResponse()
|
||||
@@ -311,7 +311,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles Deprecated', async function() {
|
||||
it('handles Deprecated', async function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw new Deprecated()
|
||||
@@ -326,7 +326,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles InvalidParameter errors', async function() {
|
||||
it('handles InvalidParameter errors', async function () {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw new InvalidParameter()
|
||||
@@ -343,7 +343,7 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ScoutCamp integration', function() {
|
||||
describe('ScoutCamp integration', function () {
|
||||
// TODO Strangly, without the useless escape the regexes do not match in Node 12.
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const expectedRouteRegex = /^\/foo\/([^\/]+?)(|\.svg|\.json)$/
|
||||
@@ -351,7 +351,7 @@ describe('BaseService', function() {
|
||||
let mockCamp
|
||||
let mockHandleRequest
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
mockCamp = {
|
||||
route: sinon.spy(),
|
||||
}
|
||||
@@ -362,12 +362,12 @@ describe('BaseService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('registers the service', function() {
|
||||
it('registers the service', function () {
|
||||
expect(mockCamp.route).to.have.been.calledOnce
|
||||
expect(mockCamp.route).to.have.been.calledWith(expectedRouteRegex)
|
||||
})
|
||||
|
||||
it('handles the request', async function() {
|
||||
it('handles the request', async function () {
|
||||
expect(mockHandleRequest).to.have.been.calledOnce
|
||||
|
||||
const {
|
||||
@@ -392,7 +392,7 @@ describe('BaseService', function() {
|
||||
expect(mockSendBadge).to.have.been.calledWith(expectedFormat, {
|
||||
text: ['cat', 'Hello namedParamA: bar with queryParamA: ?'],
|
||||
color: 'lightgrey',
|
||||
template: undefined,
|
||||
template: 'flat',
|
||||
namedLogo: undefined,
|
||||
logo: undefined,
|
||||
logoWidth: undefined,
|
||||
@@ -404,8 +404,8 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDefinition', function() {
|
||||
it('returns the expected result', function() {
|
||||
describe('getDefinition', function () {
|
||||
it('returns the expected result', function () {
|
||||
const {
|
||||
category,
|
||||
name,
|
||||
@@ -432,12 +432,12 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('validate', function() {
|
||||
describe('validate', function () {
|
||||
const dummySchema = Joi.object({
|
||||
requiredString: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
it('throws error for invalid responses', function() {
|
||||
it('throws error for invalid responses', function () {
|
||||
expect(() =>
|
||||
DummyService._validate(
|
||||
{ requiredString: ['this', "shouldn't", 'work'] },
|
||||
@@ -449,19 +449,19 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('request', function() {
|
||||
describe('request', function () {
|
||||
let sandbox
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
})
|
||||
|
||||
it('logs appropriate information', async function() {
|
||||
it('logs appropriate information', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '',
|
||||
res: { statusCode: 200 },
|
||||
@@ -491,7 +491,7 @@ describe('BaseService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('handles errors', async function() {
|
||||
it('handles errors', async function () {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '',
|
||||
res: { statusCode: 404 },
|
||||
@@ -512,14 +512,14 @@ describe('BaseService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Metrics', function() {
|
||||
describe('Metrics', function () {
|
||||
let register
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
register = new prometheus.Registry()
|
||||
})
|
||||
const url = 'some-url'
|
||||
|
||||
it('service response size metric is optional', async function() {
|
||||
it('service response size metric is optional', async function () {
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance: new PrometheusMetrics({ register }),
|
||||
ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled,
|
||||
@@ -544,7 +544,7 @@ describe('BaseService', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('service response size metric is disabled by default', async function() {
|
||||
it('service response size metric is disabled by default', async function () {
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance: new PrometheusMetrics({ register }),
|
||||
ServiceClass: DummyService,
|
||||
@@ -565,7 +565,7 @@ describe('BaseService', function() {
|
||||
).to.not.contain('service_response_bytes_bucket')
|
||||
})
|
||||
})
|
||||
describe('auth', function() {
|
||||
describe('auth', function () {
|
||||
class AuthService extends DummyService {
|
||||
static get auth() {
|
||||
return {
|
||||
@@ -582,7 +582,7 @@ describe('BaseService', function() {
|
||||
}
|
||||
}
|
||||
|
||||
it('when auth is configured properly, invoke() sets authHelper', async function() {
|
||||
it('when auth is configured properly, invoke() sets authHelper', async function () {
|
||||
expect(
|
||||
await AuthService.invoke(
|
||||
{},
|
||||
@@ -598,7 +598,7 @@ describe('BaseService', function() {
|
||||
).to.deep.equal({ message: 'The CI password is abc123' })
|
||||
})
|
||||
|
||||
it('when auth is not configured properly, invoke() returns inacessible', async function() {
|
||||
it('when auth is not configured properly, invoke() returns inacessible', async function () {
|
||||
expect(
|
||||
await AuthService.invoke(
|
||||
{},
|
||||
|
||||
@@ -7,9 +7,7 @@ const coalesce = require('./coalesce')
|
||||
const serverStartTimeGMTString = new Date().toGMTString()
|
||||
const serverStartTimestamp = Date.now()
|
||||
|
||||
const isOptionalNonNegativeInteger = Joi.number()
|
||||
.integer()
|
||||
.min(0)
|
||||
const isOptionalNonNegativeInteger = Joi.number().integer().min(0)
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
cacheSeconds: isOptionalNonNegativeInteger,
|
||||
@@ -69,7 +67,7 @@ function setHeadersForCacheLength(res, cacheLengthSeconds) {
|
||||
cacheControl = 'no-cache, no-store, must-revalidate'
|
||||
expires = nowGMTString
|
||||
} else {
|
||||
cacheControl = `max-age=${cacheLengthSeconds}`
|
||||
cacheControl = `max-age=${cacheLengthSeconds} s-maxage=${cacheLengthSeconds}`
|
||||
expires = new Date(now.getTime() + cacheLengthSeconds * 1000).toGMTString()
|
||||
}
|
||||
|
||||
@@ -94,7 +92,7 @@ function setCacheHeaders({
|
||||
setHeadersForCacheLength(res, cacheLengthSeconds)
|
||||
}
|
||||
|
||||
const staticCacheControlHeader = `max-age=${24 * 3600}` // 1 day.
|
||||
const staticCacheControlHeader = `max-age=${24 * 3600} s-maxage=${24 * 3600}` // 1 day.
|
||||
function setCacheHeadersForStaticResource(res) {
|
||||
res.setHeader('Cache-Control', staticCacheControlHeader)
|
||||
res.setHeader('Last-Modified', serverStartTimeGMTString)
|
||||
|
||||
@@ -15,13 +15,13 @@ const {
|
||||
|
||||
chai.use(require('chai-datetime'))
|
||||
|
||||
describe('Cache header functions', function() {
|
||||
describe('Cache header functions', function () {
|
||||
let res
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
res = httpMocks.createResponse()
|
||||
})
|
||||
|
||||
describe('coalesceCacheLength', function() {
|
||||
describe('coalesceCacheLength', function () {
|
||||
const cacheHeaderConfig = { defaultCacheLengthSeconds: 777 }
|
||||
test(coalesceCacheLength, () => {
|
||||
given({ cacheHeaderConfig, queryParams: {} }).expect(777)
|
||||
@@ -101,18 +101,18 @@ describe('Cache header functions', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('setHeadersForCacheLength', function() {
|
||||
describe('setHeadersForCacheLength', function () {
|
||||
let sandbox
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
sandbox.useFakeTimers()
|
||||
})
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
sandbox = undefined
|
||||
})
|
||||
|
||||
it('should set the correct Date header', function() {
|
||||
it('should set the correct Date header', function () {
|
||||
// Confidence check.
|
||||
expect(res._headers.date).to.equal(undefined)
|
||||
|
||||
@@ -124,40 +124,42 @@ describe('Cache header functions', function() {
|
||||
expect(res._headers.date).to.equal(now)
|
||||
})
|
||||
|
||||
context('cacheLengthSeconds is zero', function() {
|
||||
beforeEach(function() {
|
||||
context('cacheLengthSeconds is zero', function () {
|
||||
beforeEach(function () {
|
||||
setHeadersForCacheLength(res, 0)
|
||||
})
|
||||
|
||||
it('should set the expected Cache-Control header', function() {
|
||||
it('should set the expected Cache-Control header', function () {
|
||||
expect(res._headers['cache-control']).to.equal(
|
||||
'no-cache, no-store, must-revalidate'
|
||||
)
|
||||
})
|
||||
|
||||
it('should set the expected Expires header', function() {
|
||||
it('should set the expected Expires header', function () {
|
||||
expect(res._headers.expires).to.equal(new Date().toGMTString())
|
||||
})
|
||||
})
|
||||
|
||||
context('cacheLengthSeconds is nonzero', function() {
|
||||
beforeEach(function() {
|
||||
context('cacheLengthSeconds is nonzero', function () {
|
||||
beforeEach(function () {
|
||||
setHeadersForCacheLength(res, 123)
|
||||
})
|
||||
|
||||
it('should set the expected Cache-Control header', function() {
|
||||
expect(res._headers['cache-control']).to.equal('max-age=123')
|
||||
it('should set the expected Cache-Control header', function () {
|
||||
expect(res._headers['cache-control']).to.equal(
|
||||
'max-age=123 s-maxage=123'
|
||||
)
|
||||
})
|
||||
|
||||
it('should set the expected Expires header', function() {
|
||||
it('should set the expected Expires header', function () {
|
||||
const expires = new Date(Date.now() + 123 * 1000).toGMTString()
|
||||
expect(res._headers.expires).to.equal(expires)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCacheHeaders', function() {
|
||||
it('sets the expected fields', function() {
|
||||
describe('setCacheHeaders', function () {
|
||||
it('sets the expected fields', function () {
|
||||
const expectedFields = ['date', 'cache-control', 'expires']
|
||||
expectedFields.forEach(field =>
|
||||
expect(res._headers[field]).to.equal(undefined)
|
||||
@@ -178,16 +180,18 @@ describe('Cache header functions', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCacheHeadersForStaticResource', function() {
|
||||
beforeEach(function() {
|
||||
describe('setCacheHeadersForStaticResource', function () {
|
||||
beforeEach(function () {
|
||||
setCacheHeadersForStaticResource(res)
|
||||
})
|
||||
|
||||
it('should set the expected Cache-Control header', function() {
|
||||
expect(res._headers['cache-control']).to.equal(`max-age=${24 * 3600}`)
|
||||
it('should set the expected Cache-Control header', function () {
|
||||
expect(res._headers['cache-control']).to.equal(
|
||||
`max-age=${24 * 3600} s-maxage=${24 * 3600}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should set the expected Last-Modified header', function() {
|
||||
it('should set the expected Last-Modified header', function () {
|
||||
const lastModified = res._headers['last-modified']
|
||||
expect(new Date(lastModified)).to.be.withinTime(
|
||||
// Within the last 60 seconds.
|
||||
@@ -197,17 +201,17 @@ describe('Cache header functions', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('serverHasBeenUpSinceResourceCached', function() {
|
||||
describe('serverHasBeenUpSinceResourceCached', function () {
|
||||
// The stringified req's are hard to understand. I thought Sazerac
|
||||
// provided a way to override the describe message, though I can't find it.
|
||||
context('when there is no If-Modified-Since header', function() {
|
||||
it('returns false', function() {
|
||||
context('when there is no If-Modified-Since header', function () {
|
||||
it('returns false', function () {
|
||||
const req = httpMocks.createRequest()
|
||||
expect(serverHasBeenUpSinceResourceCached(req)).to.equal(false)
|
||||
})
|
||||
})
|
||||
context('when the If-Modified-Since header is invalid', function() {
|
||||
it('returns false', function() {
|
||||
context('when the If-Modified-Since header is invalid', function () {
|
||||
it('returns false', function () {
|
||||
const req = httpMocks.createRequest({
|
||||
headers: { 'If-Modified-Since': 'this-is-not-a-date' },
|
||||
})
|
||||
@@ -216,8 +220,8 @@ describe('Cache header functions', function() {
|
||||
})
|
||||
context(
|
||||
'when the If-Modified-Since header is before the process started',
|
||||
function() {
|
||||
it('returns false', function() {
|
||||
function () {
|
||||
it('returns false', function () {
|
||||
const req = httpMocks.createRequest({
|
||||
headers: { 'If-Modified-Since': '2018-02-01T05:00:00.000Z' },
|
||||
})
|
||||
@@ -227,8 +231,8 @@ describe('Cache header functions', function() {
|
||||
)
|
||||
context(
|
||||
'when the If-Modified-Since header is after the process started',
|
||||
function() {
|
||||
it('returns true', function() {
|
||||
function () {
|
||||
it('returns true', function () {
|
||||
const modifiedTimeStamp = new Date(Date.now() + 1800000)
|
||||
const req = httpMocks.createRequest({
|
||||
headers: { 'If-Modified-Since': modifiedTimeStamp.toISOString() },
|
||||
|
||||
@@ -7,7 +7,7 @@ const defaultErrorMessages = {
|
||||
}
|
||||
|
||||
module.exports = function checkErrorResponse(errorMessages = {}) {
|
||||
return async function({ buffer, res }) {
|
||||
return async function ({ buffer, res }) {
|
||||
let error
|
||||
errorMessages = { ...defaultErrorMessages, ...errorMessages }
|
||||
if (res.statusCode === 404) {
|
||||
|
||||
@@ -4,11 +4,11 @@ const { expect } = require('chai')
|
||||
const { NotFound, InvalidResponse, Inaccessible } = require('./errors')
|
||||
const checkErrorResponse = require('./check-error-response')
|
||||
|
||||
describe('async error handler', function() {
|
||||
describe('async error handler', function () {
|
||||
const buffer = Buffer.from('some stuff')
|
||||
|
||||
context('when status is 200', function() {
|
||||
it('passes through the inputs', async function() {
|
||||
context('when status is 200', function () {
|
||||
it('passes through the inputs', async function () {
|
||||
const res = { statusCode: 200 }
|
||||
expect(await checkErrorResponse()({ res, buffer })).to.deep.equal({
|
||||
res,
|
||||
@@ -17,11 +17,11 @@ describe('async error handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 404', function() {
|
||||
context('when status is 404', function () {
|
||||
const buffer = Buffer.from('some stuff')
|
||||
const res = { statusCode: 404 }
|
||||
|
||||
it('throws NotFound', async function() {
|
||||
it('throws NotFound', async function () {
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
expect.fail('Expected to throw')
|
||||
@@ -34,7 +34,7 @@ describe('async error handler', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom not found message', async function() {
|
||||
it('displays the custom not found message', async function () {
|
||||
const notFoundMessage = 'no goblins found'
|
||||
try {
|
||||
await checkErrorResponse({ 404: notFoundMessage })({ res, buffer })
|
||||
@@ -47,8 +47,8 @@ describe('async error handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 4xx', function() {
|
||||
it('throws InvalidResponse', async function() {
|
||||
context('when status is 4xx', function () {
|
||||
it('throws InvalidResponse', async function () {
|
||||
const res = { statusCode: 499 }
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
@@ -64,7 +64,7 @@ describe('async error handler', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom error message', async function() {
|
||||
it('displays the custom error message', async function () {
|
||||
const res = { statusCode: 403 }
|
||||
try {
|
||||
await checkErrorResponse({ 403: 'access denied' })({ res })
|
||||
@@ -79,8 +79,8 @@ describe('async error handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 5xx', function() {
|
||||
it('throws Inaccessible', async function() {
|
||||
context('when status is 5xx', function () {
|
||||
it('throws Inaccessible', async function () {
|
||||
const res = { statusCode: 503 }
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
@@ -96,7 +96,7 @@ describe('async error handler', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom error message', async function() {
|
||||
it('displays the custom error message', async function () {
|
||||
const res = { statusCode: 500 }
|
||||
try {
|
||||
await checkErrorResponse({ 500: 'server overloaded' })({ res, buffer })
|
||||
|
||||
@@ -104,7 +104,23 @@ module.exports = function coalesceBadge(
|
||||
labelColor: defaultLabelColor,
|
||||
} = defaultBadgeData
|
||||
|
||||
const style = coalesce(overrideStyle, serviceStyle)
|
||||
let style = coalesce(overrideStyle, serviceStyle)
|
||||
if (typeof style !== 'string') {
|
||||
style = 'flat'
|
||||
}
|
||||
if (style.startsWith('popout')) {
|
||||
style = style.replace('popout', 'flat')
|
||||
}
|
||||
const styleValues = [
|
||||
'plastic',
|
||||
'flat',
|
||||
'flat-square',
|
||||
'for-the-badge',
|
||||
'social',
|
||||
]
|
||||
if (!styleValues.includes(style)) {
|
||||
style = 'flat'
|
||||
}
|
||||
|
||||
let namedLogo, namedLogoColor, logoWidth, logoPosition, logoSvgBase64
|
||||
if (overrideLogo) {
|
||||
|
||||
@@ -4,9 +4,9 @@ const { expect } = require('chai')
|
||||
const { getShieldsIcon, getSimpleIcon } = require('../../lib/logos')
|
||||
const coalesceBadge = require('./coalesce-badge')
|
||||
|
||||
describe('coalesceBadge', function() {
|
||||
describe('Label', function() {
|
||||
it('uses the default label', function() {
|
||||
describe('coalesceBadge', function () {
|
||||
describe('Label', function () {
|
||||
it('uses the default label', function () {
|
||||
expect(coalesceBadge({}, {}, { label: 'heyo' }).text).to.deep.equal([
|
||||
'heyo',
|
||||
'n/a',
|
||||
@@ -14,34 +14,34 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
|
||||
// This behavior isn't great and we might want to remove it.
|
||||
it('uses the category as a default label', function() {
|
||||
it('uses the category as a default label', function () {
|
||||
expect(
|
||||
coalesceBadge({}, {}, {}, { category: 'cat' }).text
|
||||
).to.deep.equal(['cat', 'n/a'])
|
||||
})
|
||||
|
||||
it('preserves an empty label', function() {
|
||||
it('preserves an empty label', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { label: '', message: '10k' }, {}).text
|
||||
).to.deep.equal(['', '10k'])
|
||||
})
|
||||
|
||||
it('overrides the label', function() {
|
||||
it('overrides the label', function () {
|
||||
expect(
|
||||
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {}).text
|
||||
).to.deep.equal(['purr count', 'n/a'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Message', function() {
|
||||
it('applies the service message', function() {
|
||||
describe('Message', function () {
|
||||
it('applies the service message', function () {
|
||||
expect(coalesceBadge({}, { message: '10k' }, {}).text).to.deep.equal([
|
||||
undefined,
|
||||
'10k',
|
||||
])
|
||||
})
|
||||
|
||||
it('applies a numeric service message', function() {
|
||||
it('applies a numeric service message', function () {
|
||||
// While a number of badges use this, in the long run we may want
|
||||
// `render()` to always return a string.
|
||||
expect(coalesceBadge({}, { message: 10 }, {}).text).to.deep.equal([
|
||||
@@ -51,12 +51,12 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Right color', function() {
|
||||
it('uses the default color', function() {
|
||||
describe('Right color', function () {
|
||||
it('uses the default color', function () {
|
||||
expect(coalesceBadge({}, {}, {}).color).to.equal('lightgrey')
|
||||
})
|
||||
|
||||
it('overrides the color', function() {
|
||||
it('overrides the color', function () {
|
||||
expect(
|
||||
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {}).color
|
||||
).to.equal('10ADED')
|
||||
@@ -66,8 +66,8 @@ describe('coalesceBadge', function() {
|
||||
).to.equal('B0ADED')
|
||||
})
|
||||
|
||||
context('In case of an error', function() {
|
||||
it('does not override the color', function() {
|
||||
context('In case of an error', function () {
|
||||
it('does not override the color', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ color: '10ADED' },
|
||||
@@ -86,23 +86,23 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('applies the service color', function() {
|
||||
it('applies the service color', function () {
|
||||
expect(coalesceBadge({}, { color: 'red' }, {}).color).to.equal('red')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Left color', function() {
|
||||
it('provides no default label color', function() {
|
||||
describe('Left color', function () {
|
||||
it('provides no default label color', function () {
|
||||
expect(coalesceBadge({}, {}, {}).labelColor).to.be.undefined
|
||||
})
|
||||
|
||||
it('applies the service label color', function() {
|
||||
it('applies the service label color', function () {
|
||||
expect(coalesceBadge({}, { labelColor: 'red' }, {}).labelColor).to.equal(
|
||||
'red'
|
||||
)
|
||||
})
|
||||
|
||||
it('overrides the label color', function() {
|
||||
it('overrides the label color', function () {
|
||||
expect(
|
||||
coalesceBadge({ labelColor: '42f483' }, { color: 'green' }, {})
|
||||
.labelColor
|
||||
@@ -113,7 +113,7 @@ describe('coalesceBadge', function() {
|
||||
).to.equal('B2f483')
|
||||
})
|
||||
|
||||
it('converts a query-string numeric color to a string', function() {
|
||||
it('converts a query-string numeric color to a string', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
// Scoutcamp converts numeric query params to numbers.
|
||||
@@ -134,20 +134,20 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Named logos', function() {
|
||||
it('when not a social badge, ignores the default named logo', function() {
|
||||
describe('Named logos', function () {
|
||||
it('when not a social badge, ignores the default named logo', function () {
|
||||
expect(coalesceBadge({}, {}, { namedLogo: 'appveyor' }).logo).to.be
|
||||
.undefined
|
||||
})
|
||||
|
||||
it('when a social badge, uses the default named logo', function() {
|
||||
it('when a social badge, uses the default named logo', function () {
|
||||
// .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`.
|
||||
expect(
|
||||
coalesceBadge({ style: 'social' }, {}, { namedLogo: 'appveyor' }).logo
|
||||
).to.equal(getSimpleIcon({ name: 'appveyor' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it('applies the named logo', function() {
|
||||
it('applies the named logo', function () {
|
||||
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).namedLogo).to.equal(
|
||||
'npm'
|
||||
)
|
||||
@@ -156,20 +156,20 @@ describe('coalesceBadge', function() {
|
||||
).and.not.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named logo with color', function() {
|
||||
it('applies the named logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo
|
||||
).to.equal(getShieldsIcon({ name: 'npm', color: 'blue' })).and.not.to.be
|
||||
.empty
|
||||
})
|
||||
|
||||
it('overrides the logo', function() {
|
||||
it('overrides the logo', function () {
|
||||
expect(
|
||||
coalesceBadge({ logo: 'npm' }, { namedLogo: 'appveyor' }, {}).logo
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it('overrides the logo with a color', function() {
|
||||
it('overrides the logo with a color', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'npm', logoColor: 'blue' },
|
||||
@@ -180,7 +180,7 @@ describe('coalesceBadge', function() {
|
||||
.empty
|
||||
})
|
||||
|
||||
it("when the logo is overridden, it ignores the service's logo color, position, and width", function() {
|
||||
it("when the logo is overridden, it ignores the service's logo color, position, and width", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'npm' },
|
||||
@@ -195,7 +195,7 @@ describe('coalesceBadge', function() {
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it("overrides the service logo's color", function() {
|
||||
it("overrides the service logo's color", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
@@ -207,7 +207,7 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/issues/2998
|
||||
it('overrides logoSvg', function() {
|
||||
it('overrides logoSvg', function () {
|
||||
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
||||
expect(coalesceBadge({ logo: 'npm' }, { logoSvg }, {}).logo).to.equal(
|
||||
getShieldsIcon({ name: 'npm' })
|
||||
@@ -215,15 +215,15 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom logos', function() {
|
||||
it('overrides the logo with custom svg', function() {
|
||||
describe('Custom logos', function () {
|
||||
it('overrides the logo with custom svg', function () {
|
||||
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
||||
expect(
|
||||
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {}).logo
|
||||
).to.equal(logoSvg)
|
||||
})
|
||||
|
||||
it('ignores the color when custom svg is provided', function() {
|
||||
it('ignores the color when custom svg is provided', function () {
|
||||
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
||||
expect(
|
||||
coalesceBadge(
|
||||
@@ -235,26 +235,26 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logo width', function() {
|
||||
it('overrides the logoWidth', function() {
|
||||
describe('Logo width', function () {
|
||||
it('overrides the logoWidth', function () {
|
||||
expect(coalesceBadge({ logoWidth: 20 }, {}, {}).logoWidth).to.equal(20)
|
||||
})
|
||||
|
||||
it('applies the logo width', function() {
|
||||
it('applies the logo width', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {}).logoWidth
|
||||
).to.equal(275)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logo position', function() {
|
||||
it('overrides the logoPosition', function() {
|
||||
describe('Logo position', function () {
|
||||
it('overrides the logoPosition', function () {
|
||||
expect(
|
||||
coalesceBadge({ logoPosition: -10 }, {}, {}).logoPosition
|
||||
).to.equal(-10)
|
||||
})
|
||||
|
||||
it('applies the logo position', function() {
|
||||
it('applies the logo position', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {})
|
||||
.logoPosition
|
||||
@@ -262,8 +262,8 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Links', function() {
|
||||
it('overrides the links', function() {
|
||||
describe('Links', function () {
|
||||
it('overrides the links', function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ link: 'https://circleci.com/gh/badges/daily-tests' },
|
||||
@@ -277,14 +277,27 @@ describe('coalesceBadge', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Style', function() {
|
||||
it('overrides the template', function() {
|
||||
expect(coalesceBadge({ style: 'pill' }, {}, {}).template).to.equal('pill')
|
||||
describe('Style', function () {
|
||||
it('falls back to flat with invalid style', function () {
|
||||
expect(coalesceBadge({ style: 'pill' }, {}, {}).template).to.equal('flat')
|
||||
expect(coalesceBadge({ style: 7 }, {}, {}).template).to.equal('flat')
|
||||
expect(coalesceBadge({ style: undefined }, {}, {}).template).to.equal(
|
||||
'flat'
|
||||
)
|
||||
})
|
||||
|
||||
it('replaces legacy popout styles', function () {
|
||||
expect(coalesceBadge({ style: 'popout' }, {}, {}).template).to.equal(
|
||||
'flat'
|
||||
)
|
||||
expect(
|
||||
coalesceBadge({ style: 'popout-square' }, {}, {}).template
|
||||
).to.equal('flat-square')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cache length', function() {
|
||||
it('overrides the cache length', function() {
|
||||
describe('Cache length', function () {
|
||||
it('overrides the cache length', function () {
|
||||
expect(
|
||||
coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {})
|
||||
.cacheLengthSeconds
|
||||
|
||||
@@ -7,8 +7,8 @@ const coalesce = require('./coalesce')
|
||||
// `undefined` instead of `null`, though h/t to
|
||||
// https://github.com/royriojas/coalescy for these tests!
|
||||
|
||||
describe('coalesce', function() {
|
||||
test(coalesce, function() {
|
||||
describe('coalesce', function () {
|
||||
test(coalesce, function () {
|
||||
given().expect(undefined)
|
||||
given(null, []).expect([])
|
||||
given(null, [], {}).expect([])
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { expect } = require('chai')
|
||||
const deprecatedService = require('./deprecated-service')
|
||||
|
||||
describe('DeprecatedService', function() {
|
||||
describe('DeprecatedService', function () {
|
||||
const route = {
|
||||
base: 'service/that/no/longer/exists',
|
||||
format: '(?:.+)',
|
||||
@@ -12,33 +12,33 @@ describe('DeprecatedService', function() {
|
||||
const dateAdded = new Date()
|
||||
const commonAttrs = { route, category, dateAdded }
|
||||
|
||||
it('returns true on isDeprecated', function() {
|
||||
it('returns true on isDeprecated', function () {
|
||||
const service = deprecatedService({ ...commonAttrs })
|
||||
expect(service.isDeprecated).to.be.true
|
||||
})
|
||||
|
||||
it('has the expected name', function() {
|
||||
it('has the expected name', function () {
|
||||
const service = deprecatedService({ ...commonAttrs })
|
||||
expect(service.name).to.equal('DeprecatedServiceThatNoLongerExists')
|
||||
})
|
||||
|
||||
it('sets specified route', function() {
|
||||
it('sets specified route', function () {
|
||||
const service = deprecatedService({ ...commonAttrs })
|
||||
expect(service.route).to.deep.equal(route)
|
||||
})
|
||||
|
||||
it('sets specified label', function() {
|
||||
it('sets specified label', function () {
|
||||
const label = 'coverity'
|
||||
const service = deprecatedService({ ...commonAttrs, label })
|
||||
expect(service.defaultBadgeData.label).to.equal(label)
|
||||
})
|
||||
|
||||
it('sets specified category', function() {
|
||||
it('sets specified category', function () {
|
||||
const service = deprecatedService({ ...commonAttrs })
|
||||
expect(service.category).to.equal(category)
|
||||
})
|
||||
|
||||
it('sets specified examples', function() {
|
||||
it('sets specified examples', function () {
|
||||
const examples = [
|
||||
{
|
||||
title: 'Not sure we would have examples',
|
||||
@@ -48,7 +48,7 @@ describe('DeprecatedService', function() {
|
||||
expect(service.examples).to.deep.equal(examples)
|
||||
})
|
||||
|
||||
it('uses default deprecation message when no message specified', async function() {
|
||||
it('uses default deprecation message when no message specified', async function () {
|
||||
const service = deprecatedService({ ...commonAttrs })
|
||||
expect(await service.invoke()).to.deep.equal({
|
||||
isError: true,
|
||||
@@ -57,7 +57,7 @@ describe('DeprecatedService', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses custom deprecation message when specified', async function() {
|
||||
it('uses custom deprecation message when specified', async function () {
|
||||
const message = 'extended outage'
|
||||
const service = deprecatedService({ ...commonAttrs, message })
|
||||
expect(await service.invoke()).to.deep.equal({
|
||||
|
||||
@@ -21,19 +21,12 @@ const schema = Joi.object({
|
||||
staticPreview: Joi.object({
|
||||
label: Joi.string(),
|
||||
message: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string()
|
||||
.allow('')
|
||||
.required(),
|
||||
Joi.number()
|
||||
)
|
||||
.try(Joi.string().allow('').required(), Joi.number())
|
||||
.required(),
|
||||
color: Joi.string(),
|
||||
style: Joi.string(),
|
||||
}).required(),
|
||||
keywords: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default([]),
|
||||
keywords: Joi.array().items(Joi.string()).default([]),
|
||||
documentation: Joi.string(), // Valid HTML.
|
||||
}).required()
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ const { expect } = require('chai')
|
||||
const { test, given } = require('sazerac')
|
||||
const { validateExample, transformExample } = require('./examples')
|
||||
|
||||
describe('validateExample function', function() {
|
||||
it('passes valid examples', function() {
|
||||
describe('validateExample function', function () {
|
||||
it('passes valid examples', function () {
|
||||
const validExamples = [
|
||||
{
|
||||
title: 'Package manager versioning badge',
|
||||
@@ -23,7 +23,7 @@ describe('validateExample function', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid examples', function() {
|
||||
it('rejects invalid examples', function () {
|
||||
const invalidExamples = [
|
||||
{},
|
||||
{ staticPreview: { message: '123' } },
|
||||
@@ -74,7 +74,7 @@ describe('validateExample function', function() {
|
||||
})
|
||||
})
|
||||
|
||||
test(transformExample, function() {
|
||||
test(transformExample, function () {
|
||||
const ExampleService = {
|
||||
name: 'ExampleService',
|
||||
route: {
|
||||
|
||||
@@ -7,8 +7,8 @@ const { mergeQueries } = require('./graphql')
|
||||
|
||||
require('../register-chai-plugins.spec')
|
||||
|
||||
describe('mergeQueries function', function() {
|
||||
it('merges valid gql queries', function() {
|
||||
describe('mergeQueries function', function () {
|
||||
it('merges valid gql queries', function () {
|
||||
expect(
|
||||
print(
|
||||
mergeQueries(
|
||||
@@ -86,7 +86,7 @@ describe('mergeQueries function', function() {
|
||||
).to.equalIgnoreSpaces('{ foo bar }')
|
||||
})
|
||||
|
||||
it('throws an error when passed invalid params', function() {
|
||||
it('throws an error when passed invalid params', function () {
|
||||
expect(() => mergeQueries('', '')).to.throw(Error)
|
||||
expect(() => mergeQueries(undefined, 17, true)).to.throw(Error)
|
||||
expect(() => mergeQueries(gql``, gql`foo`)).to.throw(Error)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const request = require('request')
|
||||
const queryString = require('query-string')
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const { setCacheHeaders } = require('./cache-headers')
|
||||
const {
|
||||
Inaccessible,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { expect } = require('chai')
|
||||
const nock = require('nock')
|
||||
const portfinder = require('portfinder')
|
||||
const Camp = require('camp')
|
||||
const Camp = require('@shields_io/camp')
|
||||
const got = require('../got-test-client')
|
||||
const coalesceBadge = require('./coalesce-badge')
|
||||
const {
|
||||
@@ -70,19 +70,19 @@ function fakeHandlerWithNetworkIo(queryParams, match, sendBadge, request) {
|
||||
})
|
||||
}
|
||||
|
||||
describe('The request handler', function() {
|
||||
describe('The request handler', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(function(done) {
|
||||
beforeEach(function (done) {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
camp.on('listening', () => done())
|
||||
})
|
||||
afterEach(function(done) {
|
||||
afterEach(function (done) {
|
||||
clearRequestCache()
|
||||
if (camp) {
|
||||
camp.close(() => done())
|
||||
@@ -92,17 +92,17 @@ describe('The request handler', function() {
|
||||
|
||||
const standardCacheHeaders = { defaultCacheLengthSeconds: 120 }
|
||||
|
||||
describe('the options object calling style', function() {
|
||||
beforeEach(function() {
|
||||
describe('the options object calling style', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, { handler: fakeHandler })
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the expected response', async function() {
|
||||
it('should return the expected response', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
json: true,
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
@@ -116,17 +116,17 @@ describe('The request handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('the function shorthand calling style', function() {
|
||||
beforeEach(function() {
|
||||
describe('the function shorthand calling style', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, fakeHandler)
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the expected response', async function() {
|
||||
it('should return the expected response', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
json: true,
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
@@ -140,8 +140,8 @@ describe('The request handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe('the response size limit', function() {
|
||||
beforeEach(function() {
|
||||
describe('the response size limit', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, {
|
||||
@@ -151,13 +151,13 @@ describe('The request handler', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should not throw an error if the response <= fetchLimitBytes', async function() {
|
||||
it('should not throw an error if the response <= fetchLimitBytes', async function () {
|
||||
nock('https://www.google.com')
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200, 'x'.repeat(100))
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
json: true,
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
@@ -170,13 +170,13 @@ describe('The request handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the response is > fetchLimitBytes', async function() {
|
||||
it('should throw an error if the response is > fetchLimitBytes', async function () {
|
||||
nock('https://www.google.com')
|
||||
.get('/foo/bar')
|
||||
.once()
|
||||
.reply(200, 'x'.repeat(101))
|
||||
const { statusCode, body } = await got(`${baseUrl}/testing/123.json`, {
|
||||
json: true,
|
||||
responseType: 'json',
|
||||
})
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.deep.equal({
|
||||
@@ -189,15 +189,15 @@ describe('The request handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
nock.cleanAll()
|
||||
})
|
||||
})
|
||||
|
||||
describe('caching', function() {
|
||||
describe('standard query parameters', function() {
|
||||
describe('caching', function () {
|
||||
describe('standard query parameters', function () {
|
||||
let handlerCallCount
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
handlerCallCount = 0
|
||||
})
|
||||
|
||||
@@ -214,12 +214,12 @@ describe('The request handler', function() {
|
||||
)
|
||||
}
|
||||
|
||||
context('With standard cache settings', function() {
|
||||
beforeEach(function() {
|
||||
context('With standard cache settings', function () {
|
||||
beforeEach(function () {
|
||||
register({ cacheHeaderConfig: standardCacheHeaders })
|
||||
})
|
||||
|
||||
it('should cache identical requests', async function() {
|
||||
it('should cache identical requests', async function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg',
|
||||
@@ -228,7 +228,7 @@ describe('The request handler', function() {
|
||||
expect(handlerCallCount).to.equal(1)
|
||||
})
|
||||
|
||||
it('should differentiate known query parameters', async function() {
|
||||
it('should differentiate known query parameters', async function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg?label=foo',
|
||||
@@ -237,7 +237,7 @@ describe('The request handler', function() {
|
||||
expect(handlerCallCount).to.equal(2)
|
||||
})
|
||||
|
||||
it('should ignore unknown query parameters', async function() {
|
||||
it('should ignore unknown query parameters', async function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg?foo=1',
|
||||
@@ -247,17 +247,17 @@ describe('The request handler', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('should set the expires header to current time + defaultCacheLengthSeconds', async function() {
|
||||
it('should set the expires header to current time + defaultCacheLengthSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900')
|
||||
expect(headers['cache-control']).to.equal('max-age=900 s-maxage=900')
|
||||
})
|
||||
|
||||
it('should set the expected cache headers on cached responses', async function() {
|
||||
it('should set the expected cache headers on cached responses', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
|
||||
// Make first request.
|
||||
@@ -268,10 +268,10 @@ describe('The request handler', function() {
|
||||
+new Date(headers.date) + 900000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900')
|
||||
expect(headers['cache-control']).to.equal('max-age=900 s-maxage=900')
|
||||
})
|
||||
|
||||
it('should let live service data override the default cache headers with longer value', async function() {
|
||||
it('should let live service data override the default cache headers with longer value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
@@ -289,10 +289,10 @@ describe('The request handler', function() {
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=400')
|
||||
expect(headers['cache-control']).to.equal('max-age=400 s-maxage=400')
|
||||
})
|
||||
|
||||
it('should not let live service data override the default cache headers with shorter value', async function() {
|
||||
it('should not let live service data override the default cache headers with shorter value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
@@ -310,10 +310,10 @@ describe('The request handler', function() {
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=300')
|
||||
expect(headers['cache-control']).to.equal('max-age=300 s-maxage=300')
|
||||
})
|
||||
|
||||
it('should set the expires header to current time + cacheSeconds', async function() {
|
||||
it('should set the expires header to current time + cacheSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
|
||||
const { headers } = await got(
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=3600`
|
||||
@@ -322,10 +322,10 @@ describe('The request handler', function() {
|
||||
+new Date(headers.date) + 3600000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=3600')
|
||||
expect(headers['cache-control']).to.equal('max-age=3600 s-maxage=3600')
|
||||
})
|
||||
|
||||
it('should ignore cacheSeconds when shorter than defaultCacheLengthSeconds', async function() {
|
||||
it('should ignore cacheSeconds when shorter than defaultCacheLengthSeconds', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 600 } })
|
||||
const { headers } = await got(
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=300`
|
||||
@@ -334,10 +334,10 @@ describe('The request handler', function() {
|
||||
+new Date(headers.date) + 600000
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=600')
|
||||
expect(headers['cache-control']).to.equal('max-age=600 s-maxage=600')
|
||||
})
|
||||
|
||||
it('should set Cache-Control: no-cache, no-store, must-revalidate if cache seconds is 0', async function() {
|
||||
it('should set Cache-Control: no-cache, no-store, must-revalidate if cache seconds is 0', async function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers.expires).to.equal(headers.date)
|
||||
@@ -346,25 +346,25 @@ describe('The request handler', function() {
|
||||
)
|
||||
})
|
||||
|
||||
describe('the cache key', function() {
|
||||
beforeEach(function() {
|
||||
describe('the cache key', function () {
|
||||
beforeEach(function () {
|
||||
register({ cacheHeaderConfig: standardCacheHeaders })
|
||||
})
|
||||
const expectedCacheKey = '/testing/123.json?color=123&label=foo'
|
||||
it('should match expected and use canonical order - 1', async function() {
|
||||
it('should match expected and use canonical order - 1', async function () {
|
||||
await got(`${baseUrl}/testing/123.json?color=123&label=foo`)
|
||||
expect(_requestCache.cache).to.have.keys(expectedCacheKey)
|
||||
})
|
||||
it('should match expected and use canonical order - 2', async function() {
|
||||
it('should match expected and use canonical order - 2', async function () {
|
||||
await got(`${baseUrl}/testing/123.json?label=foo&color=123`)
|
||||
expect(_requestCache.cache).to.have.keys(expectedCacheKey)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom query parameters', function() {
|
||||
describe('custom query parameters', function () {
|
||||
let handlerCallCount
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
handlerCallCount = 0
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
@@ -378,7 +378,7 @@ describe('The request handler', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should differentiate them', async function() {
|
||||
it('should differentiate them', async function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg?foo=1',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const { expect } = require('chai')
|
||||
const { loadServiceClasses, InvalidService } = require('./loader')
|
||||
|
||||
describe('loadServiceClasses function', function() {
|
||||
it('throws if module exports empty', function() {
|
||||
describe('loadServiceClasses function', function () {
|
||||
it('throws if module exports empty', function () {
|
||||
expect(() =>
|
||||
loadServiceClasses(['./loader-test-fixtures/empty-undefined.fixture.js'])
|
||||
).to.throw(InvalidService)
|
||||
@@ -26,7 +26,7 @@ describe('loadServiceClasses function', function() {
|
||||
).to.throw(InvalidService)
|
||||
})
|
||||
|
||||
it('throws if module exports invalid', function() {
|
||||
it('throws if module exports invalid', function () {
|
||||
expect(() =>
|
||||
loadServiceClasses(['./loader-test-fixtures/invalid-no-base.fixture.js'])
|
||||
).to.throw(InvalidService)
|
||||
@@ -47,7 +47,7 @@ describe('loadServiceClasses function', function() {
|
||||
).to.throw(InvalidService)
|
||||
})
|
||||
|
||||
it('registers services if module exports valid service classes', function() {
|
||||
it('registers services if module exports valid service classes', function () {
|
||||
expect(
|
||||
loadServiceClasses([
|
||||
'./loader-test-fixtures/valid-array.fixture.js',
|
||||
|
||||
@@ -126,7 +126,7 @@ Cache.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
clear: function () {
|
||||
this.cache.clear()
|
||||
this.newest = null
|
||||
this.oldest = null
|
||||
|
||||
@@ -25,14 +25,14 @@ function expectCacheSlots(cache, keys) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('The LRU cache', function() {
|
||||
it('should support a zero capacity', function() {
|
||||
describe('The LRU cache', function () {
|
||||
it('should support a zero capacity', function () {
|
||||
const cache = new LRU(0)
|
||||
cache.set('key', 'value')
|
||||
expect(cache.cache.size).to.equal(0)
|
||||
})
|
||||
|
||||
it('should support a one capacity', function() {
|
||||
it('should support a one capacity', function () {
|
||||
const cache = new LRU(1)
|
||||
cache.set('key1', 'value1')
|
||||
expectCacheSlots(cache, ['key1'])
|
||||
@@ -42,7 +42,7 @@ describe('The LRU cache', function() {
|
||||
expect(cache.get('key2')).to.equal('value2')
|
||||
})
|
||||
|
||||
it('should remove the oldest element when reaching capacity', function() {
|
||||
it('should remove the oldest element when reaching capacity', function () {
|
||||
const cache = new LRU(2)
|
||||
|
||||
cache.set('key1', 'value1')
|
||||
@@ -57,7 +57,7 @@ describe('The LRU cache', function() {
|
||||
expect(cache.get('key3')).to.equal('value3')
|
||||
})
|
||||
|
||||
it('should make sure that resetting a key in cache makes it newest', function() {
|
||||
it('should make sure that resetting a key in cache makes it newest', function () {
|
||||
const cache = new LRU(2)
|
||||
|
||||
cache.set('key', 'value')
|
||||
@@ -70,9 +70,9 @@ describe('The LRU cache', function() {
|
||||
expectCacheSlots(cache, ['key2', 'key'])
|
||||
})
|
||||
|
||||
describe('getting a key in the cache', function() {
|
||||
context('when the requested key is oldest', function() {
|
||||
it('should leave the keys in the expected order', function() {
|
||||
describe('getting a key in the cache', function () {
|
||||
context('when the requested key is oldest', function () {
|
||||
it('should leave the keys in the expected order', function () {
|
||||
const cache = new LRU(2)
|
||||
cache.set('key1', 'value1')
|
||||
cache.set('key2', 'value2')
|
||||
@@ -85,8 +85,8 @@ describe('The LRU cache', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the requested key is newest', function() {
|
||||
it('should leave the keys in the expected order', function() {
|
||||
context('when the requested key is newest', function () {
|
||||
it('should leave the keys in the expected order', function () {
|
||||
const cache = new LRU(2)
|
||||
cache.set('key1', 'value1')
|
||||
cache.set('key2', 'value2')
|
||||
@@ -97,8 +97,8 @@ describe('The LRU cache', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the requested key is in the middle', function() {
|
||||
it('should leave the keys in the expected order', function() {
|
||||
context('when the requested key is in the middle', function () {
|
||||
it('should leave the keys in the expected order', function () {
|
||||
const cache = new LRU(3)
|
||||
cache.set('key1', 'value1')
|
||||
cache.set('key2', 'value2')
|
||||
@@ -113,7 +113,7 @@ describe('The LRU cache', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('should clear', function() {
|
||||
it('should clear', function () {
|
||||
// Set up.
|
||||
const cache = new LRU(2)
|
||||
cache.set('key1', 'value1')
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const Camp = require('camp')
|
||||
const Camp = require('@shields_io/camp')
|
||||
const portfinder = require('portfinder')
|
||||
const { expect } = require('chai')
|
||||
const got = require('../got-test-client')
|
||||
const redirector = require('./redirector')
|
||||
|
||||
describe('Redirector', function() {
|
||||
describe('Redirector', function () {
|
||||
const route = {
|
||||
base: 'very/old/service',
|
||||
pattern: ':namedParamA',
|
||||
@@ -16,15 +16,15 @@ describe('Redirector', function() {
|
||||
const dateAdded = new Date()
|
||||
const attrs = { category, route, transformPath, dateAdded }
|
||||
|
||||
it('returns true on isDeprecated', function() {
|
||||
it('returns true on isDeprecated', function () {
|
||||
expect(redirector(attrs).isDeprecated).to.be.true
|
||||
})
|
||||
|
||||
it('has the expected name', function() {
|
||||
it('has the expected name', function () {
|
||||
expect(redirector(attrs).name).to.equal('VeryOldServiceRedirect')
|
||||
})
|
||||
|
||||
it('overrides the name', function() {
|
||||
it('overrides the name', function () {
|
||||
expect(
|
||||
redirector({
|
||||
...attrs,
|
||||
@@ -33,33 +33,33 @@ describe('Redirector', function() {
|
||||
).to.equal('ShinyRedirect')
|
||||
})
|
||||
|
||||
it('sets specified route', function() {
|
||||
it('sets specified route', function () {
|
||||
expect(redirector(attrs).route).to.deep.equal(route)
|
||||
})
|
||||
|
||||
it('sets specified category', function() {
|
||||
it('sets specified category', function () {
|
||||
expect(redirector(attrs).category).to.equal(category)
|
||||
})
|
||||
|
||||
it('throws the expected error when dateAdded is missing', function() {
|
||||
it('throws the expected error when dateAdded is missing', function () {
|
||||
expect(() =>
|
||||
redirector({ route, category, transformPath }).validateDefinition()
|
||||
).to.throw('"dateAdded" is required')
|
||||
})
|
||||
|
||||
describe('ScoutCamp integration', function() {
|
||||
describe('ScoutCamp integration', function () {
|
||||
let port, baseUrl
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
@@ -68,7 +68,7 @@ describe('Redirector', function() {
|
||||
|
||||
const transformPath = ({ namedParamA }) => `/new/service/${namedParamA}`
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
const ServiceClass = redirector({
|
||||
category,
|
||||
route,
|
||||
@@ -81,7 +81,7 @@ describe('Redirector', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect as configured', async function() {
|
||||
it('should redirect as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.svg`,
|
||||
{
|
||||
@@ -93,7 +93,7 @@ describe('Redirector', function() {
|
||||
expect(headers.location).to.equal('/new/service/hello-world.svg')
|
||||
})
|
||||
|
||||
it('should redirect raster extensions to the canonical path as configured', async function() {
|
||||
it('should redirect raster extensions to the canonical path as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.png`,
|
||||
{
|
||||
@@ -107,7 +107,7 @@ describe('Redirector', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should forward the query params', async function() {
|
||||
it('should forward the query params', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/very/old/service/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
@@ -121,14 +121,14 @@ describe('Redirector', function() {
|
||||
)
|
||||
})
|
||||
|
||||
describe('transformQueryParams', function() {
|
||||
describe('transformQueryParams', function () {
|
||||
const route = {
|
||||
base: 'another/old/service',
|
||||
pattern: 'token/:token/:namedParamA',
|
||||
}
|
||||
const transformQueryParams = ({ token }) => ({ token })
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
const ServiceClass = redirector({
|
||||
category,
|
||||
route,
|
||||
@@ -139,7 +139,7 @@ describe('Redirector', function() {
|
||||
ServiceClass.register({ camp }, {})
|
||||
})
|
||||
|
||||
it('should forward the transformed query params', async function() {
|
||||
it('should forward the transformed query params', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg`,
|
||||
{
|
||||
@@ -153,7 +153,7 @@ describe('Redirector', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should forward the specified and transformed query params', async function() {
|
||||
it('should forward the specified and transformed query params', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
@@ -167,7 +167,7 @@ describe('Redirector', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should use transformed query params on param conflicts by default', async function() {
|
||||
it('should use transformed query params on param conflicts by default', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456`,
|
||||
{
|
||||
@@ -181,7 +181,7 @@ describe('Redirector', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('should use specified query params on param conflicts when configured', async function() {
|
||||
it('should use specified query params on param conflicts when configured', async function () {
|
||||
const route = {
|
||||
base: 'override/service',
|
||||
pattern: 'token/:token/:namedParamA',
|
||||
|
||||
@@ -9,9 +9,7 @@ function makeFullUrl(base, partialUrl) {
|
||||
}
|
||||
|
||||
const isValidRoute = Joi.object({
|
||||
base: Joi.string()
|
||||
.allow('')
|
||||
.required(),
|
||||
base: Joi.string().allow('').required(),
|
||||
pattern: Joi.string().allow(''),
|
||||
format: Joi.string(),
|
||||
capture: Joi.alternatives().conditional('format', {
|
||||
|
||||
@@ -9,8 +9,8 @@ const {
|
||||
getQueryParamNames,
|
||||
} = require('./route')
|
||||
|
||||
describe('Route helpers', function() {
|
||||
context('A `pattern` with a named param is declared', function() {
|
||||
describe('Route helpers', function () {
|
||||
context('A `pattern` with a named param is declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
pattern: ':namedParamA',
|
||||
@@ -36,7 +36,7 @@ describe('Route helpers', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('A `format` with a named param is declared', function() {
|
||||
context('A `format` with a named param is declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '([^/]+?)',
|
||||
@@ -62,7 +62,7 @@ describe('Route helpers', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('No named params are declared', function() {
|
||||
context('No named params are declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '(?:[^/]+)',
|
||||
@@ -78,7 +78,7 @@ describe('Route helpers', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('The wrong number of params are declared', function() {
|
||||
context('The wrong number of params are declared', function () {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '([^/]+)/([^/]+)',
|
||||
@@ -94,7 +94,7 @@ describe('Route helpers', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('getQueryParamNames', function() {
|
||||
it('getQueryParamNames', function () {
|
||||
expect(
|
||||
getQueryParamNames({
|
||||
queryParamSchema: Joi.object({ foo: Joi.string() }).required(),
|
||||
|
||||
@@ -5,10 +5,7 @@ const Joi = require('@hapi/joi')
|
||||
// This should be kept in sync with the schema in
|
||||
// `frontend/lib/service-definitions/index.ts`.
|
||||
|
||||
const arrayOfStrings = Joi.array()
|
||||
.items(Joi.string())
|
||||
.min(0)
|
||||
.required()
|
||||
const arrayOfStrings = Joi.array().items(Joi.string()).min(0).required()
|
||||
|
||||
const objectOfKeyValues = Joi.object()
|
||||
.pattern(/./, Joi.string().allow(null))
|
||||
@@ -39,9 +36,7 @@ const serviceDefinition = Joi.object({
|
||||
}).required(),
|
||||
preview: Joi.object({
|
||||
label: Joi.string(),
|
||||
message: Joi.string()
|
||||
.allow('')
|
||||
.required(),
|
||||
message: Joi.string().allow('').required(),
|
||||
color: Joi.string().required(),
|
||||
style: Joi.string(),
|
||||
namedLogo: Joi.string(),
|
||||
@@ -70,9 +65,7 @@ const serviceDefinitionExport = Joi.object({
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
services: Joi.array()
|
||||
.items(serviceDefinition)
|
||||
.required(),
|
||||
services: Joi.array().items(serviceDefinition).required(),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinitionExport(examples, message = undefined) {
|
||||
|
||||
@@ -7,19 +7,19 @@ const trace = require('./trace')
|
||||
const { InvalidParameter } = require('./errors')
|
||||
const validate = require('./validate')
|
||||
|
||||
describe('validate', function() {
|
||||
describe('validate', function () {
|
||||
const schema = Joi.object({
|
||||
requiredString: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
let sandbox
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
})
|
||||
|
||||
@@ -35,8 +35,8 @@ describe('validate', function() {
|
||||
traceSuccessMessage,
|
||||
}
|
||||
|
||||
context('schema is not provided', function() {
|
||||
it('throws the expected programmer error', function() {
|
||||
context('schema is not provided', function () {
|
||||
it('throws the expected programmer error', function () {
|
||||
try {
|
||||
validate(options, { requiredString: 'bar' }, undefined)
|
||||
expect.fail('Expected to throw')
|
||||
@@ -47,8 +47,8 @@ describe('validate', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('data matches schema', function() {
|
||||
it('logs the data', function() {
|
||||
context('data matches schema', function () {
|
||||
it('logs the data', function () {
|
||||
validate(options, { requiredString: 'bar' }, schema)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'validate',
|
||||
@@ -60,8 +60,8 @@ describe('validate', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('data does not match schema', function() {
|
||||
it('logs the data and throws the expected error', function() {
|
||||
context('data does not match schema', function () {
|
||||
it('logs the data and throws the expected error', function () {
|
||||
try {
|
||||
validate(
|
||||
options,
|
||||
@@ -84,8 +84,8 @@ describe('validate', function() {
|
||||
)
|
||||
})
|
||||
|
||||
context('with includeKeys: true', function() {
|
||||
it('includes keys in the error text', function() {
|
||||
context('with includeKeys: true', function () {
|
||||
it('includes keys in the error text', function () {
|
||||
try {
|
||||
validate(
|
||||
{ ...options, includeKeys: true },
|
||||
@@ -108,7 +108,7 @@ describe('validate', function() {
|
||||
})
|
||||
})
|
||||
|
||||
it('allowAndStripUnknownKeys', function() {
|
||||
it('allowAndStripUnknownKeys', function () {
|
||||
try {
|
||||
validate(
|
||||
{ ...options, allowAndStripUnknownKeys: false, includeKeys: true },
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
const merge = require('deepmerge')
|
||||
const config = require('config').util.toObject()
|
||||
const portfinder = require('portfinder')
|
||||
const Server = require('./server')
|
||||
|
||||
function createTestServer({ port }) {
|
||||
const serverConfig = {
|
||||
...config,
|
||||
public: {
|
||||
...config.public,
|
||||
bind: {
|
||||
...config.public.bind,
|
||||
port,
|
||||
},
|
||||
},
|
||||
async function createTestServer(customConfig = {}) {
|
||||
const mergedConfig = merge(config, customConfig)
|
||||
if (!mergedConfig.public.bind.port) {
|
||||
mergedConfig.public.bind.port = await portfinder.getPortPromise()
|
||||
}
|
||||
|
||||
return new Server(serverConfig)
|
||||
return new Server(mergedConfig)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
86
core/server/influx-metrics.js
Normal file
86
core/server/influx-metrics.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const { promisify } = require('util')
|
||||
const { post } = require('request')
|
||||
const postAsync = promisify(post)
|
||||
const generateInstanceId = require('./instance-id-generator')
|
||||
const { promClientJsonToInfluxV2 } = require('./metrics/format-converters')
|
||||
const log = require('./log')
|
||||
|
||||
module.exports = class InfluxMetrics {
|
||||
constructor(metricInstance, config) {
|
||||
this._metricInstance = metricInstance
|
||||
this._config = config
|
||||
this._instanceId = this.getInstanceId()
|
||||
}
|
||||
|
||||
async sendMetrics() {
|
||||
const auth = {
|
||||
user: this._config.username,
|
||||
pass: this._config.password,
|
||||
}
|
||||
const request = {
|
||||
uri: this._config.url,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: this.metrics(),
|
||||
timeout: this._config.timeoutMillseconds,
|
||||
auth,
|
||||
}
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await postAsync(request)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
new Error(`Cannot push metrics. Cause: ${error.name}: ${error.message}`)
|
||||
)
|
||||
}
|
||||
if (response && response.statusCode >= 300) {
|
||||
log.error(
|
||||
new Error(
|
||||
`Cannot push metrics. ${response.request.href} responded with status code ${response.statusCode}`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
startPushingMetrics() {
|
||||
this._intervalId = setInterval(
|
||||
() => this.sendMetrics(),
|
||||
this._config.intervalSeconds * 1000
|
||||
)
|
||||
}
|
||||
|
||||
metrics() {
|
||||
return promClientJsonToInfluxV2(this._metricInstance.metrics(), {
|
||||
env: this._config.envLabel,
|
||||
application: 'shields',
|
||||
instance: this._instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
getInstanceId() {
|
||||
const {
|
||||
hostnameAliases = {},
|
||||
instanceIdFrom,
|
||||
instanceIdEnvVarName,
|
||||
} = this._config
|
||||
let instance
|
||||
if (instanceIdFrom === 'env-var') {
|
||||
instance = process.env[instanceIdEnvVarName]
|
||||
} else if (instanceIdFrom === 'hostname') {
|
||||
const hostname = os.hostname()
|
||||
instance = hostnameAliases[hostname] || hostname
|
||||
} else if (instanceIdFrom === 'random') {
|
||||
instance = generateInstanceId()
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
stopPushingMetrics() {
|
||||
if (this._intervalId) {
|
||||
clearInterval(this._intervalId)
|
||||
this._intervalId = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
174
core/server/influx-metrics.spec.js
Normal file
174
core/server/influx-metrics.spec.js
Normal file
@@ -0,0 +1,174 @@
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const nock = require('nock')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const log = require('./log')
|
||||
const InfluxMetrics = require('./influx-metrics')
|
||||
require('../register-chai-plugins.spec')
|
||||
describe('Influx metrics', function () {
|
||||
const metricInstance = {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
describe('"metrics" function', function () {
|
||||
let osHostnameStub
|
||||
afterEach(function () {
|
||||
nock.enableNetConnect()
|
||||
delete process.env.INSTANCE_ID
|
||||
if (osHostnameStub) {
|
||||
osHostnameStub.restore()
|
||||
}
|
||||
})
|
||||
it('should use an environment variable value as an instance label', async function () {
|
||||
process.env.INSTANCE_ID = 'instance3'
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
instanceIdFrom: 'env-var',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
})
|
||||
|
||||
expect(influxMetrics.metrics()).to.contain('instance=instance3')
|
||||
})
|
||||
|
||||
it('should use a hostname as an instance label', async function () {
|
||||
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'hostname',
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname')
|
||||
})
|
||||
|
||||
it('should use a random string as an instance label', async function () {
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'random',
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.match(/instance=\w+ /)
|
||||
})
|
||||
|
||||
it('should use a hostname alias as an instance label', async function () {
|
||||
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'hostname',
|
||||
hostnameAliases: { 'test-hostname': 'test-hostname-alias' },
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.contain(
|
||||
'instance=test-hostname-alias'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('startPushingMetrics', function () {
|
||||
let influxMetrics, clock
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
influxMetrics.stopPushingMetrics()
|
||||
nock.cleanAll()
|
||||
nock.enableNetConnect()
|
||||
delete process.env.INSTANCE_ID
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('should send metrics', async function () {
|
||||
const scope = nock('http://shields-metrics.io/', {
|
||||
reqheaders: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.persist()
|
||||
.post(
|
||||
'/metrics',
|
||||
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11'
|
||||
)
|
||||
.basicAuth({ user: 'metrics-username', pass: 'metrics-password' })
|
||||
.reply(200)
|
||||
process.env.INSTANCE_ID = 'instance2'
|
||||
influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
url: 'http://shields-metrics.io/metrics',
|
||||
timeoutMillseconds: 100,
|
||||
intervalSeconds: 0.001,
|
||||
username: 'metrics-username',
|
||||
password: 'metrics-password',
|
||||
instanceIdFrom: 'env-var',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
envLabel: 'test-env',
|
||||
})
|
||||
|
||||
influxMetrics.startPushingMetrics()
|
||||
|
||||
await clock.tickAsync(10)
|
||||
expect(scope.isDone()).to.be.equal(
|
||||
true,
|
||||
`pending mocks: ${scope.pendingMocks()}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendMetrics', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(log, 'error')
|
||||
})
|
||||
afterEach(function () {
|
||||
log.error.restore()
|
||||
nock.cleanAll()
|
||||
nock.enableNetConnect()
|
||||
})
|
||||
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
url: 'http://shields-metrics.io/metrics',
|
||||
timeoutMillseconds: 50,
|
||||
intervalSeconds: 0,
|
||||
username: 'metrics-username',
|
||||
password: 'metrics-password',
|
||||
})
|
||||
it('should log errors', async function () {
|
||||
nock.disableNetConnect()
|
||||
|
||||
await influxMetrics.sendMetrics()
|
||||
|
||||
expect(log.error).to.be.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. Cause: NetConnectNotAllowedError: Nock: Disallowed net connect for "shields-metrics.io:80/metrics"'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('should log error responses', async function () {
|
||||
nock('http://shields-metrics.io/').persist().post('/metrics').reply(400)
|
||||
|
||||
await influxMetrics.sendMetrics()
|
||||
|
||||
expect(log.error).to.be.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. http://shields-metrics.io/metrics responded with status code 400'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
8
core/server/instance-id-generator.js
Normal file
8
core/server/instance-id-generator.js
Normal file
@@ -0,0 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
function generateInstanceId() {
|
||||
// from https://gist.github.com/gordonbrander/2230317
|
||||
return Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
module.exports = generateInstanceId
|
||||
27
core/server/metrics/format-converters.js
Normal file
27
core/server/metrics/format-converters.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict'
|
||||
const groupBy = require('lodash.groupby')
|
||||
|
||||
function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
|
||||
// TODO Replace with Array.prototype.flatMap() after migrating to Node.js >= 11
|
||||
const flatMap = (f, arr) => arr.reduce((acc, x) => acc.concat(f(x)), [])
|
||||
return flatMap(metric => {
|
||||
const valuesByLabels = groupBy(metric.values, value =>
|
||||
JSON.stringify(Object.entries(value.labels).sort())
|
||||
)
|
||||
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
|
||||
const labels = Object.entries(metricsWithSameLabel[0].labels)
|
||||
.concat(Object.entries(extraLabels))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`)
|
||||
.join(',')
|
||||
const labelsFormatted = labels ? `,${labels}` : ''
|
||||
const values = metricsWithSameLabel
|
||||
.sort((a, b) => a.metricName.localeCompare(b.metricName))
|
||||
.map(value => `${value.metricName || metric.name}=${value.value}`)
|
||||
.join(',')
|
||||
return `prometheus${labelsFormatted} ${values}`
|
||||
})
|
||||
}, metrics).join('\n')
|
||||
}
|
||||
|
||||
module.exports = { promClientJsonToInfluxV2 }
|
||||
213
core/server/metrics/format-converters.spec.js
Normal file
213
core/server/metrics/format-converters.spec.js
Normal file
@@ -0,0 +1,213 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const prometheus = require('prom-client')
|
||||
const { promClientJsonToInfluxV2 } = require('./format-converters')
|
||||
|
||||
describe('Metric format converters', function () {
|
||||
describe('prom-client JSON to InfluxDB line protocol (version 2)', function () {
|
||||
it('converts a counter', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter1=11')
|
||||
})
|
||||
|
||||
it('converts a counter (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const counter = new prometheus.Counter({
|
||||
name: 'counter1',
|
||||
help: 'counter 1 help',
|
||||
registers: [register],
|
||||
})
|
||||
counter.inc(11)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter1=11')
|
||||
})
|
||||
|
||||
it('converts a gauge', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'gause 1 help',
|
||||
name: 'gauge1',
|
||||
type: 'gauge',
|
||||
values: [{ value: 20, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||
})
|
||||
|
||||
it('converts a gauge (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const gauge = new prometheus.Gauge({
|
||||
name: 'gauge1',
|
||||
help: 'gauge 1 help',
|
||||
registers: [register],
|
||||
})
|
||||
gauge.inc(20)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||
})
|
||||
|
||||
const sortLines = text => text.split('\n').sort().join('\n')
|
||||
|
||||
it('converts a histogram', function () {
|
||||
const json = [
|
||||
{
|
||||
name: 'histogram1',
|
||||
help: 'histogram 1 help',
|
||||
type: 'histogram',
|
||||
values: [
|
||||
{ labels: { le: 5 }, value: 1, metricName: 'histogram1_bucket' },
|
||||
{ labels: { le: 15 }, value: 2, metricName: 'histogram1_bucket' },
|
||||
{ labels: { le: 50 }, value: 2, metricName: 'histogram1_bucket' },
|
||||
{
|
||||
labels: { le: '+Inf' },
|
||||
value: 3,
|
||||
metricName: 'histogram1_bucket',
|
||||
},
|
||||
{ labels: {}, value: 111, metricName: 'histogram1_sum' },
|
||||
{ labels: {}, value: 3, metricName: 'histogram1_count' },
|
||||
],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||
prometheus,le=50 histogram1_bucket=2
|
||||
prometheus,le=15 histogram1_bucket=2
|
||||
prometheus,le=5 histogram1_bucket=1
|
||||
prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a histogram (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const histogram = new prometheus.Histogram({
|
||||
name: 'histogram1',
|
||||
help: 'histogram 1 help',
|
||||
buckets: [5, 15, 50],
|
||||
registers: [register],
|
||||
})
|
||||
histogram.observe(100)
|
||||
histogram.observe(10)
|
||||
histogram.observe(1)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||
prometheus,le=50 histogram1_bucket=2
|
||||
prometheus,le=15 histogram1_bucket=2
|
||||
prometheus,le=5 histogram1_bucket=1
|
||||
prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a summary', function () {
|
||||
const json = [
|
||||
{
|
||||
name: 'summary1',
|
||||
help: 'summary 1 help',
|
||||
type: 'summary',
|
||||
values: [
|
||||
{ labels: { quantile: 0.1 }, value: 1 },
|
||||
{ labels: { quantile: 0.9 }, value: 100 },
|
||||
{ labels: { quantile: 0.99 }, value: 100 },
|
||||
{ metricName: 'summary1_sum', labels: {}, value: 111 },
|
||||
{ metricName: 'summary1_count', labels: {}, value: 3 },
|
||||
],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||
prometheus,quantile=0.9 summary1=100
|
||||
prometheus,quantile=0.1 summary1=1
|
||||
prometheus summary1_count=3,summary1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a summary (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const summary = new prometheus.Summary({
|
||||
name: 'summary1',
|
||||
help: 'summary 1 help',
|
||||
percentiles: [0.1, 0.9, 0.99],
|
||||
registers: [register],
|
||||
})
|
||||
summary.observe(100)
|
||||
summary.observe(10)
|
||||
summary.observe(1)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||
prometheus,quantile=0.9 summary1=100
|
||||
prometheus,quantile=0.1 summary1=1
|
||||
prometheus summary1_count=3,summary1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a counter and skip a timestamp', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 4 help',
|
||||
name: 'counter4',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {}, timestamp: 1581850552292 }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter4=11')
|
||||
})
|
||||
|
||||
it('converts a counter and adds extra labels', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json, {
|
||||
instance: 'instance1',
|
||||
env: 'production',
|
||||
})
|
||||
|
||||
expect(influx).to.be.equal(
|
||||
'prometheus,env=production,instance=instance1 counter1=11'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -39,10 +39,7 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
||||
const ip =
|
||||
(req.headers['x-forwarded-for'] || '').split(', ')[0] ||
|
||||
req.socket.remoteAddress
|
||||
const badgeType = req.url
|
||||
.split(/[/-]/)
|
||||
.slice(0, 3)
|
||||
.join('')
|
||||
const badgeType = req.url.split(/[/-]/).slice(0, 3).join('')
|
||||
const referer = req.headers.referer
|
||||
|
||||
if (ipRateLimit.isBanned(ip, req, res)) {
|
||||
@@ -91,7 +88,7 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
||||
})
|
||||
})
|
||||
|
||||
return function() {
|
||||
return function () {
|
||||
ipRateLimit.stop()
|
||||
badgeTypeRateLimit.stop()
|
||||
refererRateLimit.stop()
|
||||
|
||||
@@ -68,11 +68,13 @@ module.exports = class PrometheusMetrics {
|
||||
registers: [this.register],
|
||||
}),
|
||||
}
|
||||
this.interval = prometheus.collectDefaultMetrics({
|
||||
register: this.register,
|
||||
})
|
||||
}
|
||||
|
||||
async initialize(server) {
|
||||
async registerMetricsEndpoint(server) {
|
||||
const { register } = this
|
||||
this.interval = prometheus.collectDefaultMetrics({ register })
|
||||
|
||||
server.route(/^\/metrics$/, (data, match, end, ask) => {
|
||||
ask.res.setHeader('Content-Type', register.contentType)
|
||||
@@ -88,6 +90,10 @@ module.exports = class PrometheusMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
metrics() {
|
||||
return this.register.getMetricsAsJSON()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} `{ inc() {} }`.
|
||||
*/
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const Camp = require('camp')
|
||||
const Camp = require('@shields_io/camp')
|
||||
const portfinder = require('portfinder')
|
||||
const got = require('../got-test-client')
|
||||
const Metrics = require('./prometheus-metrics')
|
||||
|
||||
describe('Prometheus metrics route', function() {
|
||||
let port, baseUrl
|
||||
beforeEach(async function() {
|
||||
describe('Prometheus metrics route', function () {
|
||||
let port, baseUrl, camp, metrics
|
||||
beforeEach(async function () {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function() {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
if (metrics) {
|
||||
metrics.stop()
|
||||
}
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('returns metrics', async function() {
|
||||
new Metrics({ enabled: true }).initialize(camp)
|
||||
it('returns default metrics', async function () {
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(camp)
|
||||
|
||||
const { statusCode, body } = await got(`${baseUrl}/metrics`)
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ const path = require('path')
|
||||
const url = require('url')
|
||||
const { URL } = url
|
||||
const bytes = require('bytes')
|
||||
const Camp = require('camp')
|
||||
const Camp = require('@shields_io/camp')
|
||||
const originalJoi = require('@hapi/joi')
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const GithubConstellation = require('../../services/github/github-constellation')
|
||||
const suggest = require('../../services/suggest')
|
||||
const { loadServiceClasses } = require('../base-service/loader')
|
||||
@@ -23,6 +23,7 @@ const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
|
||||
const log = require('./log')
|
||||
const sysMonitor = require('./monitor')
|
||||
const PrometheusMetrics = require('./prometheus-metrics')
|
||||
const InfluxMetrics = require('./influx-metrics')
|
||||
|
||||
const Joi = originalJoi
|
||||
.extend(base => ({
|
||||
@@ -70,17 +71,40 @@ const publicConfigSchema = Joi.object({
|
||||
Joi.string().pattern(/^\\\\\.\\pipe\\.+$/)
|
||||
),
|
||||
address: Joi.alternatives().try(
|
||||
Joi.string()
|
||||
.ip()
|
||||
.required(),
|
||||
Joi.string()
|
||||
.hostname()
|
||||
.required()
|
||||
Joi.string().ip().required(),
|
||||
Joi.string().hostname().required()
|
||||
),
|
||||
},
|
||||
metrics: {
|
||||
prometheus: {
|
||||
enabled: Joi.boolean().required(),
|
||||
endpointEnabled: Joi.boolean().required(),
|
||||
},
|
||||
influx: {
|
||||
enabled: Joi.boolean().required(),
|
||||
url: Joi.string()
|
||||
.uri()
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
timeoutMilliseconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
intervalSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
instanceIdFrom: Joi.string()
|
||||
.equal('hostname', 'env-var', 'random')
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
|
||||
is: 'env-var',
|
||||
then: Joi.required(),
|
||||
}),
|
||||
envLabel: Joi.string().when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required(),
|
||||
}),
|
||||
hostnameAliases: Joi.object(),
|
||||
},
|
||||
},
|
||||
ssl: {
|
||||
@@ -91,9 +115,7 @@ const publicConfigSchema = Joi.object({
|
||||
redirectUrl: optionalUrl,
|
||||
rasterUrl: optionalUrl,
|
||||
cors: {
|
||||
allowedOrigin: Joi.array()
|
||||
.items(optionalUrl)
|
||||
.required(),
|
||||
allowedOrigin: Joi.array().items(optionalUrl).required(),
|
||||
},
|
||||
persistence: {
|
||||
dir: Joi.string().required(),
|
||||
@@ -105,10 +127,7 @@ const publicConfigSchema = Joi.object({
|
||||
baseUri: requiredUrl,
|
||||
debug: {
|
||||
enabled: Joi.boolean().required(),
|
||||
intervalSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.required(),
|
||||
intervalSeconds: Joi.number().integer().min(1).required(),
|
||||
},
|
||||
},
|
||||
jira: defaultService,
|
||||
@@ -124,13 +143,12 @@ const publicConfigSchema = Joi.object({
|
||||
trace: Joi.boolean().required(),
|
||||
}).required(),
|
||||
cacheHeaders: {
|
||||
defaultCacheLengthSeconds: Joi.number()
|
||||
.integer()
|
||||
.required(),
|
||||
defaultCacheLengthSeconds: Joi.number().integer().required(),
|
||||
},
|
||||
rateLimit: Joi.boolean().required(),
|
||||
handleInternalErrors: Joi.boolean().required(),
|
||||
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
|
||||
shieldsProductionHerokuHacks: Joi.boolean(),
|
||||
}).required()
|
||||
|
||||
const privateConfigSchema = Joi.object({
|
||||
@@ -160,8 +178,13 @@ const privateConfigSchema = Joi.object({
|
||||
twitch_client_id: Joi.string(),
|
||||
twitch_client_secret: Joi.string(),
|
||||
wheelmap_token: Joi.string(),
|
||||
influx_username: Joi.string(),
|
||||
influx_password: Joi.string(),
|
||||
}).required()
|
||||
|
||||
const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
||||
influx_username: Joi.string().required(),
|
||||
influx_password: Joi.string().required(),
|
||||
})
|
||||
/**
|
||||
* The Server is based on the web framework Scoutcamp. It creates
|
||||
* an http server, sets up helpers for token persistence and monitoring.
|
||||
@@ -173,22 +196,25 @@ class Server {
|
||||
* Badge Server Constructor
|
||||
*
|
||||
* @param {object} config Configuration object read from config yaml files
|
||||
* by https://www.npmjs.com/package/config and validated against
|
||||
* publicConfigSchema and privateConfigSchema
|
||||
* by https://www.npmjs.com/package/config and validated against
|
||||
* publicConfigSchema and privateConfigSchema
|
||||
* @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
|
||||
* @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
|
||||
*/
|
||||
constructor(config) {
|
||||
const publicConfig = Joi.attempt(config.public, publicConfigSchema)
|
||||
let privateConfig
|
||||
try {
|
||||
privateConfig = Joi.attempt(config.private, privateConfigSchema)
|
||||
} catch (e) {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
const privateConfig = this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateConfigSchema
|
||||
)
|
||||
// We want to require an username and a password for the influx metrics
|
||||
// only if the influx metrics are enabled. The private config schema
|
||||
// and the public config schema are two separate schemas so we have to run
|
||||
// validation manually.
|
||||
if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
|
||||
this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateMetricsInfluxConfigSchema
|
||||
)
|
||||
}
|
||||
this.config = {
|
||||
@@ -201,8 +227,31 @@ class Server {
|
||||
service: publicConfig.services.github,
|
||||
private: privateConfig,
|
||||
})
|
||||
|
||||
if (publicConfig.metrics.prometheus.enabled) {
|
||||
this.metricInstance = new PrometheusMetrics()
|
||||
if (publicConfig.metrics.influx.enabled) {
|
||||
this.influxMetrics = new InfluxMetrics(
|
||||
this.metricInstance,
|
||||
Object.assign({}, publicConfig.metrics.influx, {
|
||||
username: privateConfig.influx_username,
|
||||
password: privateConfig.influx_password,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePrivateConfig(privateConfig, privateConfigSchema) {
|
||||
try {
|
||||
return Joi.attempt(privateConfig, privateConfigSchema)
|
||||
} catch (e) {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +391,8 @@ class Server {
|
||||
rasterUrl: config.public.rasterUrl,
|
||||
private: config.private,
|
||||
public: config.public,
|
||||
shieldsProductionHerokuHacks:
|
||||
config.public.shieldsProductionHerokuHacks,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -363,7 +414,7 @@ class Server {
|
||||
|
||||
log(`Server is starting up: ${this.baseUrl}`)
|
||||
|
||||
const camp = (this.camp = Camp.start({
|
||||
const camp = (this.camp = Camp.create({
|
||||
documentRoot: path.resolve(__dirname, '..', '..', 'public'),
|
||||
port,
|
||||
hostname,
|
||||
@@ -379,9 +430,14 @@ class Server {
|
||||
)
|
||||
|
||||
const { githubConstellation } = this
|
||||
githubConstellation.initialize(camp)
|
||||
await githubConstellation.initialize(camp)
|
||||
if (metricInstance) {
|
||||
metricInstance.initialize(camp)
|
||||
if (this.config.public.metrics.prometheus.endpointEnabled) {
|
||||
metricInstance.registerMetricsEndpoint(camp)
|
||||
}
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.startPushingMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
@@ -391,6 +447,8 @@ class Server {
|
||||
this.registerRedirects()
|
||||
this.registerServices()
|
||||
|
||||
camp.listenAsConfigured()
|
||||
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
}
|
||||
|
||||
@@ -425,6 +483,9 @@ class Server {
|
||||
}
|
||||
|
||||
if (this.metricInstance) {
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.stopPushingMetrics()
|
||||
}
|
||||
this.metricInstance.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,163 +2,331 @@
|
||||
|
||||
const { expect } = require('chai')
|
||||
const isSvg = require('is-svg')
|
||||
const portfinder = require('portfinder')
|
||||
const config = require('config')
|
||||
const got = require('../got-test-client')
|
||||
const Server = require('./server')
|
||||
const { createTestServer } = require('./in-process-server-test-helpers')
|
||||
|
||||
describe('The server', function() {
|
||||
let server, baseUrl
|
||||
before('Start the server', async function() {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
const port = await portfinder.getPortPromise()
|
||||
server = createTestServer({ port })
|
||||
baseUrl = server.baseUrl
|
||||
await server.start()
|
||||
})
|
||||
after('Shut down the server', async function() {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
server = undefined
|
||||
})
|
||||
|
||||
it('should allow strings for port', async function() {
|
||||
// fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
|
||||
const pipeServer = createTestServer({
|
||||
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
|
||||
describe('The server', function () {
|
||||
describe('running', function () {
|
||||
let server, baseUrl
|
||||
before('Start the server', async function () {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
server = await createTestServer()
|
||||
baseUrl = server.baseUrl
|
||||
await server.start()
|
||||
})
|
||||
after('Shut down the server', async function () {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
server = undefined
|
||||
})
|
||||
expect(pipeServer).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('should produce colorscheme badges', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
it('should allow strings for port', async function () {
|
||||
// fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
|
||||
const pipeServer = await createTestServer({
|
||||
public: {
|
||||
bind: {
|
||||
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(pipeServer).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('should redirect colorscheme PNG badges as configured', async function() {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{
|
||||
it('should produce colorscheme badges', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should redirect colorscheme PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/:fruit-apple-green.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect modern PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/:fruit-apple-green.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect modern PNG badges as configured', async function() {
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce json badges', async function() {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}twitter/follow/_Pyves.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
})
|
||||
it('should produce json badges', async function () {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}twitter/follow/_Pyves.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
})
|
||||
|
||||
it('should preserve label case', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fRuiT')
|
||||
})
|
||||
it('should preserve label case', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.satisfy(isSvg).and.to.include('fRuiT')
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/pull/1319
|
||||
it('should not crash with a numeric logo', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?logo=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
// https://github.com/badges/shields/pull/1319
|
||||
it('should not crash with a numeric logo', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?logo=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should not crash with a numeric link', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
it('should not crash with a numeric link', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should not crash with a boolean link', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=true`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
it('should not crash with a boolean link', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=true`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should return the 404 badge for unknown badges', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/not/a/badge.svg`,
|
||||
{ throwHttpErrors: false }
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
it('should return the 404 badge for unknown badges', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/not/a/badge.svg`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should return the 404 badge page for rando links', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
|
||||
{
|
||||
it('should return the 404 badge page for rando links', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should redirect the root as configured', async function () {
|
||||
const { statusCode, headers } = await got(baseUrl, {
|
||||
followRedirect: false,
|
||||
})
|
||||
|
||||
expect(statusCode).to.equal(302)
|
||||
// This value is set in `config/test.yml`
|
||||
expect(headers.location).to.equal('http://frontend.example.test')
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('410')
|
||||
.and.to.include('jpg no longer available')
|
||||
})
|
||||
})
|
||||
|
||||
describe('configuration', function () {
|
||||
let server
|
||||
afterEach(async function () {
|
||||
if (server) {
|
||||
server.stop()
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should redirect the root as configured', async function() {
|
||||
const { statusCode, headers } = await got(baseUrl, {
|
||||
followRedirect: false,
|
||||
})
|
||||
|
||||
expect(statusCode).to.equal(302)
|
||||
// This value is set in `config/test.yml`
|
||||
expect(headers.location).to.equal('http://frontend.example.test')
|
||||
it('should allow to enable prometheus metrics', async function () {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
metrics: { prometheus: { enabled: true, endpointEnabled: true } },
|
||||
},
|
||||
})
|
||||
await server.start()
|
||||
|
||||
const { statusCode } = await got(`${server.baseUrl}metrics`)
|
||||
|
||||
expect(statusCode).to.be.equal(200)
|
||||
})
|
||||
|
||||
it('should allow to disable prometheus metrics', async function () {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
metrics: { prometheus: { enabled: true, endpointEnabled: false } },
|
||||
},
|
||||
})
|
||||
await server.start()
|
||||
|
||||
const { statusCode } = await got(`${server.baseUrl}metrics`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
|
||||
expect(statusCode).to.be.equal(404)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
describe('configuration validation', function () {
|
||||
describe('influx', function () {
|
||||
let customConfig
|
||||
beforeEach(function () {
|
||||
customConfig = config.util.toObject()
|
||||
customConfig.public.metrics.influx = {
|
||||
enabled: true,
|
||||
url: 'http://localhost:8081/telegraf',
|
||||
timeoutMilliseconds: 1000,
|
||||
intervalSeconds: 2,
|
||||
instanceIdFrom: 'random',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
hostnameAliases: { 'metrics-hostname': 'metrics-hostname-alias' },
|
||||
envLabel: 'test-env',
|
||||
}
|
||||
customConfig.private = {
|
||||
influx_username: 'telegraf',
|
||||
influx_password: 'telegrafpass',
|
||||
}
|
||||
})
|
||||
|
||||
it('should not require influx configuration', function () {
|
||||
delete customConfig.public.metrics.influx
|
||||
expect(() => new Server(config.util.toObject())).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require url when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.url
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.url" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not require url when influx configuration is disabled', function () {
|
||||
customConfig.public.metrics.influx.enabled = false
|
||||
delete customConfig.public.metrics.influx.url
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require timeoutMilliseconds when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.timeoutMilliseconds
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.timeoutMilliseconds" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require intervalSeconds when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.intervalSeconds
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.intervalSeconds" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require instanceIdFrom when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.instanceIdFrom
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.instanceIdFrom" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require instanceIdEnvVarName when instanceIdFrom is env-var', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
|
||||
delete customConfig.public.metrics.influx.instanceIdEnvVarName
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.instanceIdEnvVarName" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow instanceIdFrom = hostname', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'hostname'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should allow instanceIdFrom = env-var', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should allow instanceIdFrom = random', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'random'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require envLabel when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.envLabel
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.envLabel" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not require hostnameAliases', function () {
|
||||
delete customConfig.public.metrics.influx.hostnameAliases
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should allow empty hostnameAliases', function () {
|
||||
customConfig.public.metrics.influx.hostnameAliases = {}
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require username when influx configuration is enabled', function () {
|
||||
delete customConfig.private.influx_username
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'Private configuration is invalid. Check these paths: influx_username'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require password when influx configuration is enabled', function () {
|
||||
delete customConfig.private.influx_password
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'Private configuration is invalid. Check these paths: influx_password'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow other private keys', function () {
|
||||
customConfig.private.gh_token = 'my-token'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
})
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('410')
|
||||
.and.to.include('jpg no longer available')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,11 +43,11 @@
|
||||
// 1. Generating the list of services to test is necessarily asynchronous, and
|
||||
// in Mocha, exclusive tests (`it.only` and `describe.only`) can only be
|
||||
// applied synchronously. In other words, if you try to add exclusive tests
|
||||
// in an asynchronous callback, all the tests will run. This is true even
|
||||
// when using `_mocha --delay`, as we are. Undoubtedly this could be fixed,
|
||||
// though it's not worth it. The problem is obscure and therefore low
|
||||
// for Mocha, which is quite backlogged. There is an easy workaround, which
|
||||
// is to generate the list of services to test in a separate process.
|
||||
// in an asynchronous callback, all the tests will run. Undoubtedly this
|
||||
// could be fixed, though it's not worth it. The problem is obscure and
|
||||
// therefore low for Mocha, which is quite backlogged. There is an easy
|
||||
// workaround, which is to generate the list of services to test in a
|
||||
// separate process.
|
||||
// 2. Executing these two steps of the test runner separately makes the process
|
||||
// easier to reason about and much easier to debug on a dev machine.
|
||||
// 3. Getting "pipefail" to work cross platform with an npm script seems tricky.
|
||||
@@ -73,11 +73,17 @@ if (process.env.TESTED_SERVER_URL) {
|
||||
} else {
|
||||
const port = 1111
|
||||
baseUrl = 'http://localhost:1111'
|
||||
before('Start running the server', function() {
|
||||
server = createTestServer({ port })
|
||||
before('Start running the server', async function () {
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
bind: {
|
||||
port,
|
||||
},
|
||||
},
|
||||
})
|
||||
server.start()
|
||||
})
|
||||
after('Shut down the server', async function() {
|
||||
after('Shut down the server', async function () {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
@@ -125,5 +131,3 @@ if (typeof onlyServices === 'undefined' || onlyServices.includes('*****')) {
|
||||
}
|
||||
|
||||
runner.toss()
|
||||
// Invoke run() asynchronously, because Mocha will not start otherwise.
|
||||
process.nextTick(run)
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
inferPullRequest,
|
||||
} = require('./infer-pull-request')
|
||||
|
||||
describe('Pull request inference', function() {
|
||||
describe('Pull request inference', function () {
|
||||
test(parseGithubPullRequestUrl, () => {
|
||||
forCases([
|
||||
given('https://github.com/badges/shields/pull/1234'),
|
||||
|
||||
@@ -29,7 +29,7 @@ async function getTitle(owner, repo, pullRequest) {
|
||||
'User-Agent': 'badges/shields',
|
||||
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
json: true,
|
||||
responseType: 'json',
|
||||
}
|
||||
)
|
||||
return title
|
||||
|
||||
@@ -85,7 +85,7 @@ class ServiceTester {
|
||||
this.beforeEach()
|
||||
})
|
||||
// eslint-disable-next-line mocha/prefer-arrow-callback
|
||||
.finally(function() {
|
||||
.finally(function () {
|
||||
// `this` is the IcedFrisby instance.
|
||||
let responseBody
|
||||
try {
|
||||
@@ -125,7 +125,7 @@ class ServiceTester {
|
||||
|
||||
const fn = this._only ? describe.only : describe
|
||||
// eslint-disable-next-line mocha/prefer-arrow-callback
|
||||
fn(this.title, function() {
|
||||
fn(this.title, function () {
|
||||
specs.forEach(spec => {
|
||||
spec._message = `[${spec.hasIntercept ? 'mocked' : 'live'}] ${
|
||||
spec._message
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { test, given } = require('sazerac')
|
||||
const servicesForTitle = require('./services-for-title')
|
||||
|
||||
describe('Services from PR title', function() {
|
||||
describe('Services from PR title', function () {
|
||||
test(servicesForTitle, () => {
|
||||
given('[Travis] Fix timeout issues').expect(['travis'])
|
||||
given('[Travis Sonar] Support user token authentication').expect([
|
||||
|
||||
@@ -6,20 +6,20 @@ const readFile = require('fs-readfile-promise')
|
||||
const { expect } = require('chai')
|
||||
const FsTokenPersistence = require('./fs-token-persistence')
|
||||
|
||||
describe('File system token persistence', function() {
|
||||
describe('File system token persistence', function () {
|
||||
let path, persistence
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
path = tmp.tmpNameSync()
|
||||
persistence = new FsTokenPersistence({ path })
|
||||
})
|
||||
|
||||
context('when the file does not exist', function() {
|
||||
it('does nothing', async function() {
|
||||
context('when the file does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('saving creates an empty file', async function() {
|
||||
it('saving creates an empty file', async function () {
|
||||
await persistence.initialize()
|
||||
|
||||
await persistence.save()
|
||||
@@ -29,20 +29,20 @@ describe('File system token persistence', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the file exists', function() {
|
||||
context('when the file exists', function () {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
fs.writeFileSync(path, JSON.stringify(initialTokens))
|
||||
})
|
||||
|
||||
it('loads the contents', async function() {
|
||||
it('loads the contents', async function () {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function() {
|
||||
it('saves the change', async function() {
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = Array.from(initialTokens)
|
||||
expected.push(newToken)
|
||||
@@ -55,8 +55,8 @@ describe('File system token persistence', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function() {
|
||||
it('saves the change', async function() {
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ const Redis = require('ioredis')
|
||||
const { expect } = require('chai')
|
||||
const RedisTokenPersistence = require('./redis-token-persistence')
|
||||
|
||||
describe('Redis token persistence', function() {
|
||||
describe('Redis token persistence', function () {
|
||||
let server
|
||||
// In CI, expect redis already to be running.
|
||||
if (!process.env.CI) {
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
server = new RedisServer({ config: { host: 'localhost' } })
|
||||
await server.open()
|
||||
})
|
||||
@@ -18,11 +18,11 @@ describe('Redis token persistence', function() {
|
||||
const key = 'tokenPersistenceIntegrationTest'
|
||||
|
||||
let redis
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
redis = new Redis()
|
||||
await redis.del(key)
|
||||
})
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
if (redis) {
|
||||
await redis.quit()
|
||||
redis = undefined
|
||||
@@ -30,44 +30,44 @@ describe('Redis token persistence', function() {
|
||||
})
|
||||
|
||||
if (!process.env.CI) {
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
await server.close()
|
||||
server = undefined
|
||||
})
|
||||
}
|
||||
|
||||
let persistence
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
persistence = new RedisTokenPersistence({ key })
|
||||
})
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
if (persistence) {
|
||||
await persistence.stop()
|
||||
persistence = undefined
|
||||
}
|
||||
})
|
||||
|
||||
context('when the key does not exist', function() {
|
||||
it('does nothing', async function() {
|
||||
context('when the key does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
context('when the key exists', function() {
|
||||
context('when the key exists', function () {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async function () {
|
||||
await redis.sadd(key, initialTokens)
|
||||
})
|
||||
|
||||
it('loads the contents', async function() {
|
||||
it('loads the contents', async function () {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens.sort()).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function() {
|
||||
it('saves the change', async function() {
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = initialTokens.slice()
|
||||
expected.push(newToken)
|
||||
@@ -80,8 +80,8 @@ describe('Redis token persistence', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function() {
|
||||
it('saves the change', async function() {
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
|
||||
@@ -13,10 +13,7 @@ const PriorityQueue = require('priorityqueuejs')
|
||||
* @returns {string} hash
|
||||
*/
|
||||
function sanitizeToken(id) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(id, 'utf-8')
|
||||
.digest('hex')
|
||||
return crypto.createHash('sha256').update(id, 'utf-8').digest('hex')
|
||||
}
|
||||
|
||||
function getUtcEpochSeconds() {
|
||||
|
||||
@@ -11,27 +11,27 @@ function expectPoolToBeExhausted(pool) {
|
||||
}).to.throw(Error, /^Token pool is exhausted$/)
|
||||
}
|
||||
|
||||
describe('The token pool', function() {
|
||||
describe('The token pool', function () {
|
||||
const ids = ['1', '2', '3', '4', '5']
|
||||
const batchSize = 3
|
||||
|
||||
let tokenPool
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
tokenPool = new TokenPool({ batchSize })
|
||||
ids.forEach(id => tokenPool.add(id))
|
||||
})
|
||||
|
||||
it('allValidTokenIds() should return the full list', function() {
|
||||
it('allValidTokenIds() should return the full list', function () {
|
||||
expect(tokenPool.allValidTokenIds()).to.deep.equal(ids)
|
||||
})
|
||||
|
||||
it('should yield the expected tokens', function() {
|
||||
it('should yield the expected tokens', function () {
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
)
|
||||
})
|
||||
|
||||
it('should repeat when reaching the end', function() {
|
||||
it('should repeat when reaching the end', function () {
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
)
|
||||
@@ -40,17 +40,17 @@ describe('The token pool', function() {
|
||||
)
|
||||
})
|
||||
|
||||
describe('serializeDebugInfo should initially return the expected', function() {
|
||||
beforeEach(function() {
|
||||
describe('serializeDebugInfo should initially return the expected', function () {
|
||||
beforeEach(function () {
|
||||
sinon.useFakeTimers({ now: 1544307744484 })
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
context('sanitize is not specified', function() {
|
||||
it('returns fully sanitized results', function() {
|
||||
context('sanitize is not specified', function () {
|
||||
it('returns fully sanitized results', function () {
|
||||
// This is `sha()` of '1', '2', '3', '4', '5'. These are written
|
||||
// literally for avoidance of doubt as to whether sanitization is
|
||||
// happening.
|
||||
@@ -79,8 +79,8 @@ describe('The token pool', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('with sanitize: false', function() {
|
||||
it('returns unsanitized results', function() {
|
||||
context('with sanitize: false', function () {
|
||||
it('returns unsanitized results', function () {
|
||||
expect(tokenPool.serializeDebugInfo({ sanitize: false })).to.deep.equal(
|
||||
{
|
||||
allValidTokenIds: ids,
|
||||
@@ -101,8 +101,8 @@ describe('The token pool', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens are marked exhausted immediately', function() {
|
||||
it('should be exhausted', function() {
|
||||
context('tokens are marked exhausted immediately', function () {
|
||||
it('should be exhausted', function () {
|
||||
ids.forEach(() => {
|
||||
const token = tokenPool.next()
|
||||
token.update(0, Token.nextResetNever)
|
||||
@@ -112,8 +112,8 @@ describe('The token pool', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens are marked after the last request', function() {
|
||||
it('should be exhausted', function() {
|
||||
context('tokens are marked after the last request', function () {
|
||||
it('should be exhausted', function () {
|
||||
ids.forEach(() => {
|
||||
const token = times(batchSize, () => tokenPool.next()).pop()
|
||||
token.update(0, Token.nextResetNever)
|
||||
@@ -123,8 +123,8 @@ describe('The token pool', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens are renewed', function() {
|
||||
it('should keep using them', function() {
|
||||
context('tokens are renewed', function () {
|
||||
it('should keep using them', function () {
|
||||
const tokensToRenew = ['2', '4']
|
||||
const renewalCount = 3
|
||||
|
||||
@@ -149,16 +149,16 @@ describe('The token pool', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens reset', function() {
|
||||
context('tokens reset', function () {
|
||||
let clock
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
afterEach(function() {
|
||||
afterEach(function () {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('should start using them', function() {
|
||||
it('should start using them', function () {
|
||||
const tokensToReset = ['2', '4']
|
||||
const futureTime = 1440
|
||||
|
||||
@@ -183,8 +183,8 @@ describe('The token pool', function() {
|
||||
})
|
||||
})
|
||||
|
||||
context('when empty', function() {
|
||||
it('next() should return the expected error', function() {
|
||||
context('when empty', function () {
|
||||
it('next() should return the expected error', function () {
|
||||
const tokenPool = new TokenPool()
|
||||
expect(() => tokenPool.next()).to.throw('Token pool is exhausted')
|
||||
})
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
'use strict'
|
||||
|
||||
describe('Main page', function() {
|
||||
describe('Main page', function () {
|
||||
const backendUrl = Cypress.env('backend_url')
|
||||
const SEARCH_INPUT = 'input[placeholder="search / project URL"]'
|
||||
|
||||
function expectBadgeExample(title, previewUrl, pattern) {
|
||||
cy.contains('tr', `${title}:`)
|
||||
.find('code')
|
||||
.should('have.text', pattern)
|
||||
cy.contains('tr', `${title}:`).find('code').should('have.text', pattern)
|
||||
cy.contains('tr', `${title}:`)
|
||||
.find('img')
|
||||
.should('have.attr', 'src', previewUrl)
|
||||
}
|
||||
|
||||
it('Search for badges', function() {
|
||||
it('Search for badges', function () {
|
||||
cy.visit('/')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('pypi')
|
||||
@@ -21,7 +19,7 @@ describe('Main page', function() {
|
||||
cy.contains('PyPI - License')
|
||||
})
|
||||
|
||||
it('Shows badge from category', function() {
|
||||
it('Shows badge from category', function () {
|
||||
cy.visit('/category/chat')
|
||||
|
||||
expectBadgeExample(
|
||||
@@ -31,7 +29,7 @@ describe('Main page', function() {
|
||||
)
|
||||
})
|
||||
|
||||
it('Suggest badges', function() {
|
||||
it('Suggest badges', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
|
||||
@@ -41,7 +39,7 @@ describe('Main page', function() {
|
||||
expectBadgeExample('GitHub issues', badgeUrl, badgeUrl)
|
||||
})
|
||||
|
||||
it('Customization form is filled with suggested badge details', function() {
|
||||
it('Customization form is filled with suggested badge details', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
@@ -53,7 +51,7 @@ describe('Main page', function() {
|
||||
cy.get('input[name="repo"]').should('have.value', 'shields')
|
||||
})
|
||||
|
||||
it('Customizate suggested badge', function() {
|
||||
it('Customizate suggested badge', function () {
|
||||
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
|
||||
cy.visit('/')
|
||||
cy.get(SEARCH_INPUT).type('https://github.com/badges/shields')
|
||||
|
||||
@@ -25,7 +25,7 @@ and learn about the [Github workflow](http://try.github.io/).
|
||||
|
||||
#### Node, NPM
|
||||
|
||||
Node 8 or later is required. If you don't already have them,
|
||||
Node 10 or later is required. If you don't already have them,
|
||||
install node and npm: https://nodejs.org/en/download/
|
||||
|
||||
### Setup a dev install
|
||||
|
||||
@@ -7,7 +7,7 @@ The Shields codebase is divided into several parts:
|
||||
1. The frontend (about 7% of the code)
|
||||
1. [`frontend`][frontend]
|
||||
2. The badge renderer (which is available as an npm package)
|
||||
1. [`gh-badges`][gh-badges]
|
||||
1. [`badge-maker`][badge-maker]
|
||||
3. The base service classes (about 8% of the code, and probably the most important
|
||||
code in the codebase)
|
||||
1. [`core/base-service`][base-service]
|
||||
@@ -24,7 +24,7 @@ The Shields codebase is divided into several parts:
|
||||
1. [`lib/suggest.js`][suggest]
|
||||
|
||||
[frontend]: https://github.com/badges/shields/tree/master/frontend
|
||||
[gh-badges]: https://github.com/badges/shields/tree/master/gh-badges
|
||||
[badge-maker]: https://github.com/badges/shields/tree/master/badge-maker
|
||||
[base-service]: https://github.com/badges/shields/tree/master/core/base-service
|
||||
[server]: https://github.com/badges/shields/tree/master/core/server
|
||||
[token-pooling]: https://github.com/badges/shields/tree/master/core/token-pooling
|
||||
@@ -36,7 +36,7 @@ The tests are also divided into several parts:
|
||||
1. Unit and functional tests of the frontend
|
||||
1. `frontend/**/*.spec.js`
|
||||
2. Unit and functional tests of the badge renderer
|
||||
1. `gh-badges/**/*.spec.js`
|
||||
1. `badge-maker/**/*.spec.js`
|
||||
3. Unit and functional tests of the core code
|
||||
1. `core/**/*.spec.js`
|
||||
4. Unit and functional tests of the service helper functions
|
||||
|
||||
@@ -36,38 +36,7 @@
|
||||
|
||||
There are [too many bottlenecks][issue 2577]!
|
||||
|
||||
## Badge servers
|
||||
|
||||
There are three public badge servers on OVH VPS’s.
|
||||
|
||||
| Cname | Hostname | Type | IP | Location |
|
||||
| --------------------------- | -------------------- | ---- | -------------- | ------------------ |
|
||||
| [s0.servers.shields.io][s0] | vps71670.vps.ovh.ca | VPS | 192.99.59.72 | Quebec, Canada |
|
||||
| [s1.servers.shields.io][s1] | vps244529.ovh.net | VPS | 51.254.114.150 | Gravelines, France |
|
||||
| [s2.servers.shields.io][s2] | vps117870.vps.ovh.ca | VPS | 149.56.96.133 | Quebec, Canada |
|
||||
|
||||
- These are single-core virtual hosts with 2 GB RAM [VPS SSD 1][].
|
||||
- The Node version (v9.4.0 at time of writing) and dependency versions on the
|
||||
servers can be inspected in Sentry, but only when an error occurs.
|
||||
- The servers use self-signed SSL certificates. ([#1460][issue 1460])
|
||||
- After accepting the certificate, you can debug an individual server using
|
||||
the links above.
|
||||
- The scripts that start the server live in the [ServerScript][] repo. However
|
||||
updates must be pulled manually. They are not updated as part of the deploy process.
|
||||
- The server runs SSH.
|
||||
- Deploys are made using a git post-receive hook.
|
||||
- The server uses systemd to automatically restart the server when it crashes.
|
||||
- Provisioning additional servers is a manual process which is yet to been
|
||||
documented.
|
||||
- The public servers _do not_ use docker. The `Dockerfile` is included for
|
||||
self-hosting (including on a Docker-capable PaaS).
|
||||
|
||||
[s0]: https://s0.servers.shields.io/index.html
|
||||
[s1]: https://s1.servers.shields.io/index.html
|
||||
[s2]: https://s2.servers.shields.io/index.html
|
||||
[vps ssd 1]: https://www.ovh.com/world/vps/vps-ssd.xml
|
||||
[issue 1460]: https://github.com/badges/shields/issues/1460
|
||||
[serverscript]: https://github.com/badges/ServerScript
|
||||
[issue 2577]: https://github.com/badges/shields/issues/2577
|
||||
|
||||
## Attached state
|
||||
|
||||
@@ -147,64 +116,25 @@ hosted on [Zeit Now][]. It's managed in the
|
||||
|
||||
## Deployment
|
||||
|
||||
To set things up for deployment:
|
||||
The deployment is done in two stages: the badge server (heroku) and the front-end (gh-pages).
|
||||
|
||||
1. Get your SSH key added to the server.
|
||||
2. Clone a fresh copy of the repository, dedicated for deployment.
|
||||
(Not required, but recommended; and lets you use `npm ci` below.)
|
||||
3. Add remotes:
|
||||
### Heroku
|
||||
|
||||
After merging a commit to master, heroku should create a staging deploy. Check this has deployed correctly in the `shields-staging` pipeline and review http://shields-staging.herokuapp.com/
|
||||
|
||||
If we're happy with it, "promote to production". This will deploy what's on staging to the `shields-production-eu` and `shields-production-us` pieplines.
|
||||
|
||||
### Frontend
|
||||
|
||||
To deploy the front-end to GH pages, use a clean clone of the shields repo.
|
||||
|
||||
```sh
|
||||
git remote add s0 root@s0.servers.shields.io:/home/m/shields.git
|
||||
git remote add s1 root@s1.servers.shields.io:/home/m/shields.git
|
||||
git remote add s2 root@s2.servers.shields.io:/home/m/shields.git
|
||||
$ git pull # update the working copy
|
||||
$ npm ci # install dependencies (devDependencies are needed to build the frontend)
|
||||
$ make deploy-gh-pages # build the frontend and push it to the gh-pages branch
|
||||
```
|
||||
|
||||
`origin` should point to GitHub as usual.
|
||||
|
||||
4. Since the deploy uses `git worktree`, make sure you have git 2.5 or later.
|
||||
|
||||
To deploy:
|
||||
|
||||
1. Use `git fetch` to obtain a current copy of
|
||||
`local-shields-io-production.yml` from the server (or obtain the current
|
||||
version of that file some other way). Save it in `config/`.
|
||||
2. Check out the commit you want to deploy.
|
||||
3. Run `npm ci`. **This is super important for the frontend build!**
|
||||
4. Run `make deploy-s0` to make a canary deploy.
|
||||
5. Check the canary deploy:
|
||||
- [Visit the server][s0]. Don't forget that most of the preview badges
|
||||
are static!
|
||||
- Look for errors in [Sentry][].
|
||||
- Keep an eye on the [status page][status].
|
||||
6. After a little while (usually 10–60 minutes), finish the deploy:
|
||||
`make push-s1 push-s2 deploy-gh-pages`.
|
||||
|
||||
To roll back, check out the commit you want to roll back to and repeat those
|
||||
steps.
|
||||
|
||||
To see which commit is deployed to a server run `git ls-remote` and then
|
||||
`git log` on the `HEAD` ref. There will be two deploy commits preceded by the
|
||||
commit which was deployed.
|
||||
|
||||
Be careful not to push the deploy commits to GitHub.
|
||||
|
||||
`make deploy-s0` does the following:
|
||||
|
||||
1. Creates a working tree in `/tmp`.
|
||||
2. In that tree, runs `features` and `examples` to generate data files
|
||||
needed for the frontend.
|
||||
3. Builds and checks in the built frontend.
|
||||
4. Checks in `local-shields-io-production.yml`.
|
||||
5. Pushes to s0, which updates dependencies and then restarts itself.
|
||||
|
||||
`make push-s1 push-s2 deploy-gh-pages` does the following:
|
||||
|
||||
1. Pushes the same working tree to s1 and s2.
|
||||
2. Creates a new working tree for the frontend.
|
||||
3. Adds a commit cleaning out the index.
|
||||
4. Adds another commit with the build frontend.
|
||||
5. Pushes to `gh-pages`.
|
||||
No secrets are required to build or deploy the frontend.
|
||||
|
||||
## DNS
|
||||
|
||||
@@ -214,7 +144,7 @@ DNS is registered with [DNSimple][].
|
||||
|
||||
## Logs
|
||||
|
||||
Logs are available on the individual servers via SSH.
|
||||
Logs can be retrieved [from heroku](https://devcenter.heroku.com/articles/logging#log-retrieval).
|
||||
|
||||
## Error reporting
|
||||
|
||||
@@ -248,11 +178,18 @@ Request performance is monitored in two places:
|
||||
[notifications]: http://shields.redsparr0w.com/discord_notification
|
||||
[monitor discord]: https://discordapp.com/channels/308323056592486420/470700909182320646
|
||||
|
||||
## Known limitations
|
||||
## Legacy servers
|
||||
|
||||
1. The only way to inspect the commit on the server is with `git ls-remote`.
|
||||
2. The production deploy installs `devDependencies`. It does not honor
|
||||
`package-lock.json`. ([#1988][issue 1988])
|
||||
There are three legacy servers on OVH VPS’s which are currently used for proxying.
|
||||
|
||||
[issue 2577]: https://github.com/badges/shields/issues/2577
|
||||
[issue 1988]: https://github.com/badges/shields/issues/1988
|
||||
| Cname | Hostname | Type | IP | Location |
|
||||
| --------------------------- | -------------------- | ---- | -------------- | ------------------ |
|
||||
| [s0.servers.shields.io][s0] | vps71670.vps.ovh.ca | VPS | 192.99.59.72 | Quebec, Canada |
|
||||
| [s1.servers.shields.io][s1] | vps244529.ovh.net | VPS | 51.254.114.150 | Gravelines, France |
|
||||
| [s2.servers.shields.io][s2] | vps117870.vps.ovh.ca | VPS | 149.56.96.133 | Quebec, Canada |
|
||||
|
||||
[s0]: https://s0.servers.shields.io/index.html
|
||||
[s1]: https://s1.servers.shields.io/index.html
|
||||
[s2]: https://s2.servers.shields.io/index.html
|
||||
|
||||
The only way to inspect the commit on the server is with `git ls-remote`.
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = class ExampleService extends LegacyService {
|
||||
static registerLegacyRouteHandler({ camp, cache }) {
|
||||
camp.route(
|
||||
/^\/example\/([^\/]+)\/([^\/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
cache(function(data, match, sendBadge, request) {
|
||||
cache(function (data, match, sendBadge, request) {
|
||||
var first = match[1]
|
||||
var second = match[2]
|
||||
var format = match[3]
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Installation
|
||||
|
||||
You will need Node 8 or later, which you can install using a
|
||||
You will need Node 10 or later, which you can install using a
|
||||
[package manager][].
|
||||
|
||||
On Ubuntu / Debian:
|
||||
|
||||
```sh
|
||||
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -; sudo apt-get install -y nodejs
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -; sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
```sh
|
||||
@@ -78,7 +78,7 @@ $ docker run --rm -p 8080:80 --name shields shields
|
||||
# or if you have shields.env file, run the following instead
|
||||
$ docker run --rm -p 8080:80 --env-file shields.env --name shields shields
|
||||
|
||||
> gh-badges@1.1.2 start /usr/src/app
|
||||
> badge-maker@3.0.0 start /usr/src/app
|
||||
> node server.js
|
||||
|
||||
http://[::1]/
|
||||
|
||||
@@ -5,7 +5,7 @@ const isSvg = require('is-svg')
|
||||
const got = require('./core/got-test-client')
|
||||
|
||||
let server
|
||||
before(function() {
|
||||
before(function () {
|
||||
this.timeout('5s')
|
||||
// remove args comming from mocha
|
||||
// https://github.com/badges/shields/issues/3365
|
||||
@@ -13,17 +13,14 @@ before(function() {
|
||||
server = require('./server')
|
||||
})
|
||||
|
||||
after('shut down the server', async function() {
|
||||
after('shut down the server', async function () {
|
||||
await server.stop()
|
||||
})
|
||||
|
||||
it('should render a badge', async function() {
|
||||
it('should render a badge', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
'http://localhost:1111/badge/fruit-apple-green.svg'
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
expect(body).to.satisfy(isSvg).and.to.include('fruit').and.to.include('apple')
|
||||
})
|
||||
|
||||
@@ -65,6 +65,7 @@ interface BadgeProps extends React.HTMLAttributes<HTMLImageElement> {
|
||||
display?: 'inline' | 'block' | 'inline-block'
|
||||
height?: string
|
||||
clickable?: boolean
|
||||
object?: boolean
|
||||
}
|
||||
|
||||
export function Badge({
|
||||
@@ -73,11 +74,20 @@ export function Badge({
|
||||
display = 'inline',
|
||||
height = '20px',
|
||||
clickable = false,
|
||||
object = false,
|
||||
...rest
|
||||
}: BadgeProps): JSX.Element {
|
||||
return (
|
||||
<BadgeWrapper clickable={clickable} display={display} height={height}>
|
||||
{src ? <img alt={alt} src={src} {...rest} /> : nonBreakingSpace}
|
||||
{src ? (
|
||||
object ? (
|
||||
<object data={src}>alt</object>
|
||||
) : (
|
||||
<img alt={alt} src={src} {...rest} />
|
||||
)
|
||||
) : (
|
||||
nonBreakingSpace
|
||||
)}
|
||||
</BadgeWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
171
frontend/components/development/style-page.tsx
Normal file
171
frontend/components/development/style-page.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import styled from 'styled-components'
|
||||
// @ts-ingnore
|
||||
import { staticBadgeUrl } from '../../../core/badge-urls/make-badge-url'
|
||||
import { baseUrl } from '../../constants'
|
||||
import Meta from '../meta'
|
||||
// @ts-ignore
|
||||
import Header from '../header'
|
||||
import { H3, Badge } from '../common'
|
||||
|
||||
const StyledTable = styled.table`
|
||||
border: 1px solid #ccc;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
`
|
||||
|
||||
interface BadgeData {
|
||||
label: string
|
||||
message: string
|
||||
labelColor?: string
|
||||
color: string
|
||||
namedLogo?: string
|
||||
links?: string[]
|
||||
}
|
||||
|
||||
function Badges({
|
||||
baseUrl,
|
||||
style,
|
||||
badges,
|
||||
}: {
|
||||
baseUrl: string
|
||||
style: string
|
||||
badges: BadgeData[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{badges.map(({ label, message, labelColor, color, namedLogo, links }) => (
|
||||
<Fragment key={`${label}-${message}-${color}-${namedLogo}`}>
|
||||
<Badge
|
||||
alt="build"
|
||||
object={Boolean(links)}
|
||||
src={staticBadgeUrl({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
labelColor,
|
||||
color,
|
||||
namedLogo,
|
||||
style,
|
||||
links,
|
||||
})}
|
||||
/>
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface StyleExamples {
|
||||
title: string
|
||||
badges: BadgeData[]
|
||||
}
|
||||
|
||||
const examples = [
|
||||
{
|
||||
title: 'Basic examples',
|
||||
badges: [
|
||||
{ label: 'build', message: 'passing', color: 'brightgreen' },
|
||||
{ label: 'tests', message: '5 passing, 1 failed', color: 'red' },
|
||||
{ label: 'python', message: '3.5 | 3.6 | 3.7', color: 'blue' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logo',
|
||||
badges: [
|
||||
{
|
||||
label: 'build',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
namedLogo: 'appveyor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'No left text',
|
||||
badges: [
|
||||
{ label: '', message: 'blueviolet', color: 'blueviolet' },
|
||||
{
|
||||
label: '',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
namedLogo: 'appveyor',
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
labelColor: 'grey',
|
||||
namedLogo: 'appveyor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
badges: [
|
||||
{
|
||||
label: 'badges',
|
||||
message: 'shields',
|
||||
color: 'blue',
|
||||
links: [
|
||||
'https://github.com/badges/',
|
||||
'https://github.com/badges/shields/',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function StyleTable({ style }: { style: string }): JSX.Element {
|
||||
return (
|
||||
<StyledTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>Badges (new)</td>
|
||||
<td>Badges (old)</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{examples.map(({ title, badges }) => (
|
||||
<tr key={title}>
|
||||
<td>{title}</td>
|
||||
<td>
|
||||
<Badges badges={badges} baseUrl={baseUrl} style={style} />
|
||||
</td>
|
||||
<td>
|
||||
<Badges
|
||||
badges={badges}
|
||||
baseUrl="http://img.shields.io"
|
||||
style={style}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledTable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = ['flat', 'flat-square', 'for-the-badge', 'social', 'plastic']
|
||||
|
||||
export default function StylePage(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Meta />
|
||||
<Header />
|
||||
{styles.map(style => (
|
||||
<Fragment key={style}>
|
||||
<H3>{style}</H3>
|
||||
<StyleTable style={style} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -345,7 +345,15 @@ export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
|
||||
snippet="?logo=appveyor"
|
||||
/>
|
||||
<QueryParam
|
||||
documentation={<span>Insert custom logo image (≥ 14px high)</span>}
|
||||
documentation={
|
||||
<span>
|
||||
Insert custom logo image (≥ 14px high). There is a limit on the
|
||||
total size of request headers we can accept (8192 bytes). From a
|
||||
practical perspective, this means the base64-encoded image text
|
||||
is limited to somewhere slightly under 8192 bytes depending on
|
||||
the rest of the request header.
|
||||
</span>
|
||||
}
|
||||
key="logoSvg"
|
||||
snippet="?logo=data:image/png;base64,…"
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import { patternToOptions, removeRegexpFromPattern } from './pattern-helpers'
|
||||
|
||||
describe('Badge URL functions', function() {
|
||||
describe('Badge URL functions', function () {
|
||||
test(patternToOptions, () => {
|
||||
given('[^\\/]+?').expect(undefined)
|
||||
given('abc|[^\\/]+').expect(undefined)
|
||||
|
||||
@@ -2,13 +2,13 @@ import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { findCategory, getDefinitionsForCategory } from '.'
|
||||
|
||||
describe('Service definition helpers', function() {
|
||||
describe('Service definition helpers', function () {
|
||||
test(findCategory, () => {
|
||||
given('build').expect({ id: 'build', name: 'Build', keywords: ['build'] })
|
||||
given('foo').expect(undefined)
|
||||
})
|
||||
|
||||
it('getDefinitionsForCategory', function() {
|
||||
it('getDefinitionsForCategory', function () {
|
||||
expect(getDefinitionsForCategory('build'))
|
||||
.to.have.length.greaterThan(10)
|
||||
.and.lessThan(75)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { test, given, forCases } from 'sazerac'
|
||||
import { predicateFromQuery } from './service-definition-set-helper'
|
||||
import { Example } from '.'
|
||||
|
||||
describe('Badge example functions', function() {
|
||||
describe('Badge example functions', function () {
|
||||
function exampleMatchesQuery(
|
||||
{ examples }: { examples: Example[] },
|
||||
query: string
|
||||
|
||||
@@ -21,6 +21,12 @@ const { categories } = yaml.safeLoad(
|
||||
// https://www.gatsbyjs.org/docs/using-gatsby-without-graphql/#the-approach-fetch-data-and-use-gatsbys-createpages-api
|
||||
async function createPages({ actions: { createPage } }) {
|
||||
if (includeDevPages) {
|
||||
createPage({
|
||||
path: '/dev/styles',
|
||||
component: require.resolve(
|
||||
'./frontend/components/development/style-page.tsx'
|
||||
),
|
||||
})
|
||||
createPage({
|
||||
path: '/dev/logos',
|
||||
component: require.resolve(
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
'use strict'
|
||||
/**
|
||||
* @module gh-badges
|
||||
*/
|
||||
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
/**
|
||||
* BadgeFactory
|
||||
*/
|
||||
class BadgeFactory {
|
||||
constructor(options) {
|
||||
if (options !== undefined) {
|
||||
console.error(
|
||||
'BadgeFactory: Constructor options are deprecated and will be ignored'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a badge
|
||||
*
|
||||
* @param {object} format Object specifying badge data
|
||||
* @param {string[]} format.text Badge text in an array e.g: ['build', 'passing']
|
||||
* @param {string} format.labelColor (Optional) Label color
|
||||
* @param {string} format.color (Optional) Message color
|
||||
* @param {string} format.colorA (Deprecated, Optional) alias for `labelColor`
|
||||
* @param {string} format.colorscheme (Deprecated, Optional) alias for `color`
|
||||
* @param {string} format.colorB (Deprecated, Optional) alias for `color`
|
||||
* @param {string} format.format (Optional) Output format: 'svg' or 'json'
|
||||
* @param {string} format.template (Optional) Visual template e.g: 'flat'
|
||||
* see {@link https://github.com/badges/shields/tree/master/gh-badges/templates}
|
||||
* @returns {string} Badge in SVG or JSON format
|
||||
* @see https://github.com/badges/shields/tree/master/gh-badges/README.md
|
||||
*/
|
||||
create(format) {
|
||||
return makeBadge(format)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BadgeFactory,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const isSvg = require('is-svg')
|
||||
const { BadgeFactory } = require('.')
|
||||
|
||||
const bf = new BadgeFactory()
|
||||
|
||||
describe('BadgeFactory class', function() {
|
||||
it('should produce badge with valid input', function() {
|
||||
expect(
|
||||
bf.create({
|
||||
text: ['build', 'passed'],
|
||||
format: 'svg',
|
||||
colorscheme: 'green',
|
||||
template: 'flat',
|
||||
})
|
||||
).to.satisfy(isSvg)
|
||||
})
|
||||
})
|
||||
@@ -1,185 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const SVGO = require('svgo')
|
||||
const dot = require('dot')
|
||||
const anafanafo = require('anafanafo')
|
||||
const { normalizeColor, toSvgColor } = require('./color')
|
||||
|
||||
// cache templates.
|
||||
const templates = {}
|
||||
const templateFiles = fs.readdirSync(path.join(__dirname, '..', 'templates'))
|
||||
dot.templateSettings.strip = false // Do not strip whitespace.
|
||||
templateFiles.forEach(async filename => {
|
||||
if (filename[0] === '.') {
|
||||
return
|
||||
}
|
||||
const templateData = fs
|
||||
.readFileSync(path.join(__dirname, '..', 'templates', filename))
|
||||
.toString()
|
||||
const extension = path.extname(filename).slice(1)
|
||||
const style = filename.slice(0, -`-template.${extension}`.length)
|
||||
// Compile the template. Necessary to always have a working template.
|
||||
templates[style] = dot.template(templateData)
|
||||
// Substitute dot code.
|
||||
const mapping = new Map()
|
||||
let mappingIndex = 1
|
||||
const untemplatedSvg = templateData.replace(/{{.*?}}/g, match => {
|
||||
// Weird substitution that currently works for all templates.
|
||||
const mapKey = `99999990${mappingIndex}.1`
|
||||
mappingIndex++
|
||||
mapping.set(mapKey, match)
|
||||
return mapKey
|
||||
})
|
||||
|
||||
const svgo = new SVGO()
|
||||
const { data, error } = await svgo.optimize(untemplatedSvg)
|
||||
|
||||
if (error !== undefined) {
|
||||
console.error(
|
||||
`Template ${filename}: ${error}\n` +
|
||||
' Generated untemplated SVG:\n' +
|
||||
`---\n${untemplatedSvg}---\n`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Substitute dot code back.
|
||||
let svg = data
|
||||
const unmappedKeys = []
|
||||
mapping.forEach((value, key) => {
|
||||
let keySubstituted = false
|
||||
svg = svg.replace(RegExp(key, 'g'), () => {
|
||||
keySubstituted = true
|
||||
return value
|
||||
})
|
||||
if (!keySubstituted) {
|
||||
unmappedKeys.push(key)
|
||||
}
|
||||
})
|
||||
if (unmappedKeys.length > 0) {
|
||||
console.error(
|
||||
`Template ${filename} has unmapped keys ` +
|
||||
`${unmappedKeys.join(', ')}.\n` +
|
||||
' Generated untemplated SVG:\n' +
|
||||
`---\n${untemplatedSvg}\n---\n` +
|
||||
' Generated template:\n' +
|
||||
`---\n${svg}\n---\n`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
templates[style] = dot.template(svg)
|
||||
})
|
||||
|
||||
function escapeXml(s) {
|
||||
if (s === undefined || typeof s !== 'string') {
|
||||
return undefined
|
||||
} else {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
||||
}
|
||||
|
||||
/*
|
||||
note: makeBadge() is fairly thinly wrapped so if we are making changes here
|
||||
it is likely this will impact on the package's public interface in index.js
|
||||
*/
|
||||
module.exports = function makeBadge({
|
||||
format,
|
||||
template,
|
||||
text,
|
||||
colorscheme,
|
||||
color,
|
||||
colorA,
|
||||
colorB,
|
||||
labelColor,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
links = ['', ''],
|
||||
}) {
|
||||
// String coercion and whitespace removal.
|
||||
text = text.map(value => `${value}`.trim())
|
||||
|
||||
let [left, right] = text
|
||||
|
||||
color = normalizeColor(color || colorB || colorscheme)
|
||||
labelColor = normalizeColor(labelColor || colorA)
|
||||
|
||||
// This ought to be the responsibility of the server, not `makeBadge`.
|
||||
if (format === 'json') {
|
||||
return JSON.stringify({
|
||||
label: left,
|
||||
message: right,
|
||||
logoWidth,
|
||||
color,
|
||||
labelColor,
|
||||
link: links,
|
||||
name: left,
|
||||
value: right,
|
||||
})
|
||||
}
|
||||
|
||||
if (!(template in templates)) {
|
||||
if (template === 'popout-square') {
|
||||
template = 'flat-square'
|
||||
} else {
|
||||
template = 'flat'
|
||||
}
|
||||
}
|
||||
if (template === 'social') {
|
||||
left = capitalize(left)
|
||||
} else if (template === 'for-the-badge') {
|
||||
left = left.toUpperCase()
|
||||
right = right.toUpperCase()
|
||||
}
|
||||
|
||||
let leftWidth = (anafanafo(left) / 10) | 0
|
||||
// Increase chances of pixel grid alignment.
|
||||
if (leftWidth % 2 === 0) {
|
||||
leftWidth++
|
||||
}
|
||||
let rightWidth = (anafanafo(right) / 10) | 0
|
||||
// Increase chances of pixel grid alignment.
|
||||
if (rightWidth % 2 === 0) {
|
||||
rightWidth++
|
||||
}
|
||||
|
||||
logoWidth = +logoWidth || (logo ? 14 : 0)
|
||||
|
||||
let logoPadding
|
||||
if (left.length === 0) {
|
||||
logoPadding = 0
|
||||
} else {
|
||||
logoPadding = logo ? 3 : 0
|
||||
}
|
||||
|
||||
const context = {
|
||||
text: [left, right],
|
||||
escapedText: [left, right].map(escapeXml),
|
||||
widths: [leftWidth + 10 + logoWidth + logoPadding, rightWidth + 10],
|
||||
links: links.map(escapeXml),
|
||||
logo: escapeXml(logo),
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
logoPadding,
|
||||
colorA: toSvgColor(labelColor),
|
||||
colorB: toSvgColor(color),
|
||||
escapeXml,
|
||||
}
|
||||
|
||||
const templateFn = templates[template]
|
||||
|
||||
// The call to template() can raise an exception.
|
||||
return templateFn(context)
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given, forCases } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const isSvg = require('is-svg')
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
function testColor(color = '', colorAttr = 'colorB') {
|
||||
return JSON.parse(
|
||||
makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
[colorAttr]: color,
|
||||
format: 'json',
|
||||
})
|
||||
).color
|
||||
}
|
||||
|
||||
describe('The badge generator', function() {
|
||||
describe('color test', function() {
|
||||
test(testColor, () => {
|
||||
// valid hex
|
||||
forCases([
|
||||
given('#4c1'),
|
||||
given('#4C1'),
|
||||
given('4C1'),
|
||||
given('4c1'),
|
||||
]).expect('#4c1')
|
||||
forCases([
|
||||
given('#abc123'),
|
||||
given('#ABC123'),
|
||||
given('abc123'),
|
||||
given('ABC123'),
|
||||
]).expect('#abc123')
|
||||
// valid rgb(a)
|
||||
given('rgb(0,128,255)').expect('rgb(0,128,255)')
|
||||
given('rgba(0,128,255,0)').expect('rgba(0,128,255,0)')
|
||||
// valid hsl(a)
|
||||
given('hsl(100, 56%, 10%)').expect('hsl(100, 56%, 10%)')
|
||||
given('hsla(25,20%,0%,0.1)').expect('hsla(25,20%,0%,0.1)')
|
||||
// CSS named color.
|
||||
given('papayawhip').expect('papayawhip')
|
||||
// Shields named color.
|
||||
given('red').expect('red')
|
||||
given('green').expect('green')
|
||||
given('blue').expect('blue')
|
||||
given('yellow').expect('yellow')
|
||||
|
||||
forCases(
|
||||
// invalid hex
|
||||
given('#123red'), // contains letter above F
|
||||
given('#red'), // contains letter above F
|
||||
// invalid rgb(a)
|
||||
given('rgb(220,128,255,0.5)'), // has alpha
|
||||
given('rgba(0,0,255)'), // no alpha
|
||||
// invalid hsl(a)
|
||||
given('hsl(360,50%,50%,0.5)'), // has alpha
|
||||
given('hsla(0,50%,101%)'), // no alpha
|
||||
// neither a css named color nor colorscheme
|
||||
given('notacolor'),
|
||||
given('bluish'),
|
||||
given('almostred'),
|
||||
given('brightmaroon'),
|
||||
given('cactus')
|
||||
).expect(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('color aliases', function() {
|
||||
test(testColor, () => {
|
||||
forCases([
|
||||
given('#4c1', 'color'),
|
||||
given('#4c1', 'colorB'),
|
||||
given('#4c1', 'colorscheme'),
|
||||
]).expect('#4c1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SVG', function() {
|
||||
it('should produce SVG', function() {
|
||||
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||
expect(svg)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('cactus')
|
||||
.and.to.include('grown')
|
||||
})
|
||||
|
||||
it('should always produce the same SVG (unless we have changed something!)', function() {
|
||||
const svg = makeBadge({ text: ['cactus', 'grown'], format: 'svg' })
|
||||
snapshot(svg)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSON', function() {
|
||||
it('should produce the expected JSON', function() {
|
||||
const json = makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'json',
|
||||
links: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
expect(JSON.parse(json)).to.deep.equal({
|
||||
name: 'cactus',
|
||||
label: 'cactus',
|
||||
value: 'grown',
|
||||
message: 'grown',
|
||||
link: ['https://example.com/', 'https://other.example.com/'],
|
||||
})
|
||||
})
|
||||
|
||||
it('should replace unknown svg template with "flat"', function() {
|
||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
template: 'unknown_style',
|
||||
})
|
||||
const jsonBadgeWithDefaultStyle = makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
})
|
||||
expect(jsonBadgeWithUnknownStyle)
|
||||
.to.equal(jsonBadgeWithDefaultStyle)
|
||||
.and.to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
it('should replace "popout-square" svg template with "flat-square"', function() {
|
||||
const jsonBadgeWithUnknownStyle = makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
template: 'popout-square',
|
||||
})
|
||||
const jsonBadgeWithDefaultStyle = makeBadge({
|
||||
text: ['name', 'Bob'],
|
||||
format: 'svg',
|
||||
template: 'flat-square',
|
||||
})
|
||||
expect(jsonBadgeWithUnknownStyle)
|
||||
.to.equal(jsonBadgeWithDefaultStyle)
|
||||
.and.to.satisfy(isSvg)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"for-the-badge" template badge generation', function() {
|
||||
// https://github.com/badges/shields/issues/1280
|
||||
it('numbers should produce a string', function() {
|
||||
const svg = makeBadge({
|
||||
text: [1998, 1999],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
})
|
||||
expect(svg)
|
||||
.to.include('1998')
|
||||
.and.to.include('1999')
|
||||
})
|
||||
|
||||
it('lowercase/mixedcase string should produce uppercase string', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['Label', '1 string'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
})
|
||||
expect(svg)
|
||||
.to.include('LABEL')
|
||||
.and.to.include('1 STRING')
|
||||
})
|
||||
})
|
||||
|
||||
describe('"social" template badge generation', function() {
|
||||
it('should produce capitalized string for badge key', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['some-key', 'some-value'],
|
||||
format: 'svg',
|
||||
template: 'social',
|
||||
})
|
||||
expect(svg)
|
||||
.to.include('Some-key')
|
||||
.and.to.include('some-value')
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/issues/1606
|
||||
it('should handle empty strings used as badge keys', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['', 'some-value'],
|
||||
format: 'json',
|
||||
template: 'social',
|
||||
})
|
||||
expect(svg)
|
||||
.to.include('""')
|
||||
.and.to.include('some-value')
|
||||
})
|
||||
})
|
||||
describe('badges with logos should always produce the same badge', function() {
|
||||
it('shields GitHub logo default color (#333333)', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['label', 'message'],
|
||||
format: 'svg',
|
||||
logo: 'github',
|
||||
})
|
||||
snapshot(svg)
|
||||
})
|
||||
|
||||
it('shields GitHub logo custom color (whitesmoke)', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['label', 'message'],
|
||||
format: 'svg',
|
||||
logo: 'github',
|
||||
logoColor: 'whitesmoke',
|
||||
})
|
||||
snapshot(svg)
|
||||
})
|
||||
|
||||
it('simple-icons javascript logo default color (#F7DF1E)', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['label', 'message'],
|
||||
format: 'svg',
|
||||
logo: 'javascript',
|
||||
})
|
||||
snapshot(svg)
|
||||
})
|
||||
|
||||
it('simple-icons javascript logo custom color (rgba(46,204,113,0.8))', function() {
|
||||
const svg = makeBadge({
|
||||
text: ['label', 'message'],
|
||||
format: 'svg',
|
||||
logo: 'javascript',
|
||||
logoColor: 'rgba(46,204,113,0.8)',
|
||||
})
|
||||
snapshot(svg)
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user