Compare commits

..

17 Commits

Author SHA1 Message Date
Marcin Mielnicki
a39c7901b5 Require default values 2020-04-21 21:09:50 +02:00
Marcin Mielnicki
a1cdd620e9 Comment added plus small refactring 2020-04-21 20:42:13 +02:00
Marcin Mielnicki
d02c3f045a Unused code removed 2020-04-20 21:35:48 +02:00
Marcin Mielnicki
06eb88eb31 Fetch from new domain in tests 2020-04-20 21:31:53 +02:00
Marcin Mielnicki
85f65734a0 Merge remote-tracking branch 'badges/master' into custom-fetch-limit 2020-04-20 21:23:42 +02:00
Marcin Mielnicki
aa185ea07c typo fix 2020-01-31 16:48:44 +01:00
Marcin Mielnicki
b3b772d95c typo fix 2020-01-24 20:50:03 +01:00
Marcin Mielnicki
670dc2bf77 Removed empty line added 2020-01-24 20:45:39 +01:00
Marcin Mielnicki
4b53ffbd3b Bytes type definition 2020-01-20 20:28:42 +01:00
Marcin Mielnicki
18ff7db947 checkCustomIntegrationConfiguration moved to config.js + tests 2020-01-20 20:15:38 +01:00
Marcin Mielnicki
cce0104ea1 Check there is no extra config 2020-01-19 20:33:04 +01:00
Marcin Mielnicki
56a30ef139 Typo fix 2020-01-18 19:58:12 +01:00
Marcin Mielnicki
46fa8adeb9 Use sazerac in config.spec.js 2020-01-12 17:34:58 +01:00
Marcin Mielnicki
f73f828aaf Merge the default configuration with a custom one 2020-01-12 17:20:15 +01:00
Marcin Mielnicki
9e7dfea103 function for configuration 2020-01-12 17:11:19 +01:00
Marcin Mielnicki
9f6f064193 Default values at the top 2020-01-11 18:23:16 +01:00
Marcin Mielnicki
d5812cbce8 Config for custom fetch limit 2020-01-10 21:10:46 +01:00
413 changed files with 9840 additions and 13820 deletions

View File

@@ -109,25 +109,28 @@ 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
environment:
CYPRESS_INSTALL_BINARY: 0
- run:
name: Check types
command: npm run check-types:package
# Run the package tests on each currently supported node version. See:
# https://github.com/badges/shields/blob/master/badge-maker/README.md#node-version-support
# https://github.com/badges/shields/blob/master/gh-badges/README.md#node-version-support
- run:
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/badge-maker/v10/results.xml
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
NODE_VERSION: v10
name: Run package tests on Node 10
@@ -135,25 +138,17 @@ package_steps: &package_steps
<<: *run_package_tests
environment:
mocha_reporter: mocha-junit-reporter
MOCHA_FILE: junit/badge-maker/v12/results.xml
MOCHA_FILE: junit/gh-badges/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:10
- image: circleci/node:8
steps:
- checkout
@@ -165,33 +160,33 @@ jobs:
main:
docker:
- image: circleci/node:10
- image: circleci/node:8
<<: *main_steps
main@node-12:
main@node-latest:
docker:
- image: circleci/node:12
- image: circleci/node:latest
<<: *main_steps
integration:
docker:
- image: circleci/node:10
- image: circleci/node:8
- image: redis
<<: *integration_steps
integration@node-12:
integration@node-latest:
docker:
- image: circleci/node:12
- image: circleci/node:latest
- image: redis
<<: *integration_steps
danger:
docker:
- image: circleci/node:10
- image: circleci/node:8
steps:
- checkout
@@ -211,7 +206,7 @@ jobs:
frontend:
docker:
- image: circleci/node:10
- image: circleci/node:8
steps:
- checkout
@@ -252,19 +247,19 @@ jobs:
services:
docker:
- image: circleci/node:10
- image: circleci/node:8
<<: *services_steps
services@node-12:
services@node-latest:
docker:
- image: circleci/node:12
- image: circleci/node:latest
<<: *services_steps
e2e:
docker:
- image: cypress/base:12
- image: cypress/base:8
steps:
- checkout
@@ -313,11 +308,11 @@ workflows:
filters:
branches:
ignore: gh-pages
- main@node-12:
- main@node-latest:
filters:
branches:
ignore: gh-pages
- integration@node-12:
- integration@node-latest:
filters:
branches:
ignore: gh-pages
@@ -335,7 +330,7 @@ workflows:
ignore:
- master
- gh-pages
- services@node-12:
- services@node-latest:
filters:
branches:
ignore:

View File

@@ -29,8 +29,81 @@ 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: camelcase
version_requirement: '>=6.0.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: escape-string-regexp
version_requirement: '>=3.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'
# badge-maker package dependencies
# gh-badges package dependencies
- package_manager: 'javascript'
directory: '/badge-maker'
directory: '/gh-badges'
update_schedule: 'weekly'

View File

@@ -3,4 +3,4 @@
/coverage
/__snapshots__
/public
badge-maker/node_modules/
gh-badges/node_modules/

View File

@@ -149,7 +149,7 @@ rules:
# allow Joi as an undefined type
jsdoc/no-undefined-types: ['error', { definedTypes: ['Joi'] }]
# all the other recommended rules as errors (not warnings)
# all the other reccomended rules as errors (not warnings)
jsdoc/check-alignment: 'error'
jsdoc/check-param-names: 'error'
jsdoc/check-tag-names: 'error'

View File

@@ -8,7 +8,7 @@ Are you experiencing an issue with...
- [ ] [shields.io](https://shields.io/#/)
- [ ] My own instance
- [ ] [badge-maker NPM package](https://www.npmjs.com/package/badge-maker)
- [ ] [gh-badges NPM package](https://www.npmjs.com/package/gh-badges)
:beetle: **Description**

2
.gitignore vendored
View File

@@ -7,7 +7,7 @@
/private
/index.html
/shields.env
badge-maker/package-lock.json
gh-badges/package-lock.json
# Folder view configuration files
.DS_Store

30
.nowignore Normal file
View File

@@ -0,0 +1,30 @@
*
!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

View File

@@ -10,5 +10,6 @@ package-lock.json
private/*.json
/.nyc_output
analytics.json
gh-badges/templates/default-template.json
supported-features.json
service-definitions.yml

View File

@@ -1,5 +1,5 @@
semi: false
singleQuote: true
trailingComma: es5
bracketSpacing: true
endOfLine: lf
arrowParens: avoid

View File

@@ -1,12 +1,12 @@
FROM node:10-alpine
FROM node:8-alpine
RUN mkdir -p /usr/src/app
RUN mkdir /usr/src/app/private
WORKDIR /usr/src/app
COPY package.json package-lock.json /usr/src/app/
# Without the badge-maker package.json and CLI script in place, `npm ci` will fail.
COPY badge-maker /usr/src/app/badge-maker/
# Without the gh-badges package.json and CLI script in place, `npm ci` will fail.
COPY gh-badges /usr/src/app/gh-badges/
# 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

View File

@@ -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][badge-maker]
- [documentation][badge-maker-docs]
- [changelog][badge-maker-changelog]
- An [NPM library for generating badges][gh-badges]
- [documentation][gh-badges-docs]
- [changelog][gh-badges-changelog]
- The [badge design specification][badge-spec]
[shields.io]: https://shields.io/
[badge-maker]: https://www.npmjs.com/package/badge-maker
[gh-badges]: https://www.npmjs.com/package/gh-badges
[badge-spec]: https://github.com/badges/shields/tree/master/spec
[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
[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
## Examples
@@ -191,11 +191,8 @@ Maintainers:
Operations:
- [calebcartwright](https://github.com/calebcartwright)
- [chris48s](https://github.com/chris48s)
- [espadrine](https://github.com/espadrine)
- [paulmelnikow](https://github.com/paulmelnikow)
- [PyvesB](https://github.com/PyvesB)
- [espadrine](https://github.com/espadrine) (sysadmin)
- [paulmelnikow](https://github.com/paulmelnikow) (limited access)
Alumni:

View File

@@ -1,25 +0,0 @@
# Security Policy
## Supported Projects
Please follow this guidance when reporting security issues affecting:
- [Shields.io](https://shields.io)
- [Raster.shields.io](https://raster.shields.io)
- Self-hosted Shields instances
- The [svg-to-image-proxy](https://www.npmjs.com/package/svg-to-image-proxy) NPM package
- The [badge-maker](https://www.npmjs.com/package/badge-maker) NPM package
The [gh-badges](https://www.npmjs.com/package/gh-badges) NPM package is now deprecated and will no longer receive fixes for bugs or security issues.
## Reporting a Vulnerability
If you find a security vulnerability affecting any of our supported projects, please email [security@shields.io](mailto:security@shields.io), rather than opening a public issue on GitHub. After receiving the initial report, we will endeavor to keep you informed of the progress towards a fix and full announcement. We may ask you for additional information. You are also welcome to propose a patch or solution.
Report security bugs in third-party modules to the person or team maintaining the module.
## Coordinated Disclosure
We aim to patch confirmed vulnerabilities within 90 days or less, disclosing the details of those vulnerabilities when a patch is published. We ask that you refrain from sharing your report with others while we work on our patch.
We may want to coordinate an advisory with you to be published simultaneously with the patch, but you are also welcome to self-disclose after 90 days if you prefer. We will never publish information about you or our communications with you without your permission.

View File

@@ -1,127 +1,19 @@
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 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 "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 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, 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 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 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 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, 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>
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>
`

View File

@@ -1,11 +0,0 @@
interface Format {
label?: string
message: string
labelColor?: string
color?: string
style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social'
}
export declare class ValidationError extends Error {}
export declare function makeBadge(format: Format): string

View File

@@ -1,35 +0,0 @@
import { expectType, expectError, expectAssignable } from 'tsd'
import { makeBadge, ValidationError } from '.'
expectError(makeBadge('string is invalid'))
expectError(makeBadge({}))
expectError(
makeBadge({
message: 'passed',
style: 'invalid style',
})
)
expectType<string>(
makeBadge({
message: 'passed',
})
)
expectType<string>(
makeBadge({
label: 'build',
message: 'passed',
})
)
expectType<string>(
makeBadge({
label: 'build',
message: 'passed',
labelColor: 'green',
color: 'red',
style: 'flat',
})
)
const error = new ValidationError()
expectAssignable<Error>(error)

View File

@@ -1,610 +0,0 @@
'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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
}
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 }

View File

@@ -1,87 +0,0 @@
'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,
}

View File

@@ -1,75 +0,0 @@
'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)'
)
})
})

View File

@@ -1,64 +0,0 @@
'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,
})
}

View File

@@ -1,563 +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 = '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)
})
})
})

View File

@@ -62,9 +62,9 @@ public:
rateLimit: 'RATE_LIMIT'
fetchLimit: 'FETCH_LIMIT'
shieldsProductionHerokuHacks: 'SHIELDS_PRODUCTION_HEROKU_HACKS'
integrations:
default:
fetchLimit: 'FETCH_LIMIT'
private:
azure_devops_token: 'AZURE_DEVOPS_TOKEN'

View File

@@ -34,8 +34,17 @@ public:
handleInternalErrors: true
fetchLimit: '10MB'
integrations:
default:
fetchLimit: '10MB'
shieldsProductionHerokuHacks: false
DynamicJson:
fetchLimit: '2MB'
DynamicXml:
fetchLimit: '128KB'
DynamicYaml:
fetchLimit: '64KB'
private: {}

View File

@@ -2,12 +2,7 @@ public:
metrics:
prometheus:
enabled: true
influx:
enabled: true
url: https://metrics.shields.io/telegraf
instanceIdFrom: env-var
instanceIdEnvVarName: HEROKU_DYNO_ID
envLabel: shields-production
endpointEnabled: true
ssl:
isSecure: true

View File

@@ -38,22 +38,18 @@ 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({

View File

@@ -59,19 +59,15 @@ 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}` : ''

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 },

View File

@@ -1,6 +1,6 @@
'use strict'
const makeBadge = require('../../badge-maker/lib/make-badge')
const makeBadge = require('../../gh-badges/lib/make-badge')
const BaseService = require('./base')
const { MetricHelper } = require('./metric-helper')
const { setCacheHeaders } = require('./cache-headers')

View File

@@ -1,6 +1,6 @@
'use strict'
const makeBadge = require('../../badge-maker/lib/make-badge')
const makeBadge = require('../../gh-badges/lib/make-badge')
const BaseService = require('./base')
const {
serverHasBeenUpSinceResourceCached,

View File

@@ -3,7 +3,7 @@
const { expect } = require('chai')
const sinon = require('sinon')
const Joi = require('@hapi/joi')
const makeBadge = require('../../badge-maker/lib/make-badge')
const makeBadge = require('../../gh-badges/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 },

View File

@@ -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 },

View File

@@ -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 },

View File

@@ -3,6 +3,7 @@
* @module
*/
const bytes = require('bytes')
// See available emoji at http://emoji.muan.co/
const emojic = require('emojic')
const Joi = require('@hapi/joi')
@@ -58,7 +59,10 @@ 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(),
@@ -67,7 +71,9 @@ 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')
@@ -420,7 +426,8 @@ class BaseService {
{ camp, handleRequest, githubApiProvider, metricInstance },
serviceConfig
) {
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
const { cacheHeaders: cacheHeaderConfig, fetchLimit } = serviceConfig
const fetchLimitBytes = bytes.parse(fetchLimit)
const { regex, captureNames } = prepareRoute(this.route)
const queryParams = getQueryParamNames(this.route)

View File

@@ -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: 'flat',
template: undefined,
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(
{},

View File

@@ -7,7 +7,9 @@ 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,
@@ -67,7 +69,7 @@ function setHeadersForCacheLength(res, cacheLengthSeconds) {
cacheControl = 'no-cache, no-store, must-revalidate'
expires = nowGMTString
} else {
cacheControl = `max-age=${cacheLengthSeconds}, s-maxage=${cacheLengthSeconds}`
cacheControl = `max-age=${cacheLengthSeconds}`
expires = new Date(now.getTime() + cacheLengthSeconds * 1000).toGMTString()
}
@@ -92,7 +94,7 @@ function setCacheHeaders({
setHeadersForCacheLength(res, cacheLengthSeconds)
}
const staticCacheControlHeader = `max-age=${24 * 3600}, s-maxage=${24 * 3600}` // 1 day.
const staticCacheControlHeader = `max-age=${24 * 3600}` // 1 day.
function setCacheHeadersForStaticResource(res) {
res.setHeader('Cache-Control', staticCacheControlHeader)
res.setHeader('Last-Modified', serverStartTimeGMTString)

View File

@@ -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,42 +124,40 @@ 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, s-maxage=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 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)
@@ -180,18 +178,16 @@ 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}, s-maxage=${24 * 3600}`
)
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 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.
@@ -201,17 +197,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' },
})
@@ -220,8 +216,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' },
})
@@ -231,8 +227,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() },

View File

@@ -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) {

View File

@@ -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 })

View File

@@ -104,23 +104,7 @@ module.exports = function coalesceBadge(
labelColor: defaultLabelColor,
} = defaultBadgeData
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'
}
const style = coalesce(overrideStyle, serviceStyle)
let namedLogo, namedLogoColor, logoWidth, logoPosition, logoSvgBase64
if (overrideLogo) {

View File

@@ -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,27 +277,14 @@ describe('coalesceBadge', function () {
})
})
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('Style', function() {
it('overrides the template', function() {
expect(coalesceBadge({ style: 'pill' }, {}, {}).template).to.equal('pill')
})
})
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

View File

@@ -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([])

View File

@@ -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({

View File

@@ -21,12 +21,19 @@ 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()

View File

@@ -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: {

View File

@@ -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)

View File

@@ -2,7 +2,7 @@
const request = require('request')
const queryString = require('query-string')
const makeBadge = require('../../badge-maker/lib/make-badge')
const makeBadge = require('../../gh-badges/lib/make-badge')
const { setCacheHeaders } = require('./cache-headers')
const {
Inaccessible,

View File

@@ -3,7 +3,7 @@
const { expect } = require('chai')
const nock = require('nock')
const portfinder = require('portfinder')
const Camp = require('@shields_io/camp')
const Camp = require('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`, {
responseType: 'json',
json: true,
})
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`, {
responseType: 'json',
json: true,
})
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`, {
responseType: 'json',
json: true,
})
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`, {
responseType: 'json',
json: true,
})
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, s-maxage=900')
expect(headers['cache-control']).to.equal('max-age=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, s-maxage=900')
expect(headers['cache-control']).to.equal('max-age=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, s-maxage=400')
expect(headers['cache-control']).to.equal('max-age=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, s-maxage=300')
expect(headers['cache-control']).to.equal('max-age=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, s-maxage=3600')
expect(headers['cache-control']).to.equal('max-age=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, s-maxage=600')
expect(headers['cache-control']).to.equal('max-age=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',

View File

@@ -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',

View File

@@ -126,7 +126,7 @@ Cache.prototype = {
}
},
clear: function () {
clear: function() {
this.cache.clear()
this.newest = null
this.oldest = null

View File

@@ -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')

View File

@@ -1,12 +1,12 @@
'use strict'
const Camp = require('@shields_io/camp')
const Camp = require('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',

View File

@@ -9,7 +9,9 @@ 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', {

View File

@@ -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(),

View File

@@ -5,7 +5,10 @@ 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))
@@ -36,7 +39,9 @@ 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(),
@@ -65,7 +70,9 @@ const serviceDefinitionExport = Joi.object({
})
)
.required(),
services: Joi.array().items(serviceDefinition).required(),
services: Joi.array()
.items(serviceDefinition)
.required(),
}).required()
function assertValidServiceDefinitionExport(examples, message = undefined) {

View File

@@ -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 },

38
core/server/config.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict'
const deepmerge = require('deepmerge')
class RedundantCustomConfiguration extends Error {
constructor(message) {
super(message)
this.name = 'RedundantCustomConfiguration'
}
}
function merge(_default, custom) {
// Overwrites the existing array values completely rather than concatenating them
// recipe from https://github.com/TehShrike/deepmerge#arraymerge
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
return deepmerge(_default, custom, {
arrayMerge: overwriteMerge,
})
}
function checkCustomIntegrationConfiguration(config, serviceClasses) {
const serviceNames = new Set(
serviceClasses.map(serviceClass => serviceClass.name)
)
const redundantConfigurations = Object.keys(config.public.integrations)
.filter(configName => configName !== 'default')
.filter(configName => !serviceNames.has(configName))
if (redundantConfigurations.length) {
throw new RedundantCustomConfiguration(
`Custom configurations found without a corresponding service: ${redundantConfigurations}`
)
}
}
module.exports = {
RedundantCustomConfiguration,
merge,
checkCustomIntegrationConfiguration,
}

View File

@@ -0,0 +1,61 @@
'use strict'
const { test, given } = require('sazerac')
const { expect } = require('chai')
const {
RedundantCustomConfiguration,
merge,
checkCustomIntegrationConfiguration,
} = require('./config')
describe('configuration', function() {
test(merge, function() {
given({ a: 2 }, {})
.describe('copies the default value')
.expect({ a: 2 })
given({ a: 2 }, { a: 3 })
.describe('overrides a primitive value')
.expect({ a: 3 })
given({ a: { a1: 1, a2: 2 } }, { a: { a1: 2 } })
.describe('merges objects')
.expect({ a: { a1: 2, a2: 2 } })
given({ a: { a1: 1, a2: 2 } }, { a: 3 })
.describe('overrides an object with a primitive')
.expect({ a: 3 })
given({ a: { a1: 1, a2: 2 } }, { a: {} })
.describe('does not override an object with an empty object')
.expect({ a: { a1: 1, a2: 2 } })
given({ a: [2, 3, 4] }, { a: [5, 6] })
.describe('overrides array')
.expect({ a: [5, 6] })
})
describe('checkCustomIntegrationConfiguration function', function() {
it('accepts the default configuration', function() {
const config = { public: { integrations: { default: {} } } }
const serviceClasses = [{ name: 'SomeService' }]
expect(() =>
checkCustomIntegrationConfiguration(config, serviceClasses)
).not.throw()
})
it('accepts a configuration for an existing service', function() {
const config = { public: { integrations: { SomeService: {} } } }
const serviceClasses = [{ name: 'SomeService' }]
expect(() =>
checkCustomIntegrationConfiguration(config, serviceClasses)
).not.throw()
})
it('throws an error if a custom config does not have a corresponding service', function() {
const config = { public: { integrations: { UnknownService: {} } } }
const serviceClasses = [{ name: 'KnownService' }]
expect(() =>
checkCustomIntegrationConfiguration(config, serviceClasses)
).to.throw(RedundantCustomConfiguration)
})
})
})

View File

@@ -6,7 +6,7 @@ const { expect } = require('chai')
const log = require('./log')
const InfluxMetrics = require('./influx-metrics')
require('../register-chai-plugins.spec')
describe('Influx metrics', function () {
describe('Influx metrics', function() {
const metricInstance = {
metrics() {
return [
@@ -20,16 +20,16 @@ describe('Influx metrics', function () {
]
},
}
describe('"metrics" function', function () {
describe('"metrics" function', function() {
let osHostnameStub
afterEach(function () {
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 () {
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',
@@ -39,7 +39,7 @@ describe('Influx metrics', function () {
expect(influxMetrics.metrics()).to.contain('instance=instance3')
})
it('should use a hostname as an instance label', async function () {
it('should use a hostname as an instance label', async function() {
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
const customConfig = {
instanceIdFrom: 'hostname',
@@ -49,7 +49,7 @@ describe('Influx metrics', function () {
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname')
})
it('should use a random string as an instance label', async function () {
it('should use a random string as an instance label', async function() {
const customConfig = {
instanceIdFrom: 'random',
}
@@ -58,7 +58,7 @@ describe('Influx metrics', function () {
expect(influxMetrics.metrics()).to.be.match(/instance=\w+ /)
})
it('should use a hostname alias as an instance label', async function () {
it('should use a hostname alias as an instance label', async function() {
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
const customConfig = {
instanceIdFrom: 'hostname',
@@ -72,12 +72,12 @@ describe('Influx metrics', function () {
})
})
describe('startPushingMetrics', function () {
describe('startPushingMetrics', function() {
let influxMetrics, clock
beforeEach(function () {
beforeEach(function() {
clock = sinon.useFakeTimers()
})
afterEach(function () {
afterEach(function() {
influxMetrics.stopPushingMetrics()
nock.cleanAll()
nock.enableNetConnect()
@@ -85,7 +85,7 @@ describe('Influx metrics', function () {
clock.restore()
})
it('should send metrics', async function () {
it('should send metrics', async function() {
const scope = nock('http://shields-metrics.io/', {
reqheaders: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -120,11 +120,11 @@ describe('Influx metrics', function () {
})
})
describe('sendMetrics', function () {
beforeEach(function () {
sinon.stub(log, 'error')
describe('sendMetrics', function() {
beforeEach(function() {
sinon.spy(log, 'error')
})
afterEach(function () {
afterEach(function() {
log.error.restore()
nock.cleanAll()
nock.enableNetConnect()
@@ -137,7 +137,7 @@ describe('Influx metrics', function () {
username: 'metrics-username',
password: 'metrics-password',
})
it('should log errors', async function () {
it('should log errors', async function() {
nock.disableNetConnect()
await influxMetrics.sendMetrics()
@@ -154,8 +154,11 @@ describe('Influx metrics', function () {
)
})
it('should log error responses', async function () {
nock('http://shields-metrics.io/').persist().post('/metrics').reply(400)
it('should log error responses', async function() {
nock('http://shields-metrics.io/')
.persist()
.post('/metrics')
.reply(400)
await influxMetrics.sendMetrics()

View File

@@ -2,7 +2,9 @@
function generateInstanceId() {
// from https://gist.github.com/gordonbrander/2230317
return Math.random().toString(36).substr(2, 9)
return Math.random()
.toString(36)
.substr(2, 9)
}
module.exports = generateInstanceId

View File

@@ -4,9 +4,9 @@ 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 () {
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',
@@ -22,7 +22,7 @@ describe('Metric format converters', function () {
expect(influx).to.be.equal('prometheus counter1=11')
})
it('converts a counter (from prometheus registry)', function () {
it('converts a counter (from prometheus registry)', function() {
const register = new prometheus.Registry()
const counter = new prometheus.Counter({
name: 'counter1',
@@ -36,7 +36,7 @@ describe('Metric format converters', function () {
expect(influx).to.be.equal('prometheus counter1=11')
})
it('converts a gauge', function () {
it('converts a gauge', function() {
const json = [
{
help: 'gause 1 help',
@@ -52,7 +52,7 @@ describe('Metric format converters', function () {
expect(influx).to.be.equal('prometheus gauge1=20')
})
it('converts a gauge (from prometheus registry)', function () {
it('converts a gauge (from prometheus registry)', function() {
const register = new prometheus.Registry()
const gauge = new prometheus.Gauge({
name: 'gauge1',
@@ -66,9 +66,13 @@ describe('Metric format converters', function () {
expect(influx).to.be.equal('prometheus gauge1=20')
})
const sortLines = text => text.split('\n').sort().join('\n')
const sortLines = text =>
text
.split('\n')
.sort()
.join('\n')
it('converts a histogram', function () {
it('converts a histogram', function() {
const json = [
{
name: 'histogram1',
@@ -101,7 +105,7 @@ prometheus histogram1_count=3,histogram1_sum=111`)
)
})
it('converts a histogram (from prometheus registry)', function () {
it('converts a histogram (from prometheus registry)', function() {
const register = new prometheus.Registry()
const histogram = new prometheus.Histogram({
name: 'histogram1',
@@ -124,7 +128,7 @@ prometheus histogram1_count=3,histogram1_sum=111`)
)
})
it('converts a summary', function () {
it('converts a summary', function() {
const json = [
{
name: 'summary1',
@@ -151,7 +155,7 @@ prometheus summary1_count=3,summary1_sum=111`)
)
})
it('converts a summary (from prometheus registry)', function () {
it('converts a summary (from prometheus registry)', function() {
const register = new prometheus.Registry()
const summary = new prometheus.Summary({
name: 'summary1',
@@ -173,7 +177,7 @@ prometheus summary1_count=3,summary1_sum=111`)
)
})
it('converts a counter and skip a timestamp', function () {
it('converts a counter and skip a timestamp', function() {
const json = [
{
help: 'counter 4 help',
@@ -189,7 +193,7 @@ prometheus summary1_count=3,summary1_sum=111`)
expect(influx).to.be.equal('prometheus counter4=11')
})
it('converts a counter and adds extra labels', function () {
it('converts a counter and adds extra labels', function() {
const json = [
{
help: 'counter 1 help',

View File

@@ -39,7 +39,10 @@ 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)) {
@@ -88,7 +91,7 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
})
})
return function () {
return function() {
ipRateLimit.stop()
badgeTypeRateLimit.stop()
refererRateLimit.stop()

View File

@@ -95,10 +95,6 @@ module.exports = class PrometheusMetrics {
}
/**
* @param {object} attrs Refer to individual attrs
* @param {string} attrs.category e.g: 'build'
* @param {string} attrs.serviceFamily e.g: 'npm'
* @param {string} attrs.name e.g: 'NpmVersion'
* @returns {object} `{ inc() {} }`.
*/
createNumRequestCounter({ category, serviceFamily, name }) {

View File

@@ -1,20 +1,20 @@
'use strict'
const { expect } = require('chai')
const Camp = require('@shields_io/camp')
const Camp = require('camp')
const portfinder = require('portfinder')
const got = require('../got-test-client')
const Metrics = require('./prometheus-metrics')
describe('Prometheus metrics route', function () {
describe('Prometheus metrics route', function() {
let port, baseUrl, camp, metrics
beforeEach(async function () {
beforeEach(async function() {
port = await portfinder.getPortPromise()
baseUrl = `http://127.0.0.1:${port}`
camp = Camp.start({ port, hostname: '::' })
await new Promise(resolve => camp.on('listening', () => resolve()))
})
afterEach(async function () {
afterEach(async function() {
if (metrics) {
metrics.stop()
}
@@ -24,7 +24,7 @@ describe('Prometheus metrics route', function () {
}
})
it('returns default metrics', async function () {
it('returns default metrics', async function() {
metrics = new Metrics()
metrics.registerMetricsEndpoint(camp)

View File

@@ -6,10 +6,9 @@
const path = require('path')
const url = require('url')
const { URL } = url
const bytes = require('bytes')
const Camp = require('@shields_io/camp')
const Camp = require('camp')
const originalJoi = require('@hapi/joi')
const makeBadge = require('../../badge-maker/lib/make-badge')
const makeBadge = require('../../gh-badges/lib/make-badge')
const GithubConstellation = require('../../services/github/github-constellation')
const suggest = require('../../services/suggest')
const { loadServiceClasses } = require('../base-service/loader')
@@ -20,6 +19,7 @@ const {
} = require('../base-service/legacy-request-handler')
const { clearRegularUpdateCache } = require('../legacy/regular-update')
const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
const { merge, checkCustomIntegrationConfiguration } = require('./config')
const log = require('./log')
const sysMonitor = require('./monitor')
const PrometheusMetrics = require('./prometheus-metrics')
@@ -59,6 +59,13 @@ const Joi = originalJoi
const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
const requiredUrl = optionalUrl.required()
const bytes = Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i)
const requireFields = {
required: schema => schema.required(),
}
const integrationSchema = Joi.object({
fetchLimit: bytes.alter(requireFields),
})
const origins = Joi.arrayFromString().items(Joi.string().origin())
const defaultService = Joi.object({ authorizedOrigins: origins }).default({
authorizedOrigins: [],
@@ -71,8 +78,12 @@ 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: {
@@ -115,7 +126,9 @@ 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(),
@@ -127,7 +140,10 @@ 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,
@@ -143,12 +159,15 @@ 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(),
integrations: Joi.object({
default: integrationSchema.tailor('required').required(),
}).pattern(Joi.string(), integrationSchema),
}).required()
const privateConfigSchema = Joi.object({
@@ -381,21 +400,25 @@ class Server {
const { config, camp, metricInstance } = this
const { apiProvider: githubApiProvider } = this.githubConstellation
loadServiceClasses().forEach(serviceClass =>
const serviceClasses = loadServiceClasses()
checkCustomIntegrationConfiguration(config, serviceClasses)
serviceClasses.forEach(serviceClass => {
const serviceConfig = merge(
config.public.integrations.default,
config.public.integrations[serviceClass.name] || {}
)
serviceClass.register(
{ camp, handleRequest, githubApiProvider, metricInstance },
{
handleInternalErrors: config.public.handleInternalErrors,
cacheHeaders: config.public.cacheHeaders,
fetchLimitBytes: bytes(config.public.fetchLimit),
rasterUrl: config.public.rasterUrl,
private: config.private,
public: config.public,
shieldsProductionHerokuHacks:
config.public.shieldsProductionHerokuHacks,
...serviceConfig,
}
)
)
})
}
/**
@@ -414,7 +437,7 @@ class Server {
log(`Server is starting up: ${this.baseUrl}`)
const camp = (this.camp = Camp.create({
const camp = (this.camp = Camp.start({
documentRoot: path.resolve(__dirname, '..', '..', 'public'),
port,
hostname,
@@ -430,7 +453,7 @@ class Server {
)
const { githubConstellation } = this
await githubConstellation.initialize(camp)
githubConstellation.initialize(camp)
if (metricInstance) {
if (this.config.public.metrics.prometheus.endpointEnabled) {
metricInstance.registerMetricsEndpoint(camp)
@@ -447,8 +470,6 @@ class Server {
this.registerRedirects()
this.registerServices()
camp.listenAsConfigured()
await new Promise(resolve => camp.on('listening', () => resolve()))
}

View File

@@ -7,24 +7,24 @@ const got = require('../got-test-client')
const Server = require('./server')
const { createTestServer } = require('./in-process-server-test-helpers')
describe('The server', function () {
describe('running', function () {
describe('The server', function() {
describe('running', function() {
let server, baseUrl
before('Start the server', async function () {
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 () {
after('Shut down the server', async function() {
if (server) {
await server.stop()
}
server = undefined
})
it('should allow strings for port', async function () {
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: {
@@ -36,7 +36,7 @@ describe('The server', function () {
expect(pipeServer).to.not.be.undefined
})
it('should produce colorscheme badges', async function () {
it('should produce colorscheme badges', async function() {
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
expect(statusCode).to.equal(200)
expect(body)
@@ -45,7 +45,7 @@ describe('The server', function () {
.and.to.include('apple')
})
it('should redirect colorscheme PNG badges as configured', async function () {
it('should redirect colorscheme PNG badges as configured', async function() {
const { statusCode, headers } = await got(
`${baseUrl}:fruit-apple-green.png`,
{
@@ -58,7 +58,7 @@ describe('The server', function () {
)
})
it('should redirect modern PNG badges as configured', async function () {
it('should redirect modern PNG badges as configured', async function() {
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
followRedirect: false,
})
@@ -68,7 +68,7 @@ describe('The server', function () {
)
})
it('should produce json badges', async function () {
it('should produce json badges', async function() {
const { statusCode, body, headers } = await got(
`${baseUrl}twitter/follow/_Pyves.json`
)
@@ -77,14 +77,16 @@ describe('The server', function () {
expect(() => JSON.parse(body)).not.to.throw()
})
it('should preserve label case', async function () {
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')
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 () {
it('should not crash with a numeric logo', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?logo=1`
)
@@ -95,7 +97,7 @@ describe('The server', function () {
.and.to.include('apple')
})
it('should not crash with a numeric link', async function () {
it('should not crash with a numeric link', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?link=1`
)
@@ -106,7 +108,7 @@ describe('The server', function () {
.and.to.include('apple')
})
it('should not crash with a boolean link', async function () {
it('should not crash with a boolean link', async function() {
const { statusCode, body } = await got(
`${baseUrl}:fruit-apple-green.svg?link=true`
)
@@ -117,7 +119,7 @@ describe('The server', function () {
.and.to.include('apple')
})
it('should return the 404 badge for unknown badges', async function () {
it('should return the 404 badge for unknown badges', async function() {
const { statusCode, body } = await got(
`${baseUrl}this/is/not/a/badge.svg`,
{
@@ -131,7 +133,7 @@ describe('The server', function () {
.and.to.include('badge not found')
})
it('should return the 404 badge page for rando links', async function () {
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`,
{
@@ -145,7 +147,7 @@ describe('The server', function () {
.and.to.include('badge not found')
})
it('should redirect the root as configured', async function () {
it('should redirect the root as configured', async function() {
const { statusCode, headers } = await got(baseUrl, {
followRedirect: false,
})
@@ -155,7 +157,7 @@ describe('The server', function () {
expect(headers.location).to.equal('http://frontend.example.test')
})
it('should return the 410 badge for obsolete formats', async function () {
it('should return the 410 badge for obsolete formats', async function() {
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
throwHttpErrors: false,
})
@@ -168,15 +170,15 @@ describe('The server', function () {
})
})
describe('configuration', function () {
describe('configuration', function() {
let server
afterEach(async function () {
afterEach(async function() {
if (server) {
server.stop()
}
})
it('should allow to enable prometheus metrics', async function () {
it('should allow to enable prometheus metrics', async function() {
// Fixes https://github.com/badges/shields/issues/2611
this.timeout(10000)
server = await createTestServer({
@@ -191,7 +193,7 @@ describe('The server', function () {
expect(statusCode).to.be.equal(200)
})
it('should allow to disable prometheus metrics', async function () {
it('should allow to disable prometheus metrics', async function() {
// Fixes https://github.com/badges/shields/issues/2611
this.timeout(10000)
server = await createTestServer({
@@ -209,10 +211,10 @@ describe('The server', function () {
})
})
describe('configuration validation', function () {
describe('influx', function () {
describe('configuration validation', function() {
describe('influx', function() {
let customConfig
beforeEach(function () {
beforeEach(function() {
customConfig = config.util.toObject()
customConfig.public.metrics.influx = {
enabled: true,
@@ -230,46 +232,46 @@ describe('The server', function () {
}
})
it('should not require influx configuration', function () {
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 () {
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 () {
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 () {
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 () {
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 () {
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 () {
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(
@@ -277,53 +279,53 @@ describe('The server', function () {
)
})
it('should allow instanceIdFrom = hostname', function () {
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 () {
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 () {
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 () {
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 () {
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 () {
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 () {
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 () {
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 () {
it('should allow other private keys', function() {
customConfig.private.gh_token = 'my-token'
expect(() => new Server(customConfig)).to.not.throw()
})

View File

@@ -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. 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. 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.
// 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,7 +73,7 @@ if (process.env.TESTED_SERVER_URL) {
} else {
const port = 1111
baseUrl = 'http://localhost:1111'
before('Start running the server', async function () {
before('Start running the server', async function() {
server = await createTestServer({
public: {
bind: {
@@ -83,7 +83,7 @@ if (process.env.TESTED_SERVER_URL) {
})
server.start()
})
after('Shut down the server', async function () {
after('Shut down the server', async function() {
if (server) {
await server.stop()
}
@@ -131,3 +131,5 @@ if (typeof onlyServices === 'undefined' || onlyServices.includes('*****')) {
}
runner.toss()
// Invoke run() asynchronously, because Mocha will not start otherwise.
process.nextTick(run)

View File

@@ -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'),

View File

@@ -29,7 +29,7 @@ async function getTitle(owner, repo, pullRequest) {
'User-Agent': 'badges/shields',
Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
responseType: 'json',
json: true,
}
)
return title

View File

@@ -25,7 +25,7 @@ class ServiceTester {
* Specifies which tests to run from the CLI or pull requests
* @param {string} attrs.title
* Prints in the Mocha output
* @param {string} attrs.pathPrefix
* @param {string} attrs.path
* Prefix which is automatically prepended to each tested URI.
* The default is `/${attrs.id}`.
*/
@@ -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 {
@@ -119,20 +119,20 @@ class ServiceTester {
* @param {number} attrs.retry.count number of times to retry test
* @param {number} attrs.retry.backoff number of milliseconds to add to the wait between each retry
*/
toss({ baseUrl, skipIntercepted, retry: { count, backoff } }) {
toss({ baseUrl, skipIntercepted, retry }) {
const { specs, pathPrefix } = this
const testerBaseUrl = `${baseUrl}${pathPrefix}`
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
}`
if (!skipIntercepted || !spec.intercepted) {
spec.baseUri(testerBaseUrl)
spec.retry(count, backoff)
spec.retry(retry.count, retry.backoff)
spec.toss()
}
})

View File

@@ -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([

View File

@@ -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()

View File

@@ -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()

View File

@@ -13,7 +13,10 @@ 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() {

View File

@@ -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')
})

View File

@@ -1,17 +1,19 @@
'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')
@@ -19,7 +21,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(
@@ -29,7 +31,7 @@ describe('Main page', function () {
)
})
it('Suggest badges', function () {
it('Suggest badges', function() {
const badgeUrl = `${backendUrl}/github/issues/badges/shields`
cy.visit('/')
@@ -39,7 +41,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')
@@ -51,7 +53,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')

View File

@@ -16,8 +16,9 @@ const { fileMatch } = danger.git
const documentation = fileMatch(
'**/*.md',
'frontend/components/usage.tsx',
'frontend/pages/endpoint.tsx'
'lib/all-badge-examples.js',
'frontend/components/usage.js',
'frontend/pages/endpoint.js'
)
const server = fileMatch('core/server/**.js', '!*.spec.js')
const serverTests = fileMatch('core/server/**.spec.js')

View File

@@ -25,7 +25,7 @@ and learn about the [Github workflow](http://try.github.io/).
#### Node, NPM
Node 10 or later is required. If you don't already have them,
Node 8 or later is required. If you don't already have them,
install node and npm: https://nodejs.org/en/download/
### Setup a dev install

View File

@@ -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. [`badge-maker`][badge-maker]
1. [`gh-badges`][gh-badges]
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
[badge-maker]: https://github.com/badges/shields/tree/master/badge-maker
[gh-badges]: https://github.com/badges/shields/tree/master/gh-badges
[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. `badge-maker/**/*.spec.js`
1. `gh-badges/**/*.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

View File

@@ -36,7 +36,38 @@
There are [too many bottlenecks][issue 2577]!
[issue 2577]: https://github.com/badges/shields/issues/2577
## Badge servers
There are three public badge servers on OVH VPSs.
| 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
## Attached state
@@ -116,25 +147,64 @@ hosted on [Zeit Now][]. It's managed in the
## Deployment
The deployment is done in two stages: the badge server (heroku) and the front-end (gh-pages).
To set things up for deployment:
### 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.
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:
```sh
$ 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
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
```
No secrets are required to build or deploy the frontend.
`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 1060 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`.
## DNS
@@ -144,7 +214,7 @@ DNS is registered with [DNSimple][].
## Logs
Logs can be retrieved [from heroku](https://devcenter.heroku.com/articles/logging#log-retrieval).
Logs are available on the individual servers via SSH.
## Error reporting
@@ -178,18 +248,11 @@ Request performance is monitored in two places:
[notifications]: http://shields.redsparr0w.com/discord_notification
[monitor discord]: https://discordapp.com/channels/308323056592486420/470700909182320646
## Legacy servers
## Known limitations
There are three legacy servers on OVH VPSs which are currently used for proxying.
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])
| 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`.
[issue 2577]: https://github.com/badges/shields/issues/2577
[issue 1988]: https://github.com/badges/shields/issues/1988

View File

@@ -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]

View File

@@ -2,7 +2,7 @@
## Installation
You will need Node 10 or later, which you can install using a
You will need Node 8 or later, which you can install using a
[package manager][].
On Ubuntu / Debian:
@@ -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
> badge-maker@3.0.0 start /usr/src/app
> gh-badges@1.1.2 start /usr/src/app
> node server.js
http://[::1]/

View File

@@ -5,22 +5,25 @@ const isSvg = require('is-svg')
const got = require('./core/got-test-client')
let server
before(function () {
before(function() {
this.timeout('5s')
// remove args coming from mocha
// remove args comming from mocha
// https://github.com/badges/shields/issues/3365
process.argv = []
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')
})

View File

@@ -65,7 +65,6 @@ interface BadgeProps extends React.HTMLAttributes<HTMLImageElement> {
display?: 'inline' | 'block' | 'inline-block'
height?: string
clickable?: boolean
object?: boolean
}
export function Badge({
@@ -74,20 +73,11 @@ export function Badge({
display = 'inline',
height = '20px',
clickable = false,
object = false,
...rest
}: BadgeProps): JSX.Element {
return (
<BadgeWrapper clickable={clickable} display={display} height={height}>
{src ? (
object ? (
<object data={src}>alt</object>
) : (
<img alt={alt} src={src} {...rest} />
)
) : (
nonBreakingSpace
)}
{src ? <img alt={alt} src={src} {...rest} /> : nonBreakingSpace}
</BadgeWrapper>
)
}

View File

@@ -1,171 +0,0 @@
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>
)
}

View File

@@ -274,7 +274,7 @@ export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
target="_blank"
title="XPath syntax"
>
&#x2F;&#x2F;data/subdata
//data/subdata
</a>
&gt;&amp;color=&lt;COLOR&gt;&amp;prefix=&lt;PREFIX&gt;&amp;suffix=&lt;SUFFIX&gt;
</StyledCode>
@@ -345,15 +345,7 @@ export default function Usage({ baseUrl }: { baseUrl: string }): JSX.Element {
snippet="?logo=appveyor"
/>
<QueryParam
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>
}
documentation={<span>Insert custom logo image ( 14px high)</span>}
key="logoSvg"
snippet="?logo=data:image/png;base64,…"
/>

View File

@@ -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)

View File

@@ -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)

View File

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

View File

@@ -21,12 +21,6 @@ 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(

View File

@@ -1,3 +1,2 @@
lib/make-badge-test-helpers.js
lib/**/*.spec.js
index.test-d.ts

View File

@@ -1,76 +1,6 @@
# Changelog
## 3.1.0
- Add TypeScript definitions
## 3.0.1
- Fix missing dependency
## 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
## 2.2.1
### Fixes

View File

@@ -1,13 +1,12 @@
# badge-maker
# gh-badges
[![npm version](https://img.shields.io/npm/v/badge-maker.svg)](https://npmjs.org/package/badge-maker)
[![npm license](https://img.shields.io/npm/l/badge-maker.svg)](https://npmjs.org/package/badge-maker)
[![npm type definitions](https://img.shields.io/npm/types/badge-maker)](https://npmjs.org/package/badge-maker)
[![npm version](https://img.shields.io/npm/v/gh-badges.svg)](https://npmjs.org/package/gh-badges)
[![npm license](https://img.shields.io/npm/l/gh-badges.svg)](https://npmjs.org/package/gh-badges)
## Installation
```sh
npm install badge-maker
npm install gh-badges
```
## Usage
@@ -15,44 +14,29 @@ npm install badge-maker
### On the console
```sh
npm install -g badge-maker
badge build passed :green > mybadge.svg
npm install -g gh-badges
badge build passed :green .png > mybadge.png
```
### As a library
With CommonJS in JavaScript,
```js
const { makeBadge, ValidationError } = require('badge-maker')
```
const { BadgeFactory } = require('gh-badges')
With ESM or TypeScript,
const bf = new BadgeFactory()
```ts
import { makeBadge, ValidationError } from 'badge-maker'
```
```js
const format = {
label: 'build',
message: 'passed',
text: ['build', 'passed'],
color: 'green',
template: 'flat',
}
const svg = makeBadge(format)
console.log(svg) // <svg...
try {
makeBadge({})
} catch (e) {
console.log(e) // ValidationError: Field `message` is required
}
const svg = bf.create(format)
```
### Node version support
The latest version of badge-maker supports all currently maintained Node
The latest version of gh-badges supports all currently maintained Node
versions. See the [Node Release Schedule][].
[node release schedule]: https://github.com/nodejs/Release#release-schedule
@@ -63,17 +47,28 @@ The format is the following:
```js
{
label: 'build', // (Optional) Badge label
message: 'passed', // (Required) Badge message
labelColor: '#555', // (Optional) Label color
color: '#4c1', // (Optional) Message color
text: [ 'build', 'passed' ], // Textual information shown, in order
// (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social'
format: 'svg', // Also supports json
color: '#4c1',
labelColor: '#555',
// See templates/ for a list of available templates.
// Each offers a different visual design.
style: 'flat',
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`.
}
```
### See also
- [templates/](./templates) for the `template` option
## Colors
There are three ways to specify `color` and `labelColor`:
@@ -131,12 +126,3 @@ 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:-`.

View File

@@ -2,18 +2,23 @@
'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 label message [:color] [@style]')
console.log('Or: badge label message color [labelColor] [@style]')
console.log('Usage: badge subject status [:color] [.output] [@style]')
console.log(
'Or: badge subject status color [labelColor] [.output] [@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()
@@ -21,8 +26,14 @@ 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)
@@ -30,14 +41,14 @@ for (let i = 4; i < process.argv.length; i++) {
}
}
const label = process.argv[2]
const message = process.argv[3]
const subject = process.argv[2]
const status = process.argv[3]
let color = process.argv[4] || ':green'
const labelColor = process.argv[5]
const colorA = process.argv[5]
const badgeData = { label, message }
const badgeData = { text: [subject, status], format }
if (style) {
badgeData.style = style
badgeData.template = style
}
if (color[0] === ':') {
@@ -47,17 +58,28 @@ if (color[0] === ':') {
console.error('Invalid color scheme.')
process.exit(1)
}
badgeData.color = color
badgeData.colorscheme = color
} else {
badgeData.color = color
if (labelColor) {
badgeData.labelColor = labelColor
badgeData.colorB = color
if (colorA) {
badgeData.colorA = colorA
}
}
;(() => {
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 {
console.log(makeBadge(badgeData))
await main()
} catch (e) {
console.error(e)
process.exit(1)

View File

@@ -1,6 +1,7 @@
'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')
@@ -13,13 +14,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)
@@ -27,13 +28,30 @@ 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')
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)
})
})

View File

@@ -2,7 +2,7 @@
const isCSSColor = require('is-css-color')
// When updating these, be sure also to update the list in `badge-maker/README.md`.
// When updating these, be sure also to update the list in `gh-badges/README.md`.
const namedColors = {
brightgreen: '#4c1',
green: '#97ca00',

43
gh-badges/lib/index.js Normal file
View File

@@ -0,0 +1,43 @@
'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,
}

Some files were not shown because too many files have changed in this diff Show More