Compare commits
2 Commits
requires-p
...
gh-package
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5496dd057a | ||
|
|
3bd1328f19 |
@@ -4,6 +4,12 @@ main_steps: &main_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
@@ -12,6 +18,11 @@ main_steps: &main_steps
|
||||
# We don't need to install the Cypress binary in jobs that aren't actually running Cypress.
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
- run:
|
||||
name: Linter
|
||||
when: always
|
||||
@@ -45,6 +56,12 @@ integration_steps: &integration_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
@@ -66,6 +83,12 @@ services_steps: &services_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
@@ -96,29 +119,20 @@ run_package_tests: &run_package_tests
|
||||
nvm install $NODE_VERSION
|
||||
nvm use $NODE_VERSION
|
||||
node --version
|
||||
|
||||
# install the shields.io dependencies
|
||||
npm ci
|
||||
|
||||
# run the package tests
|
||||
npm run test:package
|
||||
npm run check-types:package
|
||||
|
||||
# delete the sheilds.io dependencies
|
||||
rm -rf node_modules/
|
||||
|
||||
# run a smoke test (render a badge with the CLI)
|
||||
# with only the package dependencies installed
|
||||
cd badge-maker
|
||||
npm link
|
||||
badge cactus grown :green @flat
|
||||
|
||||
package_steps: &package_steps
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install node and npm
|
||||
name: Install dependencies
|
||||
command: |
|
||||
set +e
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
@@ -126,74 +140,102 @@ package_steps: &package_steps
|
||||
nvm install v12
|
||||
nvm use v12
|
||||
npm install -g npm
|
||||
npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
# 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://nodejs.org/en/about/releases/
|
||||
# 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
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
name: Run package tests on Node 10
|
||||
|
||||
- run:
|
||||
<<: *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
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
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
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
name: Run package tests on Node 14
|
||||
|
||||
- store_test_results:
|
||||
path: junit
|
||||
|
||||
jobs:
|
||||
npm-install:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
environment:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
|
||||
main:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
- image: circleci/node:8
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
main@node-14:
|
||||
main@node-latest:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:latest
|
||||
|
||||
<<: *main_steps
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
- image: circleci/node:8
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
integration@node-14:
|
||||
integration@node-latest:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:latest
|
||||
- image: redis
|
||||
|
||||
<<: *integration_steps
|
||||
|
||||
danger:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
@@ -210,10 +252,16 @@ jobs:
|
||||
|
||||
frontend:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
@@ -251,22 +299,29 @@ jobs:
|
||||
|
||||
services:
|
||||
docker:
|
||||
- image: circleci/node:12
|
||||
- image: circleci/node:8
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
services@node-14:
|
||||
services@node-latest:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
- image: circleci/node:latest
|
||||
|
||||
<<: *services_steps
|
||||
|
||||
e2e:
|
||||
docker:
|
||||
- image: cypress/base:12
|
||||
- image: cypress/base:8
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
name: Restore node_modules
|
||||
keys:
|
||||
- v2-dependencies-{{ checksum "package-lock.json" }}
|
||||
# https://github.com/badges/shields/issues/1937
|
||||
- v2-dependencies-
|
||||
|
||||
- restore_cache:
|
||||
name: Restore Cypress binary
|
||||
keys:
|
||||
@@ -312,11 +367,11 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- main@node-14:
|
||||
- main@node-latest:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
- integration@node-14:
|
||||
- integration@node-latest:
|
||||
filters:
|
||||
branches:
|
||||
ignore: gh-pages
|
||||
@@ -334,7 +389,7 @@ workflows:
|
||||
ignore:
|
||||
- master
|
||||
- gh-pages
|
||||
- services@node-14:
|
||||
- services@node-latest:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
|
||||
@@ -4,33 +4,8 @@ update_configs:
|
||||
- package_manager: 'javascript'
|
||||
directory: '/'
|
||||
update_schedule: 'weekly'
|
||||
automerged_updates:
|
||||
- match:
|
||||
dependency_name: 'chai*'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'cypress'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'eslint*'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'enzyme*'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'mocha*'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'sazerac'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'sinon*'
|
||||
update_type: 'semver:minor'
|
||||
- match:
|
||||
dependency_name: 'snap-shot-it'
|
||||
update_type: 'semver:minor'
|
||||
|
||||
# badge-maker package dependencies
|
||||
# gh-badges package dependencies
|
||||
- package_manager: 'javascript'
|
||||
directory: '/badge-maker'
|
||||
directory: '/gh-badges'
|
||||
update_schedule: 'weekly'
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
/coverage
|
||||
/__snapshots__
|
||||
/public
|
||||
badge-maker/node_modules/
|
||||
gh-badges/node_modules/
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
extends:
|
||||
- standard
|
||||
- standard-jsx
|
||||
- standard-react
|
||||
- plugin:@typescript-eslint/recommended
|
||||
- prettier
|
||||
- prettier/@typescript-eslint
|
||||
- prettier/standard
|
||||
- prettier/react
|
||||
@@ -16,8 +15,6 @@ parserOptions:
|
||||
settings:
|
||||
react:
|
||||
version: '16.8'
|
||||
jsdoc:
|
||||
mode: jsdoc
|
||||
|
||||
plugins:
|
||||
- chai-friendly
|
||||
@@ -42,6 +39,7 @@ overrides:
|
||||
es6: true
|
||||
rules:
|
||||
no-console: 'off'
|
||||
'@typescript-eslint/no-var-requires': off
|
||||
|
||||
- files:
|
||||
- '**/*.@(ts|tsx)'
|
||||
@@ -50,13 +48,8 @@ overrides:
|
||||
parser: '@typescript-eslint/parser'
|
||||
rules:
|
||||
# Argh.
|
||||
'@typescript-eslint/explicit-function-return-type':
|
||||
['error', { 'allowExpressions': true }]
|
||||
'@typescript-eslint/no-empty-function': 'error'
|
||||
'@typescript-eslint/no-var-requires': 'error'
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
'@typescript-eslint/no-object-literal-type-assertion': 'off'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'@typescript-eslint/ban-ts-ignore': 'off'
|
||||
|
||||
- files:
|
||||
- core/**/*.ts
|
||||
@@ -120,9 +113,9 @@ rules:
|
||||
# Allow unused parameters. In callbacks, removing them seems to obscure
|
||||
# what the functions are doing.
|
||||
'@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }]
|
||||
no-unused-vars: 'off'
|
||||
no-unused-vars: off
|
||||
|
||||
'@typescript-eslint/no-var-requires': 'off'
|
||||
'@typescript-eslint/no-var-requires': error
|
||||
|
||||
# These should be disabled by eslint-config-prettier, but are not.
|
||||
no-extra-semi: 'off'
|
||||
@@ -151,7 +144,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'
|
||||
@@ -170,8 +163,6 @@ rules:
|
||||
|
||||
# Disable some from TypeScript.
|
||||
'@typescript-eslint/camelcase': off
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
'@typescript-eslint/no-empty-function': 'off'
|
||||
|
||||
react/jsx-sort-props: 'error'
|
||||
react-hooks/rules-of-hooks: 'error'
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/1_Bug_report.md
vendored
@@ -8,7 +8,7 @@ Are you experiencing an issue with...
|
||||
|
||||
- [ ] [shields.io](https://shields.io/#/)
|
||||
- [ ] My own instance
|
||||
- [ ] [badge-maker NPM package](https://www.npmjs.com/package/badge-maker)
|
||||
- [ ] [gh-badges NPM package](https://www.npmjs.com/package/gh-badges)
|
||||
|
||||
:beetle: **Description**
|
||||
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/5_Support_question.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/5_Support_question.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: ❓ Support Question
|
||||
about: Ask a question about shields.io
|
||||
labels: 'question'
|
||||
---
|
||||
|
||||
:question: **Question**
|
||||
|
||||
<!--
|
||||
Ask your question clearly and concisely.
|
||||
|
||||
#support on our [Discord](https://discordapp.com/invite/HjJCwm5)
|
||||
is also a great place to ask questions and get help
|
||||
-->
|
||||
|
||||
<!-- Love Shields? Please consider donating $10 to sustain our activities:
|
||||
👉 https://opencollective.com/shields -->
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +0,0 @@
|
||||
contact_links:
|
||||
- name: ❓ Support Question
|
||||
url: https://github.com/badges/shields/discussions
|
||||
about: Ask a question about Shields.io
|
||||
10
.github/workflows/auto-approve.yml
vendored
10
.github/workflows/auto-approve.yml
vendored
@@ -1,10 +0,0 @@
|
||||
name: Auto approve
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: chris48s/approve-bot@2.0.1
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||
@@ -113,6 +113,3 @@ service-definitions.yml
|
||||
|
||||
# Rendered API docs
|
||||
/api-docs/
|
||||
|
||||
# Flamebearer
|
||||
flamegraph.html
|
||||
|
||||
30
.nowignore
Normal file
30
.nowignore
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
semi: false
|
||||
singleQuote: true
|
||||
trailingComma: es5
|
||||
bracketSpacing: true
|
||||
endOfLine: lf
|
||||
arrowParens: avoid
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
 or directly to [@calebcartwright](https://github.com/calebcartwright)  or [@paulmelnikow](https://github.com/paulmelnikow) 
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -49,9 +49,8 @@ simple changes, like badge additions. These are usually tagged with
|
||||
|
||||
Please review [these impeccable guidelines][code review guidelines].
|
||||
|
||||
You can monitor [issues][], [discussions][] and the [chat room][], and help
|
||||
other people who have questions about contributing to Shields, or using it
|
||||
for their projects.
|
||||
You can monitor [issues][] and the [chat room][], and help other people who
|
||||
have questions about contributing to Shields, or using it for their projects.
|
||||
|
||||
Feel free to reach out to one of the [maintainers][]
|
||||
if you need help getting started.
|
||||
@@ -59,7 +58,6 @@ if you need help getting started.
|
||||
[service badge pr tag]: https://github.com/badges/shields/pulls?q=is%3Apr+is%3Aopen+label%3Aservice-badge
|
||||
[code review guidelines]: https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e
|
||||
[issues]: https://github.com/badges/shields/issues
|
||||
[discussions]: https://github.com/badges/shields/discussions
|
||||
[chat room]: https://discordapp.com/invite/HjJCwm5
|
||||
[maintainers]: https://github.com/badges/shields#project-leaders
|
||||
|
||||
@@ -88,9 +86,9 @@ We're also asking for [one-time \$10 donations](https://opencollective.com/shiel
|
||||
There are three places to get help:
|
||||
|
||||
1. If you're new to the project, a good place to start is the [tutorial][].
|
||||
2. If you need help getting started or implementing a change, [start a discussion][discussions]
|
||||
2. If you need help getting started or implementing a change, [open an issue][]
|
||||
with your question. We promise it's okay to do that. If there is already an
|
||||
issue open for the feature you're working on, you can post there directly.
|
||||
issue open for the feature you're working on, you can post there.
|
||||
3. You can also join the [chat room][] and ask your question there.
|
||||
|
||||
[tutorial]: doc/TUTORIAL.md
|
||||
@@ -98,23 +96,20 @@ There are three places to get help:
|
||||
## Badge guidelines
|
||||
|
||||
- Shields.io hosts integrations for services which are primarily
|
||||
used by developers or which are widely used by developers.
|
||||
used by developers or which are widely used by developers
|
||||
- The left-hand side of a badge should not advertise. It should be a lowercase _noun_
|
||||
succinctly describing the meaning of the right-hand side.
|
||||
- Except for badges using the `social` style, logos should be _turned off by
|
||||
default_.
|
||||
- Badges should not obtain data from undocumented or reverse-engineered API endpoints.
|
||||
- Badges should not obtain data by scraping web pages - these are likely to break frequently.
|
||||
Whereas API publishers are incentivised to maintain a stable platform for their users,
|
||||
authors of web pages have no such incentive.
|
||||
- Badges may require users to specify a token in the badge URL as long it is scoped only to
|
||||
fetching information and doesn't expose any sensitive information. Generating a token with the
|
||||
correct scope must be clearly documented.
|
||||
|
||||
## Badge URLs
|
||||
|
||||
- The format of new badges should be of the form `/SERVICE/NOUN/PARAMETERS`.
|
||||
- There is further documentation on this in [badge-urls](https://github.com/badges/shields/blob/master/doc/badge-urls.md)
|
||||
- The format of new badges should be of the form
|
||||
`/SERVICE/NOUN/PARAMETERS/QUALIFIERS`. For instance,
|
||||
`/gitter/room/nwjs/nw.js`. The vendor is gitter, the
|
||||
badge is for rooms, and the parameter is nwjs/nw.js.
|
||||
- Services which require a url/hostname parameter should use a query parameter to accept that value. For instance,
|
||||
`/discourse/topics?server=https://meta.discourse.org`.
|
||||
|
||||
## Coding guidelines
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
FROM node:12-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
|
||||
|
||||
66
README.md
66
README.md
@@ -43,16 +43,16 @@ Every month it serves over 470 million images.
|
||||
This repo hosts:
|
||||
|
||||
- The [Shields.io][shields.io] frontend and server code
|
||||
- An [NPM library for generating badges][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
|
||||
|
||||
@@ -70,14 +70,10 @@ This repo hosts:
|
||||
[Make your own badges!][custom badges]
|
||||
(Quick example: `https://img.shields.io/badge/left-right-f39f37`)
|
||||
|
||||
Browse a [complete list of badges][shields.io].
|
||||
|
||||
[custom badges]: http://shields.io/#your-badge
|
||||
|
||||
### Quickstart
|
||||
|
||||
Browse a [complete list of badges][shields.io] and locate a particular badge by using the search bar or by browsing the categories. Click on the badge to fill in required data elements for that badge type (like your username or repo) and optionally customize (label, colors etc.). And it's ready for use!
|
||||
|
||||
Use the button at the bottom to copy your badge url or snippet, which can then be added to places like your GitHub readme files or other web pages.
|
||||
|
||||
## Contributing
|
||||
|
||||
Shields is a community project. We invite your participation through issues
|
||||
@@ -98,8 +94,8 @@ You can read a [tutorial on how to add a badge][tutorial].
|
||||
|
||||
## Development
|
||||
|
||||
1. Install Node 12 or later. You can use the [package manager][] of your choice.
|
||||
Tests need to pass in Node 12 and 14.
|
||||
1. Install Node 8 or later. You can use the [package manager][] of your choice.
|
||||
Tests need to pass in Node 8 and 10.
|
||||
2. Clone this repository.
|
||||
3. Run `npm ci` to install the dependencies.
|
||||
4. Run `npm start` to start the badge server and the frontend dev server.
|
||||
@@ -128,8 +124,8 @@ Please report any Gitpod bugs, questions, or suggestions in issue
|
||||
|
||||
[Snapshot tests][] ensure we don't inadvertently make changes that affect the
|
||||
SVG or JSON output. When deliberately changing the output, run
|
||||
`SNAPSHOT_DRY=1 npm run test:package` to preview changes to the saved
|
||||
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:package` to update them.
|
||||
`SNAPSHOT_DRY=1 npm run test:js:server` to preview changes to the saved
|
||||
snapshots, and `SNAPSHOT_UPDATE=1 npm run test:js:server` to update them.
|
||||
|
||||
The server can be configured to use [Sentry][] ([configuration][sentry configuration]) and [Prometheus][] ([configuration][prometheus configuration]).
|
||||
|
||||
@@ -187,6 +183,7 @@ Maintainers:
|
||||
- [calebcartwright](https://github.com/calebcartwright) (core team)
|
||||
- [chris48s](https://github.com/chris48s) (core team)
|
||||
- [Daniel15](https://github.com/Daniel15) (core team)
|
||||
- [espadrine](https://github.com/espadrine) (core team)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow) (core team)
|
||||
- [platan](https://github.com/platan) (core team)
|
||||
- [PyvesB](https://github.com/PyvesB) (core team)
|
||||
@@ -194,22 +191,19 @@ Maintainers:
|
||||
|
||||
Operations:
|
||||
|
||||
- [calebcartwright](https://github.com/calebcartwright)
|
||||
- [chris48s](https://github.com/chris48s)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow)
|
||||
- [PyvesB](https://github.com/PyvesB)
|
||||
- [espadrine](https://github.com/espadrine) (sysadmin)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow) (limited access)
|
||||
|
||||
Alumni:
|
||||
|
||||
- [espadrine](https://github.com/espadrine)
|
||||
- [olivierlacan](https://github.com/olivierlacan)
|
||||
|
||||
## Related projects
|
||||
|
||||
- [poser PHP library][poser]
|
||||
- [badgerbadgerbadger gem][gem]
|
||||
- [pybadges python library][pybadges]
|
||||
|
||||
[poser]: https://github.com/badges/poser
|
||||
[gem]: https://github.com/badges/badgerbadgerbadger
|
||||
[pybadges]: https://github.com/google/pybadges
|
||||
|
||||
## License
|
||||
@@ -220,6 +214,28 @@ domain unless specified otherwise.
|
||||
The assets in `logo/` are trademarks of their respective companies and are
|
||||
under their terms and license.
|
||||
|
||||
## Community
|
||||
## Contributors
|
||||
|
||||
Thanks to the people and companies who donate money, services or time to keep the project running. [https://shields.io/community](https://shields.io/community)
|
||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
<a href="https://github.com/badges/shields/graphs/contributors"><img src="https://opencollective.com/shields/contributors.svg?width=890" /></a>
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/shields#backer)]
|
||||
|
||||
<a href="https://opencollective.com/shields#backers" target="_blank"><img src="https://opencollective.com/shields/backers.svg?width=890"></a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/shields#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/shields/sponsor/0/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/1/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/2/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/3/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/4/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/5/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/6/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/7/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/8/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/shields/sponsor/9/website" target="_blank"><img src="https://opencollective.com/shields/sponsor/9/avatar.svg"></a>
|
||||
|
||||
25
SECURITY.md
25
SECURITY.md
@@ -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.
|
||||
@@ -1,135 +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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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 aria-hidden="true" x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" fill="#fff" textLength="350">cactus</text><text aria-hidden="true" x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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 aria-hidden="true" x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" fill="#fff" textLength="350">cactus</text><text aria-hidden="true" x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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 aria-hidden="true" x="405" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="405" y="140" transform="scale(.1)" fill="#fff" textLength="350">cactus</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="835" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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 aria-hidden="true" x="225" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="225" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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 aria-hidden="true" x="405" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="405" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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 aria-hidden="true" x="455" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="455" y="140" transform="scale(.1)" fill="#fff" 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"><a target="_blank" xlink:href="https://shields.io/"><rect width="45" x="0" height="20" fill="rgba(0,0,0,0)" /><text aria-hidden="true" x="235" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" fill="#fff" textLength="350">cactus</text></a><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="45" x="45" height="20" fill="rgba(0,0,0,0)" /><text aria-hidden="true" x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" fill="#fff" textLength="350">grown</text></a></g></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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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)" fill="#fff" textLength="350">cactus</text><text x="665" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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)" fill="#fff" textLength="350">cactus</text><text x="835" y="140" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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)" fill="#fff" 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"><a target="_blank" xlink:href="https://shields.io/"><rect width="45" x="0" height="20" fill="rgba(0,0,0,0)" /><text x="235" y="140" transform="scale(.1)" fill="#fff" textLength="350">cactus</text></a><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="45" x="45" height="20" fill="rgba(0,0,0,0)" /><text x="665" y="140" transform="scale(.1)" fill="#fff" textLength="350">grown</text></a></g></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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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 aria-hidden="true" x="235" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="130" transform="scale(.1)" fill="#fff" textLength="350">cactus</text><text aria-hidden="true" x="665" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="130" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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 aria-hidden="true" x="405" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="405" y="130" transform="scale(.1)" fill="#fff" textLength="350">cactus</text><text aria-hidden="true" x="835" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="835" y="130" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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 aria-hidden="true" x="225" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="225" y="130" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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 aria-hidden="true" x="405" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="405" y="130" transform="scale(.1)" fill="#fff" 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" role="img" aria-label="grown"><title>grown</title><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 aria-hidden="true" x="455" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="455" y="130" transform="scale(.1)" fill="#fff" 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"><a target="_blank" xlink:href="https://shields.io/"><rect width="45" x="0" height="18" fill="rgba(0,0,0,0)" /><text aria-hidden="true" x="235" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="130" transform="scale(.1)" fill="#fff" textLength="350">cactus</text></a><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="45" x="45" height="18" fill="rgba(0,0,0,0)" /><text aria-hidden="true" x="665" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="130" transform="scale(.1)" fill="#fff" textLength="350">grown</text></a></g></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" role="img" aria-label="CACTUS: GROWN"><title>CACTUS: GROWN</title><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 fill="#fff" x="370" y="175" transform="scale(.1)" textLength="500">CACTUS</text><text fill="#fff" 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" role="img" aria-label="CACTUS: GROWN"><title>CACTUS: GROWN</title><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 fill="#fff" x="540" y="175" transform="scale(.1)" textLength="500">CACTUS</text><text fill="#fff" 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" role="img" aria-label="GROWN"><title>GROWN</title><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 fill="#fff" 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" role="img" aria-label="GROWN"><title>GROWN</title><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 fill="#fff" 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" role="img" aria-label="GROWN"><title>GROWN</title><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 fill="#fff" x="230" y="175" transform="scale(.1)" textLength="-60"></text><text fill="#fff" 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"><a target="_blank" xlink:href="https://shields.io/"><rect width="74" height="28" fill="rgba(0,0,0,0)"/><text fill="#fff" x="370" y="175" transform="scale(.1)" textLength="500">CACTUS</text></a><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="73" height="28" x="74" fill="rgba(0,0,0,0)"/><text fill="#fff" x="1105" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></a></g></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" role="img" aria-label="Cactus: grown"><title>Cactus: grown</title><style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="true" 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"><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="47" height="19" rx="2" /><text aria-hidden="true" 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 aria-hidden="true" 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></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" role="img" aria-label="Cactus: grown"><title>Cactus: grown</title><style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="true" 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"><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="64" height="19" rx="2" /><text aria-hidden="true" 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 aria-hidden="true" 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></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" role="img" aria-label="grown"><title>grown</title><style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="true" 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"><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="11" height="19" rx="2" /><text aria-hidden="true" x="55" y="150" fill="#fff" transform="scale(.1)" textLength="10"></text><text x="55" y="140" transform="scale(.1)" textLength="10"></text><text aria-hidden="true" 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></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" role="img" aria-label="grown"><title>grown</title><style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="true" 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"><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="25" height="19" rx="2" /><text aria-hidden="true" x="195" y="150" fill="#fff" transform="scale(.1)" textLength="10"></text><text x="195" y="140" transform="scale(.1)" textLength="10"></text><text aria-hidden="true" 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></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" role="img" aria-label="grown"><title>grown</title><style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="true" 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"><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="25" height="19" rx="2" /><text aria-hidden="true" x="195" y="150" fill="#fff" transform="scale(.1)" textLength="10"></text><text x="195" y="140" transform="scale(.1)" textLength="10"></text><text aria-hidden="true" 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></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:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="false" 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"><a target="_blank" xlink:href="https://shields.io/"><text aria-hidden="true" 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><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="47" height="19" rx="2" /></a><a target="_blank" xlink:href="https://www.google.co.uk/"><rect width="42" x="53" height="20" fill="rgba(0,0,0,0)" /><text aria-hidden="true" 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></a></g></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" role="img" aria-label="label: message"><title>label: message</title><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 aria-hidden="true" x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" fill="#fff" textLength="270">label</text><text aria-hidden="true" x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" fill="#fff" textLength="490">message</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator text colors should use black text when the label color is light 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="cactus: grown"><title>cactus: grown</title><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="#f3f3f3"/><rect x="45" width="45" height="20" fill="#000"/><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 aria-hidden="true" x="235" y="150" fill="#ccc" fill-opacity=".3" transform="scale(.1)" textLength="350">cactus</text><text x="235" y="140" transform="scale(.1)" fill="#333" textLength="350">cactus</text><text aria-hidden="true" x="665" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="350">grown</text><text x="665" y="140" transform="scale(.1)" fill="#fff" textLength="350">grown</text></g></svg>
|
||||
`
|
||||
|
||||
exports['The badge generator text colors should use black text when the message color is light 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="147" height="28" role="img" aria-label="CACTUS: GROWN"><title>CACTUS: GROWN</title><g shape-rendering="crispEdges"><rect width="74" height="28" fill="#000"/><rect x="74" width="73" height="28" fill="#e2ffe1"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><text fill="#fff" x="370" y="175" transform="scale(.1)" textLength="500">CACTUS</text><text fill="#333" x="1105" y="175" font-weight="bold" transform="scale(.1)" textLength="490">GROWN</text></g></svg>
|
||||
exports['The badge generator badges with logos should always produce the same badge simple-icons javascript logo default color (#F7DF1E) 1'] = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="113" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="113" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h59v20H54z"/><path fill="url(#b)" d="M0 0h113v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><image x="5" y="3" width="14" height="14" xlink:href="javascript"/> <text x="365" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">label</text><text x="365" y="140" transform="scale(.1)" textLength="270">label</text><text x="825" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">message</text><text x="825" y="140" transform="scale(.1)" textLength="490">message</text></g> </svg>
|
||||
`
|
||||
|
||||
11
badge-maker/index.d.ts
vendored
11
badge-maker/index.d.ts
vendored
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,741 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const anafanafo = require('anafanafo')
|
||||
const { brightness } = require('./color')
|
||||
|
||||
const fontFamily = 'font-family="Verdana,Geneva,DejaVu Sans,sans-serif"'
|
||||
const socialFontFamily =
|
||||
'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"'
|
||||
const brightnessThreshold = 0.69
|
||||
|
||||
function capitalize(s) {
|
||||
return `${s.charAt(0).toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
function colorsForBackground(color) {
|
||||
if (brightness(color) <= brightnessThreshold) {
|
||||
return {
|
||||
textColor: '#fff',
|
||||
shadowColor: '#010101',
|
||||
}
|
||||
}
|
||||
return {
|
||||
textColor: '#333',
|
||||
shadowColor: '#ccc',
|
||||
}
|
||||
}
|
||||
|
||||
function escapeXml(s) {
|
||||
if (s === undefined || typeof s !== 'string') {
|
||||
return undefined
|
||||
} else {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
}
|
||||
|
||||
function roundUpToOdd(val) {
|
||||
// Increase chances of pixel grid alignment.
|
||||
return val % 2 === 0 ? val + 1 : val
|
||||
}
|
||||
|
||||
function preferredWidthOf(str) {
|
||||
return roundUpToOdd((anafanafo(str) / 10) | 0)
|
||||
}
|
||||
|
||||
function computeWidths({ label, message }) {
|
||||
return {
|
||||
labelWidth: preferredWidthOf(label),
|
||||
messageWidth: preferredWidthOf(message),
|
||||
}
|
||||
}
|
||||
|
||||
function createAccessibleText({ label, message }) {
|
||||
const labelPrefix = label ? `${label}: ` : ''
|
||||
return labelPrefix + message
|
||||
}
|
||||
|
||||
function hasLinks({ links }) {
|
||||
const [leftLink, rightLink] = links || []
|
||||
const hasLeftLink = leftLink && leftLink.length
|
||||
const hasRightLink = rightLink && rightLink.length
|
||||
const hasLink = hasLeftLink && hasRightLink
|
||||
return { hasLink, hasLeftLink, hasRightLink }
|
||||
}
|
||||
|
||||
function shouldWrapBodyWithLink({ links }) {
|
||||
const { hasLeftLink, hasRightLink } = hasLinks({ links })
|
||||
return hasLeftLink && !hasRightLink
|
||||
}
|
||||
|
||||
function renderAriaAttributes({ accessibleText, links }) {
|
||||
const { hasLink } = hasLinks({ links })
|
||||
return hasLink ? '' : `role="img" aria-label="${escapeXml(accessibleText)}"`
|
||||
}
|
||||
|
||||
function renderTitle({ accessibleText, links }) {
|
||||
const { hasLink } = hasLinks({ links })
|
||||
return hasLink ? '' : `<title>${escapeXml(accessibleText)}</title>`
|
||||
}
|
||||
|
||||
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 renderLink({
|
||||
link,
|
||||
height,
|
||||
textLength,
|
||||
horizPadding,
|
||||
leftMargin,
|
||||
renderedText,
|
||||
}) {
|
||||
const rectHeight = height
|
||||
const rectWidth = textLength + horizPadding * 2
|
||||
const rectX = leftMargin > 1 ? leftMargin + 1 : 0
|
||||
return `<a target="_blank" xlink:href="${escapeXml(link)}">
|
||||
<rect width="${rectWidth}" x="${rectX}" height="${rectHeight}" fill="rgba(0,0,0,0)" />
|
||||
${renderedText}
|
||||
</a>`
|
||||
}
|
||||
|
||||
function renderText({
|
||||
leftMargin,
|
||||
horizPadding = 0,
|
||||
content,
|
||||
link,
|
||||
height,
|
||||
verticalMargin = 0,
|
||||
shadow = false,
|
||||
color,
|
||||
}) {
|
||||
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 = ''
|
||||
const { textColor, shadowColor } = colorsForBackground(color)
|
||||
if (shadow) {
|
||||
renderedText = `<text aria-hidden="true" x="${x}" y="${shadowMargin}" fill="${shadowColor}" fill-opacity=".3" transform="scale(.1)" textLength="${outTextLength}">${escapedContent}</text>`
|
||||
}
|
||||
renderedText += `<text x="${x}" y="${textMargin}" transform="scale(.1)" fill="${textColor}" textLength="${outTextLength}">${escapedContent}</text>`
|
||||
|
||||
return {
|
||||
renderedText: link
|
||||
? renderLink({
|
||||
link,
|
||||
height,
|
||||
textLength,
|
||||
horizPadding,
|
||||
leftMargin,
|
||||
renderedText,
|
||||
})
|
||||
: renderedText,
|
||||
width: textLength,
|
||||
}
|
||||
}
|
||||
|
||||
function renderBadge(
|
||||
{ links, leftWidth, rightWidth, height, accessibleText },
|
||||
main
|
||||
) {
|
||||
const width = leftWidth + rightWidth
|
||||
const leftLink = escapeXml(links[0])
|
||||
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" ${renderAriaAttributes(
|
||||
{ links, accessibleText }
|
||||
)}>
|
||||
|
||||
${renderTitle({ accessibleText, links })}
|
||||
${
|
||||
shouldWrapBodyWithLink({ links })
|
||||
? `<a target="_blank" xlink:href="${leftLink}">${main}</a>`
|
||||
: main
|
||||
}
|
||||
</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'
|
||||
}
|
||||
|
||||
const [leftLink, rightLink] = links
|
||||
|
||||
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,
|
||||
link: !shouldWrapBodyWithLink({ links }) && leftLink,
|
||||
height: this.constructor.height,
|
||||
verticalMargin: this.constructor.verticalMargin,
|
||||
shadow: this.constructor.shadow,
|
||||
color: labelColor,
|
||||
})
|
||||
|
||||
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,
|
||||
link: rightLink,
|
||||
height: this.constructor.height,
|
||||
verticalMargin: this.constructor.verticalMargin,
|
||||
shadow: this.constructor.shadow,
|
||||
color,
|
||||
})
|
||||
|
||||
let rightWidth = messageWidth + 2 * horizPadding
|
||||
if (hasLogo && !hasLabel) {
|
||||
rightWidth += totalLogoWidth + horizPadding - 1
|
||||
}
|
||||
|
||||
const width = leftWidth + rightWidth
|
||||
|
||||
const accessibleText = createAccessibleText({ label, message })
|
||||
|
||||
this.links = links
|
||||
this.leftWidth = leftWidth
|
||||
this.rightWidth = rightWidth
|
||||
this.width = width
|
||||
this.labelColor = labelColor
|
||||
this.color = color
|
||||
this.label = label
|
||||
this.message = message
|
||||
this.accessibleText = accessibleText
|
||||
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,
|
||||
accessibleText: this.accessibleText,
|
||||
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,
|
||||
accessibleText: this.accessibleText,
|
||||
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,
|
||||
accessibleText: this.accessibleText,
|
||||
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, hasRightLink, hasLink } = hasLinks({ links })
|
||||
|
||||
const accessibleText = createAccessibleText({ label, message })
|
||||
|
||||
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 renderLabelText() {
|
||||
const rect = `<rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="${labelWidth}" height="${internalHeight}" rx="2" />`
|
||||
const shadow = `<text aria-hidden="true" x="${labelTextX}" y="150" fill="#fff" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
||||
const text = `<text x="${labelTextX}" y="140" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
||||
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
|
||||
return `
|
||||
<a target="_blank" xlink:href="${leftLink}">
|
||||
${shadow}
|
||||
${text}
|
||||
${rect}
|
||||
</a>
|
||||
`
|
||||
}
|
||||
return `
|
||||
${rect}
|
||||
${shadow}
|
||||
${text}
|
||||
`
|
||||
}
|
||||
|
||||
function renderMessageText() {
|
||||
const messageTextX = (labelWidth + messageWidth / 2 + 6) * 10
|
||||
const messageTextLength = (messageWidth - 8) * 10
|
||||
const escapedMessage = escapeXml(message)
|
||||
const rect = `<rect width="${messageWidth + 1}" x="${
|
||||
labelWidth + 6
|
||||
}" height="${internalHeight + 1}" fill="rgba(0,0,0,0)" />`
|
||||
const shadow = `<text aria-hidden="true" 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 `
|
||||
<a target="_blank" xlink:href="${rightLink}">
|
||||
${rect}
|
||||
${shadow}
|
||||
${text}
|
||||
</a>
|
||||
`
|
||||
}
|
||||
return `
|
||||
${shadow}
|
||||
${text}
|
||||
`
|
||||
}
|
||||
|
||||
const badge = renderBadge(
|
||||
{
|
||||
links,
|
||||
leftWidth: labelWidth + 1,
|
||||
rightWidth: hasMessage ? messageWidth + 6 : 0,
|
||||
accessibleText,
|
||||
height: externalHeight,
|
||||
},
|
||||
`
|
||||
<style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{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 aria-hidden="${!hasLink}" fill="#333" text-anchor="middle" ${socialFontFamily} text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px">
|
||||
${renderLabelText()}
|
||||
${hasMessage ? renderMessageText() : ''}
|
||||
</g>
|
||||
`
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
let [leftLink, rightLink] = links
|
||||
leftLink = escapeXml(leftLink)
|
||||
rightLink = escapeXml(rightLink)
|
||||
const { hasLeftLink, hasRightLink } = hasLinks({ links })
|
||||
|
||||
const accessibleText = createAccessibleText({ label, message })
|
||||
|
||||
function renderLabelText() {
|
||||
const { textColor } = colorsForBackground(labelColor)
|
||||
const labelTextX = ((labelWidth + totalLogoWidth) / 2) * 10
|
||||
const labelTextLength = (labelWidth - (24 + totalLogoWidth)) * 10
|
||||
const escapedLabel = escapeXml(label)
|
||||
const text = `<text fill="${textColor}" x="${labelTextX}" y="175" transform="scale(.1)" textLength="${labelTextLength}">${escapedLabel}</text>`
|
||||
if (hasLeftLink && !shouldWrapBodyWithLink({ links })) {
|
||||
return `
|
||||
<a target="_blank" xlink:href="${leftLink}">
|
||||
<rect width="${leftWidth}" height="${height}" fill="rgba(0,0,0,0)"/>
|
||||
${text}
|
||||
</a>
|
||||
`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function renderMessageText() {
|
||||
const { textColor } = colorsForBackground(color)
|
||||
const text = `<text fill="${textColor}" x="${
|
||||
(labelWidth + messageWidth / 2) * 10
|
||||
}" y="175" font-weight="bold" transform="scale(.1)" textLength="${
|
||||
(messageWidth - 24) * 10
|
||||
}">
|
||||
${escapeXml(message)}</text>`
|
||||
if (hasRightLink) {
|
||||
return `
|
||||
<a target="_blank" xlink:href="${rightLink}">
|
||||
<rect width="${rightWidth}" height="${height}" x="${labelWidth}" fill="rgba(0,0,0,0)"/>
|
||||
${text}
|
||||
</a>
|
||||
`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const badge = renderBadge(
|
||||
{
|
||||
links,
|
||||
leftWidth,
|
||||
rightWidth,
|
||||
accessibleText,
|
||||
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() : ''}
|
||||
${renderMessageText()}
|
||||
</g>`
|
||||
)
|
||||
|
||||
if (minify) {
|
||||
return stripXmlWhitespace(badge)
|
||||
}
|
||||
return badge
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
plastic,
|
||||
flat,
|
||||
social,
|
||||
'flat-square': flatSquare,
|
||||
'for-the-badge': forTheBadge,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
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
|
||||
|
||||
// This ought to be the responsibility of the server, not `makeBadge`.
|
||||
if (format === 'json') {
|
||||
return JSON.stringify({
|
||||
label,
|
||||
message,
|
||||
logoWidth,
|
||||
// Only call normalizeColor for the JSON case: this is handled
|
||||
// internally by toSvgColor in the SVG case.
|
||||
color: normalizeColor(color),
|
||||
labelColor: normalizeColor(labelColor),
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
})
|
||||
}
|
||||
|
||||
const render = badgeRenderers[template]
|
||||
if (!render) {
|
||||
throw new Error(`Unknown template: '${template}'`)
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -1,589 +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)
|
||||
})
|
||||
})
|
||||
|
||||
describe('text colors', function () {
|
||||
it('should use black text when the label color is light', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'flat',
|
||||
color: '#000',
|
||||
labelColor: '#f3f3f3',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should use black text when the message color is light', function () {
|
||||
snapshot(
|
||||
makeBadge({
|
||||
text: ['cactus', 'grown'],
|
||||
format: 'svg',
|
||||
template: 'for-the-badge',
|
||||
color: '#e2ffe1',
|
||||
labelColor: '#000',
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,15 +6,6 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: 'METRICS_PROMETHEUS_ENABLED'
|
||||
endpointEnabled: 'METRICS_PROMETHEUS_ENDPOINT_ENABLED'
|
||||
influx:
|
||||
enabled: 'METRICS_INFLUX_ENABLED'
|
||||
url: 'METRICS_INFLUX_URL'
|
||||
timeoutMilliseconds: 'METRICS_INFLUX_TIMEOUT_MILLISECONDS'
|
||||
intervalSeconds: 'METRICS_INFLUX_INTERVAL_SECONDS'
|
||||
instanceIdFrom: 'METRICS_INFLUX_INSTANCE_ID_FROM'
|
||||
instanceIdEnvVarName: 'METRICS_INFLUX_INSTANCE_ID_ENV_VAR_NAME'
|
||||
envLabel: 'METRICS_INFLUX_ENV_LABEL'
|
||||
|
||||
ssl:
|
||||
isSecure: 'HTTPS'
|
||||
@@ -34,29 +25,16 @@ public:
|
||||
dir: 'PERSISTENCE_DIR'
|
||||
|
||||
services:
|
||||
bitbucketServer:
|
||||
authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS'
|
||||
drone:
|
||||
authorizedOrigins: 'DRONE_ORIGINS'
|
||||
github:
|
||||
baseUri: 'GITHUB_URL'
|
||||
debug:
|
||||
enabled: 'GITHUB_DEBUG_ENABLED'
|
||||
intervalSeconds: 'GITHUB_DEBUG_INTERVAL_SECONDS'
|
||||
jenkins:
|
||||
authorizedOrigins: 'JENKINS_ORIGINS'
|
||||
jira:
|
||||
authorizedOrigins: 'JIRA_ORIGINS'
|
||||
nexus:
|
||||
authorizedOrigins: 'NEXUS_ORIGINS'
|
||||
npm:
|
||||
authorizedOrigins: 'NPM_ORIGINS'
|
||||
sonar:
|
||||
authorizedOrigins: 'SONAR_ORIGINS'
|
||||
teamcity:
|
||||
authorizedOrigins: 'TEAMCITY_ORIGINS'
|
||||
trace: 'TRACE_SERVICES'
|
||||
|
||||
profiling:
|
||||
makeBadge: 'PROFILE_MAKE_BADGE'
|
||||
|
||||
cacheHeaders:
|
||||
defaultCacheLengthSeconds: 'BADGE_MAX_AGE_SECONDS'
|
||||
|
||||
@@ -68,11 +46,6 @@ private:
|
||||
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
|
||||
bintray_user: 'BINTRAY_USER'
|
||||
bintray_apikey: 'BINTRAY_API_KEY'
|
||||
bitbucket_username: 'BITBUCKET_USER'
|
||||
bitbucket_password: 'BITBUCKET_PASS'
|
||||
bitbucket_server_username: 'BITBUCKET_SERVER_USER'
|
||||
bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
|
||||
discord_bot_token: 'DISCORD_BOT_TOKEN'
|
||||
drone_token: 'DRONE_TOKEN'
|
||||
gh_client_id: 'GH_CLIENT_ID'
|
||||
gh_client_secret: 'GH_CLIENT_SECRET'
|
||||
@@ -90,11 +63,6 @@ private:
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
sonarqube_token: 'SONARQUBE_TOKEN'
|
||||
teamcity_user: 'TEAMCITY_USER'
|
||||
teamcity_pass: 'TEAMCITY_PASS'
|
||||
twitch_client_id: 'TWITCH_CLIENT_ID'
|
||||
twitch_client_secret: 'TWITCH_CLIENT_SECRET'
|
||||
wheelmap_token: 'WHEELMAP_TOKEN'
|
||||
influx_username: 'INFLUX_USERNAME'
|
||||
influx_password: 'INFLUX_PASSWORD'
|
||||
youtube_api_key: 'YOUTUBE_API_KEY'
|
||||
|
||||
@@ -5,11 +5,7 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
endpointEnabled: false
|
||||
influx:
|
||||
enabled: false
|
||||
timeoutMilliseconds: 1000
|
||||
intervalSeconds: 15
|
||||
|
||||
ssl:
|
||||
isSecure: false
|
||||
|
||||
@@ -27,6 +23,9 @@ public:
|
||||
intervalSeconds: 200
|
||||
trace: false
|
||||
|
||||
profiling:
|
||||
makeBadge: false
|
||||
|
||||
cacheHeaders:
|
||||
defaultCacheLengthSeconds: 120
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
private:
|
||||
# These are the keys which are set on the production servers.
|
||||
discord_bot_token: ...
|
||||
bintray_user: ...
|
||||
bintray_apikey: ...
|
||||
gh_client_id: ...
|
||||
gh_client_secret: ...
|
||||
redis_url: ...
|
||||
sentry_dsn: ...
|
||||
shields_secret: ...
|
||||
sl_insight_userUuid: ...
|
||||
@@ -11,4 +10,3 @@ private:
|
||||
twitch_client_id: ...
|
||||
twitch_client_secret: ...
|
||||
wheelmap_token: ...
|
||||
youtube_api_key: ...
|
||||
|
||||
@@ -8,4 +8,3 @@ private:
|
||||
twitch_client_id: '...'
|
||||
twitch_client_secret: '...'
|
||||
wheelmap_token: '...'
|
||||
youtube_api_key: '...'
|
||||
|
||||
@@ -2,12 +2,6 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: true
|
||||
influx:
|
||||
enabled: true
|
||||
url: https://metrics.shields.io/telegraf
|
||||
instanceIdFrom: env-var
|
||||
instanceIdEnvVarName: HEROKU_DYNO_ID
|
||||
envLabel: shields-production
|
||||
|
||||
ssl:
|
||||
isSecure: true
|
||||
|
||||
4
core/badge-urls/make-badge-url.d.ts
vendored
4
core/badge-urls/make-badge-url.d.ts
vendored
@@ -38,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({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const { URL } = require('url')
|
||||
const queryString = require('query-string')
|
||||
const { compile } = require('path-to-regexp')
|
||||
const pathToRegexp = require('path-to-regexp')
|
||||
|
||||
function badgeUrlFromPath({
|
||||
baseUrl = '',
|
||||
@@ -33,10 +33,9 @@ function badgeUrlFromPattern({
|
||||
format = '',
|
||||
longCache = false,
|
||||
}) {
|
||||
const toPath = compile(pattern, {
|
||||
const toPath = pathToRegexp.compile(pattern, {
|
||||
strict: true,
|
||||
sensitive: true,
|
||||
encode: encodeURIComponent,
|
||||
})
|
||||
|
||||
const path = toPath(namedParams)
|
||||
@@ -59,19 +58,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}` : ''
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,68 +1,34 @@
|
||||
'use strict'
|
||||
|
||||
const { URL } = require('url')
|
||||
const { InvalidParameter } = require('./errors')
|
||||
|
||||
class AuthHelper {
|
||||
constructor(
|
||||
{
|
||||
userKey,
|
||||
passKey,
|
||||
authorizedOrigins,
|
||||
serviceKey,
|
||||
isRequired = false,
|
||||
defaultToEmptyStringForUser = false,
|
||||
},
|
||||
config
|
||||
privateConfig
|
||||
) {
|
||||
if (!userKey && !passKey) {
|
||||
throw Error('Expected userKey or passKey to be set')
|
||||
}
|
||||
|
||||
if (!authorizedOrigins && !serviceKey) {
|
||||
throw Error('Expected authorizedOrigins or serviceKey to be set')
|
||||
}
|
||||
|
||||
this._userKey = userKey
|
||||
this._passKey = passKey
|
||||
if (userKey) {
|
||||
this._user = config.private[userKey]
|
||||
this.user = privateConfig[userKey]
|
||||
} else {
|
||||
this._user = defaultToEmptyStringForUser ? '' : undefined
|
||||
this.user = defaultToEmptyStringForUser ? '' : undefined
|
||||
}
|
||||
this._pass = passKey ? config.private[passKey] : undefined
|
||||
this.pass = passKey ? privateConfig[passKey] : undefined
|
||||
this.isRequired = isRequired
|
||||
|
||||
if (serviceKey !== undefined && !(serviceKey in config.public.services)) {
|
||||
// Keep this as its own error, as it's useful to the programmer as they're
|
||||
// getting auth set up.
|
||||
throw Error(`Service key ${serviceKey} was missing from config schema`)
|
||||
}
|
||||
|
||||
let requireStrictSsl, requireStrictSslToAuthenticate
|
||||
if (serviceKey === undefined) {
|
||||
requireStrictSsl = true
|
||||
requireStrictSslToAuthenticate = true
|
||||
} else {
|
||||
;({
|
||||
authorizedOrigins,
|
||||
requireStrictSsl = true,
|
||||
requireStrictSslToAuthenticate = true,
|
||||
} = config.public.services[serviceKey])
|
||||
}
|
||||
if (!Array.isArray(authorizedOrigins)) {
|
||||
throw Error('Expected authorizedOrigins to be an array of origins')
|
||||
}
|
||||
this._authorizedOrigins = authorizedOrigins
|
||||
this._requireStrictSsl = requireStrictSsl
|
||||
this._requireStrictSslToAuthenticate = requireStrictSslToAuthenticate
|
||||
}
|
||||
|
||||
get isConfigured() {
|
||||
return (
|
||||
this._authorizedOrigins.length > 0 &&
|
||||
(this._userKey ? Boolean(this._user) : true) &&
|
||||
(this._passKey ? Boolean(this._pass) : true)
|
||||
(this._userKey ? Boolean(this.user) : true) &&
|
||||
(this._passKey ? Boolean(this.pass) : true)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,142 +36,19 @@ class AuthHelper {
|
||||
if (this.isRequired) {
|
||||
return this.isConfigured
|
||||
} else {
|
||||
const configIsEmpty = !this._user && !this._pass
|
||||
const configIsEmpty = !this.user && !this.pass
|
||||
return this.isConfigured || configIsEmpty
|
||||
}
|
||||
}
|
||||
|
||||
static _isInsecureSslRequest({ options = {} }) {
|
||||
const { strictSSL = true } = options
|
||||
return strictSSL !== true
|
||||
}
|
||||
|
||||
enforceStrictSsl({ options = {} }) {
|
||||
if (
|
||||
this._requireStrictSsl &&
|
||||
this.constructor._isInsecureSslRequest({ options })
|
||||
) {
|
||||
throw new InvalidParameter({ prettyMessage: 'strict ssl is required' })
|
||||
}
|
||||
}
|
||||
|
||||
shouldAuthenticateRequest({ url, options = {} }) {
|
||||
let parsed
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch (e) {
|
||||
throw new InvalidParameter({ prettyMessage: 'invalid url parameter' })
|
||||
}
|
||||
|
||||
const { protocol, host } = parsed
|
||||
const origin = `${protocol}//${host}`
|
||||
const originViolation = !this._authorizedOrigins.includes(origin)
|
||||
|
||||
const strictSslCheckViolation =
|
||||
this._requireStrictSslToAuthenticate &&
|
||||
this.constructor._isInsecureSslRequest({ options })
|
||||
|
||||
return this.isConfigured && !originViolation && !strictSslCheckViolation
|
||||
}
|
||||
|
||||
get _basicAuth() {
|
||||
const { _user: user, _pass: pass } = this
|
||||
get basicAuth() {
|
||||
const { user, pass } = this
|
||||
return this.isConfigured ? { user, pass } : undefined
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for `withBasicAuth()` and friends.
|
||||
*/
|
||||
_withAnyAuth(requestParams, mergeAuthFn) {
|
||||
this.enforceStrictSsl(requestParams)
|
||||
|
||||
const shouldAuthenticate = this.shouldAuthenticateRequest(requestParams)
|
||||
if (this.isRequired && !shouldAuthenticate) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'requested origin not authorized',
|
||||
})
|
||||
}
|
||||
|
||||
return shouldAuthenticate ? mergeAuthFn(requestParams) : requestParams
|
||||
}
|
||||
|
||||
static _mergeAuth(requestParams, auth) {
|
||||
const { options, ...rest } = requestParams
|
||||
return {
|
||||
options: {
|
||||
auth,
|
||||
...options,
|
||||
},
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
withBasicAuth(requestParams) {
|
||||
return this._withAnyAuth(requestParams, requestParams =>
|
||||
this.constructor._mergeAuth(requestParams, this._basicAuth)
|
||||
)
|
||||
}
|
||||
|
||||
_bearerAuthHeader(bearerKey) {
|
||||
const { _pass: pass } = this
|
||||
return this.isConfigured
|
||||
? { Authorization: `${bearerKey} ${pass}` }
|
||||
: undefined
|
||||
}
|
||||
|
||||
static _mergeHeaders(requestParams, headers) {
|
||||
const {
|
||||
options: { headers: existingHeaders, ...restOptions } = {},
|
||||
...rest
|
||||
} = requestParams
|
||||
return {
|
||||
options: {
|
||||
headers: {
|
||||
...existingHeaders,
|
||||
...headers,
|
||||
},
|
||||
...restOptions,
|
||||
},
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
withBearerAuthHeader(
|
||||
requestParams,
|
||||
bearerKey = 'Bearer' // lgtm [js/hardcoded-credentials]
|
||||
) {
|
||||
return this._withAnyAuth(requestParams, requestParams =>
|
||||
this.constructor._mergeHeaders(
|
||||
requestParams,
|
||||
this._bearerAuthHeader(bearerKey)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static _mergeQueryParams(requestParams, query) {
|
||||
const {
|
||||
options: { qs: existingQuery, ...restOptions } = {},
|
||||
...rest
|
||||
} = requestParams
|
||||
return {
|
||||
options: {
|
||||
qs: {
|
||||
...existingQuery,
|
||||
...query,
|
||||
},
|
||||
...restOptions,
|
||||
},
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
withQueryStringAuth({ userKey, passKey }, requestParams) {
|
||||
return this._withAnyAuth(requestParams, requestParams =>
|
||||
this.constructor._mergeQueryParams(requestParams, {
|
||||
...(userKey ? { [userKey]: this._user } : undefined),
|
||||
...(passKey ? { [passKey]: this._pass } : undefined),
|
||||
})
|
||||
)
|
||||
get bearerAuthHeader() {
|
||||
const { pass } = this
|
||||
return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,42 +3,18 @@
|
||||
const { expect } = require('chai')
|
||||
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 () {
|
||||
expect(() => new AuthHelper({}, {})).to.throw(
|
||||
Error,
|
||||
'Expected userKey or passKey to be set'
|
||||
)
|
||||
})
|
||||
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 () {
|
||||
expect(
|
||||
() =>
|
||||
new AuthHelper(
|
||||
{
|
||||
userKey: 'myci_user',
|
||||
passKey: 'myci_pass',
|
||||
authorizedOrigins: true,
|
||||
},
|
||||
{ private: {} }
|
||||
)
|
||||
).to.throw(Error, 'Expected authorizedOrigins to be an array of origins')
|
||||
})
|
||||
describe('AuthHelper', function() {
|
||||
it('throws without userKey or passKey', function() {
|
||||
expect(() => new AuthHelper({}, {})).to.throw(
|
||||
Error,
|
||||
'Expected userKey or passKey to be set'
|
||||
)
|
||||
})
|
||||
|
||||
describe('isValid', function () {
|
||||
describe('isValid', function() {
|
||||
function validate(config, privateConfig) {
|
||||
return new AuthHelper(
|
||||
{ authorizedOrigins: ['https://example.test'], ...config },
|
||||
{ private: privateConfig }
|
||||
).isValid
|
||||
return new AuthHelper(config, privateConfig).isValid
|
||||
}
|
||||
test(validate, () => {
|
||||
forCases([
|
||||
@@ -89,12 +65,9 @@ describe('AuthHelper', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('_basicAuth', function () {
|
||||
describe('basicAuth', function() {
|
||||
function validate(config, privateConfig) {
|
||||
return new AuthHelper(
|
||||
{ authorizedOrigins: ['https://example.test'], ...config },
|
||||
{ private: privateConfig }
|
||||
)._basicAuth
|
||||
return new AuthHelper(config, privateConfig).basicAuth
|
||||
}
|
||||
test(validate, () => {
|
||||
forCases([
|
||||
@@ -127,250 +100,4 @@ describe('AuthHelper', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('_isInsecureSslRequest', function () {
|
||||
test(AuthHelper._isInsecureSslRequest, () => {
|
||||
forCases([
|
||||
given({ url: 'http://example.test' }),
|
||||
given({ url: 'http://example.test', options: {} }),
|
||||
given({ url: 'http://example.test', options: { strictSSL: true } }),
|
||||
given({
|
||||
url: 'http://example.test',
|
||||
options: { strictSSL: undefined },
|
||||
}),
|
||||
]).expect(false)
|
||||
given({
|
||||
url: 'http://example.test',
|
||||
options: { strictSSL: false },
|
||||
}).expect(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('enforceStrictSsl', function () {
|
||||
const authConfig = {
|
||||
userKey: 'myci_user',
|
||||
passKey: 'myci_pass',
|
||||
serviceKey: 'myci',
|
||||
}
|
||||
|
||||
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 () {
|
||||
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
|
||||
})
|
||||
it('throws for insecure requests', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
})
|
||||
|
||||
context("when strict SSL isn't required", function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
myci: {
|
||||
authorizedOrigins: ['http://myci.test'],
|
||||
requireStrictSsl: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
})
|
||||
it('does not throw for secure requests', function () {
|
||||
expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
|
||||
})
|
||||
it('does not throw for insecure requests', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({ options: { strictSSL: false } })
|
||||
).not.to.throw()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldAuthenticateRequest', function () {
|
||||
const authConfig = {
|
||||
userKey: 'myci_user',
|
||||
passKey: 'myci_pass',
|
||||
serviceKey: 'myci',
|
||||
}
|
||||
|
||||
context('by default', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
myci: {
|
||||
authorizedOrigins: ['https://myci.test'],
|
||||
},
|
||||
},
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
})
|
||||
const shouldAuthenticateRequest = requestOptions =>
|
||||
authHelper.shouldAuthenticateRequest(requestOptions)
|
||||
describe('a secure request to an authorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({ url: 'https://myci.test/api' }).expect(true)
|
||||
})
|
||||
})
|
||||
describe('an insecure request', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({
|
||||
url: 'https://myci.test/api',
|
||||
options: { strictSSL: false },
|
||||
}).expect(false)
|
||||
})
|
||||
})
|
||||
describe('a request to an unauthorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
forCases([
|
||||
given({ url: 'http://myci.test/api' }),
|
||||
given({ url: 'https://myci.test:12345/api' }),
|
||||
given({ url: 'https://other.test/api' }),
|
||||
]).expect(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when auth over insecure SSL is allowed', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
myci: {
|
||||
authorizedOrigins: ['https://myci.test'],
|
||||
requireStrictSslToAuthenticate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
})
|
||||
const shouldAuthenticateRequest = requestOptions =>
|
||||
authHelper.shouldAuthenticateRequest(requestOptions)
|
||||
describe('a secure request to an authorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({ url: 'https://myci.test' }).expect(true)
|
||||
})
|
||||
})
|
||||
describe('an insecure request', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({
|
||||
url: 'https://myci.test',
|
||||
options: { strictSSL: false },
|
||||
}).expect(true)
|
||||
})
|
||||
})
|
||||
describe('a request to an unauthorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
forCases([
|
||||
given({ url: 'http://myci.test' }),
|
||||
given({ url: 'https://myci.test:12345/' }),
|
||||
given({ url: 'https://other.test' }),
|
||||
]).expect(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when the service is partly configured', function () {
|
||||
const authHelper = new AuthHelper(authConfig, {
|
||||
public: {
|
||||
services: {
|
||||
myci: {
|
||||
authorizedOrigins: ['https://myci.test'],
|
||||
requireStrictSslToAuthenticate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
private: { myci_user: 'admin' },
|
||||
})
|
||||
const shouldAuthenticateRequest = requestOptions =>
|
||||
authHelper.shouldAuthenticateRequest(requestOptions)
|
||||
describe('a secure request to an authorized origin', function () {
|
||||
test(shouldAuthenticateRequest, () => {
|
||||
given({ url: 'https://myci.test' }).expect(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('withBasicAuth', function () {
|
||||
const authHelper = new AuthHelper(
|
||||
{
|
||||
userKey: 'myci_user',
|
||||
passKey: 'myci_pass',
|
||||
serviceKey: 'myci',
|
||||
},
|
||||
{
|
||||
public: {
|
||||
services: {
|
||||
myci: {
|
||||
authorizedOrigins: ['https://myci.test'],
|
||||
},
|
||||
},
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
}
|
||||
)
|
||||
const withBasicAuth = requestOptions =>
|
||||
authHelper.withBasicAuth(requestOptions)
|
||||
|
||||
describe('authenticates a secure request to an authorized origin', function () {
|
||||
test(withBasicAuth, () => {
|
||||
given({
|
||||
url: 'https://myci.test/api',
|
||||
}).expect({
|
||||
url: 'https://myci.test/api',
|
||||
options: {
|
||||
auth: { user: 'admin', pass: 'abc123' },
|
||||
},
|
||||
})
|
||||
given({
|
||||
url: 'https://myci.test/api',
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
},
|
||||
}).expect({
|
||||
url: 'https://myci.test/api',
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
auth: { user: 'admin', pass: 'abc123' },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('does not authenticate a request to an unauthorized origin', function () {
|
||||
test(withBasicAuth, () => {
|
||||
given({
|
||||
url: 'https://other.test/api',
|
||||
}).expect({
|
||||
url: 'https://other.test/api',
|
||||
})
|
||||
given({
|
||||
url: 'https://other.test/api',
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
},
|
||||
}).expect({
|
||||
url: 'https://other.test/api',
|
||||
options: {
|
||||
headers: { Accept: 'application/json' },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('throws on an insecure SSL request', function () {
|
||||
expect(() =>
|
||||
withBasicAuth({
|
||||
url: 'https://myci.test/api',
|
||||
options: { strictSSL: false },
|
||||
})
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,9 +46,6 @@ class BaseGraphqlService extends BaseService {
|
||||
* and custom error messages e.g: `{ 404: 'package not found' }`.
|
||||
* This can be used to extend or override the
|
||||
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
|
||||
* @param {Function} [attrs.transformJson=data => data] Function which takes the raw json and transforms it before
|
||||
* further procesing. In case of multiple query in a single graphql call and few of them
|
||||
* throw error, partial data might be used ignoring the error.
|
||||
* @param {Function} [attrs.transformErrors=defaultTransformErrors]
|
||||
* Function which takes an errors object from a GraphQL
|
||||
* response and returns an instance of ShieldsRuntimeError.
|
||||
@@ -64,7 +61,6 @@ class BaseGraphqlService extends BaseService {
|
||||
variables = {},
|
||||
options = {},
|
||||
httpErrorMessages = {},
|
||||
transformJson = data => data,
|
||||
transformErrors = defaultTransformErrors,
|
||||
}) {
|
||||
const mergedOptions = {
|
||||
@@ -78,7 +74,7 @@ class BaseGraphqlService extends BaseService {
|
||||
options: mergedOptions,
|
||||
errorMessages: httpErrorMessages,
|
||||
})
|
||||
const json = transformJson(this._parseJson(buffer))
|
||||
const json = this._parseJson(buffer)
|
||||
if (json.errors) {
|
||||
const exception = transformErrors(json.errors)
|
||||
if (exception instanceof ShieldsRuntimeError) {
|
||||
|
||||
@@ -12,8 +12,15 @@ const dummySchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
class DummyGraphqlService extends BaseGraphqlService {
|
||||
static category = 'cat'
|
||||
static route = { base: 'foo' }
|
||||
static get category() {
|
||||
return 'cat'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'foo',
|
||||
}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestGraphql({
|
||||
@@ -29,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"}',
|
||||
@@ -41,7 +48,7 @@ describe('BaseGraphqlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
await DummyGraphqlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -57,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({
|
||||
@@ -91,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 },
|
||||
@@ -107,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 },
|
||||
@@ -124,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 },
|
||||
@@ -142,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 },
|
||||
@@ -160,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({
|
||||
@@ -171,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',
|
||||
|
||||
@@ -10,8 +10,15 @@ const dummySchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
class DummyJsonService extends BaseJsonService {
|
||||
static category = 'cat'
|
||||
static route = { base: 'foo' }
|
||||
static get category() {
|
||||
return 'cat'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'foo',
|
||||
}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestJson({
|
||||
@@ -22,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"}',
|
||||
@@ -34,7 +41,7 @@ describe('BaseJsonService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
await DummyJsonService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -42,13 +49,11 @@ describe('BaseJsonService', function () {
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.json',
|
||||
{
|
||||
headers: { Accept: 'application/json' },
|
||||
}
|
||||
{ headers: { Accept: 'application/json' } }
|
||||
)
|
||||
})
|
||||
|
||||
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({
|
||||
@@ -76,8 +81,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 },
|
||||
@@ -92,7 +97,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 },
|
||||
@@ -109,7 +114,7 @@ describe('BaseJsonService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles unparseable json responses', async function () {
|
||||
it('handles unparseable json responses', async function() {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'not json',
|
||||
res: { statusCode: 200 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const makeBadge = require('../../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')
|
||||
|
||||
@@ -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,
|
||||
@@ -13,6 +13,9 @@ const { prepareRoute, namedParamsForMatch } = require('./route')
|
||||
|
||||
module.exports = class BaseStaticService extends BaseService {
|
||||
static register({ camp, metricInstance }, serviceConfig) {
|
||||
const {
|
||||
profiling: { makeBadge: shouldProfileMakeBadge },
|
||||
} = serviceConfig
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
|
||||
const metricHelper = MetricHelper.create({
|
||||
@@ -49,9 +52,16 @@ module.exports = class BaseStaticService extends BaseService {
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
badgeData.format = format
|
||||
|
||||
if (shouldProfileMakeBadge) {
|
||||
console.time('makeBadge total')
|
||||
}
|
||||
const svg = makeBadge(badgeData)
|
||||
if (shouldProfileMakeBadge) {
|
||||
console.timeEnd('makeBadge total')
|
||||
}
|
||||
|
||||
setCacheHeadersForStaticResource(ask.res)
|
||||
|
||||
const svg = makeBadge(badgeData)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
|
||||
metricHandle.noteResponseSent()
|
||||
|
||||
@@ -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 }) {
|
||||
@@ -15,8 +15,15 @@ const schema = Joi.object({
|
||||
}).required()
|
||||
|
||||
class DummySvgScrapingService extends BaseSvgScrapingService {
|
||||
static category = 'cat'
|
||||
static route = { base: 'foo' }
|
||||
static get category() {
|
||||
return 'cat'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'foo',
|
||||
}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
return this._requestSvg({
|
||||
@@ -26,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({
|
||||
@@ -34,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,
|
||||
@@ -53,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 }
|
||||
@@ -61,13 +68,11 @@ describe('BaseSvgScrapingService', function () {
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.svg',
|
||||
{
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
}
|
||||
{ headers: { Accept: 'image/svg+xml' } }
|
||||
)
|
||||
})
|
||||
|
||||
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({
|
||||
@@ -98,8 +103,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 },
|
||||
@@ -114,9 +119,11 @@ describe('BaseSvgScrapingService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('allows overriding the valueMatcher', async function () {
|
||||
it('allows overriding the valueMatcher', async function() {
|
||||
class WithValueMatcher extends BaseSvgScrapingService {
|
||||
static route = {}
|
||||
static get route() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
return this._requestSvg({
|
||||
@@ -140,7 +147,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 },
|
||||
|
||||
@@ -10,8 +10,15 @@ const dummySchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
class DummyXmlService extends BaseXmlService {
|
||||
static category = 'cat'
|
||||
static route = { base: 'foo' }
|
||||
static get category() {
|
||||
return 'cat'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'foo',
|
||||
}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestXml({
|
||||
@@ -22,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>',
|
||||
@@ -34,7 +41,7 @@ describe('BaseXmlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
await DummyXmlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -42,15 +49,15 @@ describe('BaseXmlService', function () {
|
||||
|
||||
expect(sendAndCacheRequest).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.xml',
|
||||
{
|
||||
headers: { Accept: 'application/xml, text/xml' },
|
||||
}
|
||||
{ headers: { Accept: 'application/xml, text/xml' } }
|
||||
)
|
||||
})
|
||||
|
||||
it('forwards options to _sendAndCacheRequest', async function () {
|
||||
it('forwards options to _sendAndCacheRequest', async function() {
|
||||
class WithCustomOptions extends BaseXmlService {
|
||||
static route = {}
|
||||
static get route() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestXml({
|
||||
@@ -78,8 +85,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 },
|
||||
@@ -94,7 +101,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() {
|
||||
@@ -121,7 +128,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 },
|
||||
@@ -138,7 +145,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 },
|
||||
|
||||
@@ -10,8 +10,15 @@ const dummySchema = Joi.object({
|
||||
}).required()
|
||||
|
||||
class DummyYamlService extends BaseYamlService {
|
||||
static category = 'cat'
|
||||
static route = { base: 'foo' }
|
||||
static get category() {
|
||||
return 'cat'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'foo',
|
||||
}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
const { requiredString } = await this._requestYaml({
|
||||
@@ -38,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,
|
||||
@@ -50,7 +57,7 @@ describe('BaseYamlService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _sendAndCacheRequest', async function () {
|
||||
it('invokes _sendAndCacheRequest', async function() {
|
||||
await DummyYamlService.invoke(
|
||||
{ sendAndCacheRequest },
|
||||
{ handleInternalErrors: false }
|
||||
@@ -67,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({
|
||||
@@ -98,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 },
|
||||
@@ -114,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 },
|
||||
@@ -131,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 },
|
||||
|
||||
@@ -8,7 +8,7 @@ const emojic = require('emojic')
|
||||
const Joi = require('@hapi/joi')
|
||||
const log = require('../server/log')
|
||||
const { AuthHelper } = require('./auth-helper')
|
||||
const { MetricHelper, MetricNames } = require('./metric-helper')
|
||||
const { MetricHelper } = require('./metric-helper')
|
||||
const { assertValidCategory } = require('./categories')
|
||||
const checkErrorResponse = require('./check-error-response')
|
||||
const coalesceBadge = require('./coalesce-badge')
|
||||
@@ -58,7 +58,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 +70,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')
|
||||
@@ -90,7 +95,9 @@ class BaseService {
|
||||
throw new Error(`Category not set for ${this.name}`)
|
||||
}
|
||||
|
||||
static isDeprecated = false
|
||||
static get isDeprecated() {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to mount this service on
|
||||
@@ -112,12 +119,14 @@ class BaseService {
|
||||
* credentials to the request. For example:
|
||||
* - `{ options: { auth: this.authHelper.basicAuth } }`
|
||||
* - `{ options: { headers: this.authHelper.bearerAuthHeader } }`
|
||||
* - `{ options: { qs: { token: this.authHelper._pass } } }`
|
||||
* - `{ options: { qs: { token: this.authHelper.pass } } }`
|
||||
*
|
||||
* @abstract
|
||||
* @type {module:core/base-service/base~Auth}
|
||||
*/
|
||||
static auth = undefined
|
||||
static get auth() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of Example objects describing example URLs for this service.
|
||||
@@ -135,7 +144,9 @@ class BaseService {
|
||||
* @abstract
|
||||
* @type {module:core/base-service/base~Example[]}
|
||||
*/
|
||||
static examples = []
|
||||
static get examples() {
|
||||
return []
|
||||
}
|
||||
|
||||
static get _cacheLength() {
|
||||
const cacheLengths = {
|
||||
@@ -154,7 +165,9 @@ class BaseService {
|
||||
*
|
||||
* @type {module:core/base-service/base~DefaultBadgeData}
|
||||
*/
|
||||
static defaultBadgeData = {}
|
||||
static get defaultBadgeData() {
|
||||
return {}
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
throw new Error(`render() function not implemented for ${this.name}`)
|
||||
@@ -201,41 +214,20 @@ class BaseService {
|
||||
return result
|
||||
}
|
||||
|
||||
constructor(
|
||||
{ sendAndCacheRequest, authHelper, metricHelper },
|
||||
{ handleInternalErrors }
|
||||
) {
|
||||
constructor({ sendAndCacheRequest, authHelper }, { handleInternalErrors }) {
|
||||
this._requestFetcher = sendAndCacheRequest
|
||||
this.authHelper = authHelper
|
||||
this._handleInternalErrors = handleInternalErrors
|
||||
this._metricHelper = metricHelper
|
||||
}
|
||||
|
||||
async _request({ url, options = {}, errorMessages = {} }) {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
logTrace(emojic.bowAndArrow, 'Request', url, '\n', options)
|
||||
const { res, buffer } = await this._requestFetcher(url, options)
|
||||
await this._meterResponse(res, buffer)
|
||||
logTrace(emojic.dart, 'Response status code', res.statusCode)
|
||||
return checkErrorResponse(errorMessages)({ buffer, res })
|
||||
}
|
||||
|
||||
static enabledMetrics = []
|
||||
|
||||
static isMetricEnabled(metricName) {
|
||||
return this.enabledMetrics.includes(metricName)
|
||||
}
|
||||
|
||||
async _meterResponse(res, buffer) {
|
||||
if (
|
||||
this._metricHelper &&
|
||||
this.constructor.isMetricEnabled(MetricNames.SERVICE_RESPONSE_SIZE) &&
|
||||
res.statusCode === 200
|
||||
) {
|
||||
this._metricHelper.noteServiceResponseSize(buffer.length)
|
||||
}
|
||||
}
|
||||
|
||||
static _validate(
|
||||
data,
|
||||
schema,
|
||||
@@ -344,7 +336,9 @@ class BaseService {
|
||||
// Like the service instance, the auth helper could be reused for each request.
|
||||
// However, moving its instantiation to `register()` makes `invoke()` harder
|
||||
// to test.
|
||||
const authHelper = this.auth ? new AuthHelper(this.auth, config) : undefined
|
||||
const authHelper = this.auth
|
||||
? new AuthHelper(this.auth, config.private)
|
||||
: undefined
|
||||
|
||||
const serviceInstance = new this({ ...context, authHelper }, config)
|
||||
|
||||
@@ -432,7 +426,6 @@ class BaseService {
|
||||
sendAndCacheRequest: request.asPromise,
|
||||
sendAndCacheRequestWithCallbacks: request,
|
||||
githubApiProvider,
|
||||
metricHelper,
|
||||
},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const chai = require('chai')
|
||||
const { expect } = chai
|
||||
const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const prometheus = require('prom-client')
|
||||
const PrometheusMetrics = require('../server/prometheus-metrics')
|
||||
const trace = require('./trace')
|
||||
const {
|
||||
NotFound,
|
||||
@@ -15,9 +12,8 @@ const {
|
||||
Deprecated,
|
||||
} = require('./errors')
|
||||
const BaseService = require('./base')
|
||||
const { MetricHelper, MetricNames } = require('./metric-helper')
|
||||
|
||||
require('../register-chai-plugins.spec')
|
||||
chai.use(require('chai-as-promised'))
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
queryParamA: Joi.string(),
|
||||
@@ -29,19 +25,32 @@ const queryParamSchema = Joi.object({
|
||||
.required()
|
||||
|
||||
class DummyService extends BaseService {
|
||||
static category = 'other'
|
||||
static route = { base: 'foo', pattern: ':namedParamA', queryParamSchema }
|
||||
static get category() {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
pattern: ':world',
|
||||
namedParams: { world: 'World' },
|
||||
staticPreview: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
|
||||
keywords: ['hello'],
|
||||
},
|
||||
]
|
||||
static get route() {
|
||||
return {
|
||||
base: 'foo',
|
||||
pattern: ':namedParamA',
|
||||
queryParamSchema,
|
||||
}
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'cat', namedLogo: 'appveyor' }
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
pattern: ':world',
|
||||
namedParams: { world: 'World' },
|
||||
staticPreview: this.render({ namedParamA: 'foo', queryParamA: 'bar' }),
|
||||
keywords: ['hello'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'cat', namedLogo: 'appveyor' }
|
||||
}
|
||||
|
||||
static render({ namedParamA, queryParamA }) {
|
||||
return {
|
||||
@@ -54,20 +63,10 @@ class DummyService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
|
||||
static enabledMetrics = [MetricNames.SERVICE_RESPONSE_SIZE]
|
||||
}
|
||||
describe('BaseService', function() {
|
||||
const defaultConfig = { handleInternalErrors: false, private: {} }
|
||||
|
||||
describe('BaseService', function () {
|
||||
const defaultConfig = {
|
||||
public: {
|
||||
handleInternalErrors: false,
|
||||
services: {},
|
||||
},
|
||||
private: {},
|
||||
}
|
||||
|
||||
it('Invokes the handler as expected', async function () {
|
||||
it('Invokes the handler as expected', async function() {
|
||||
expect(
|
||||
await DummyService.invoke(
|
||||
{},
|
||||
@@ -80,7 +79,7 @@ describe('BaseService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('Validates query params', async function () {
|
||||
it('Validates query params', async function() {
|
||||
expect(
|
||||
await DummyService.invoke(
|
||||
{},
|
||||
@@ -95,47 +94,55 @@ 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$/
|
||||
'render() function not implemented for BaseService'
|
||||
)
|
||||
})
|
||||
|
||||
it('Should throw if route is not overridden', function () {
|
||||
return expect(BaseService.invoke({}, {}, {})).to.be.rejectedWith(
|
||||
/^Route not defined for BaseService$/
|
||||
)
|
||||
it('Should throw if route is not overridden', async function() {
|
||||
try {
|
||||
await BaseService.invoke({}, {}, {})
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e.message).to.equal('Route not defined for BaseService')
|
||||
}
|
||||
})
|
||||
|
||||
class WithRoute extends BaseService {
|
||||
static route = {}
|
||||
static get route() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
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 handle() is not overridden', async function() {
|
||||
try {
|
||||
await WithRoute.invoke({}, {}, {})
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e.message).to.equal('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$/
|
||||
'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,
|
||||
@@ -163,8 +170,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 {
|
||||
@@ -185,7 +192,7 @@ describe('BaseService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('On invalid data', function () {
|
||||
context('On invalid data', function() {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
return {
|
||||
@@ -194,7 +201,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(
|
||||
{},
|
||||
@@ -212,7 +219,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(
|
||||
{},
|
||||
@@ -227,8 +234,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")
|
||||
@@ -248,8 +255,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()
|
||||
@@ -264,7 +271,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()
|
||||
@@ -279,7 +286,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()
|
||||
@@ -294,7 +301,7 @@ describe('BaseService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('handles Deprecated', async function () {
|
||||
it('handles Deprecated', async function() {
|
||||
class ThrowingService extends DummyService {
|
||||
async handle() {
|
||||
throw new Deprecated()
|
||||
@@ -309,7 +316,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()
|
||||
@@ -326,7 +333,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)$/
|
||||
@@ -334,7 +341,7 @@ describe('BaseService', function () {
|
||||
let mockCamp
|
||||
let mockHandleRequest
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
mockCamp = {
|
||||
route: sinon.spy(),
|
||||
}
|
||||
@@ -345,12 +352,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 {
|
||||
@@ -375,7 +382,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,
|
||||
@@ -387,8 +394,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,
|
||||
@@ -415,36 +422,37 @@ 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 () {
|
||||
expect(() =>
|
||||
it('throws error for invalid responses', async function() {
|
||||
try {
|
||||
DummyService._validate(
|
||||
{ requiredString: ['this', "shouldn't", 'work'] },
|
||||
dummySchema
|
||||
)
|
||||
)
|
||||
.to.throw()
|
||||
.instanceof(InvalidResponse)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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 },
|
||||
@@ -474,7 +482,7 @@ describe('BaseService', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('handles errors', async function () {
|
||||
it('handles errors', async function() {
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: '',
|
||||
res: { statusCode: 404 },
|
||||
@@ -495,105 +503,37 @@ describe('BaseService', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Metrics', function () {
|
||||
let register
|
||||
beforeEach(function () {
|
||||
register = new prometheus.Registry()
|
||||
})
|
||||
const url = 'some-url'
|
||||
|
||||
it('service response size metric is optional', async function () {
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance: new PrometheusMetrics({ register }),
|
||||
ServiceClass: DummyServiceWithServiceResponseSizeMetricEnabled,
|
||||
})
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'x'.repeat(65536 + 1),
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
const serviceInstance = new DummyServiceWithServiceResponseSizeMetricEnabled(
|
||||
{ sendAndCacheRequest, metricHelper },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
await serviceInstance._request({ url })
|
||||
|
||||
expect(register.getSingleMetricAsString('service_response_bytes'))
|
||||
.to.contain(
|
||||
'service_response_bytes_bucket{le="65536",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 0\n'
|
||||
)
|
||||
.and.to.contain(
|
||||
'service_response_bytes_bucket{le="131072",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 1\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('service response size metric is disabled by default', async function () {
|
||||
const metricHelper = MetricHelper.create({
|
||||
metricInstance: new PrometheusMetrics({ register }),
|
||||
ServiceClass: DummyService,
|
||||
})
|
||||
const sendAndCacheRequest = async () => ({
|
||||
buffer: 'x',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ sendAndCacheRequest, metricHelper },
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
await serviceInstance._request({ url })
|
||||
|
||||
expect(
|
||||
register.getSingleMetricAsString('service_response_bytes')
|
||||
).to.not.contain('service_response_bytes_bucket')
|
||||
})
|
||||
})
|
||||
describe('auth', function () {
|
||||
describe('auth', function() {
|
||||
class AuthService extends DummyService {
|
||||
static auth = {
|
||||
passKey: 'myci_pass',
|
||||
serviceKey: 'myci',
|
||||
isRequired: true,
|
||||
static get auth() {
|
||||
return {
|
||||
passKey: 'myci_pass',
|
||||
isRequired: true,
|
||||
}
|
||||
}
|
||||
|
||||
async handle() {
|
||||
return {
|
||||
message: `The CI password is ${this.authHelper._pass}`,
|
||||
message: `The CI password is ${this.authHelper.pass}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
{},
|
||||
{
|
||||
public: {
|
||||
...defaultConfig.public,
|
||||
services: { myci: { authorizedOrigins: ['https://myci.test'] } },
|
||||
},
|
||||
private: { myci_pass: 'abc123' },
|
||||
},
|
||||
{ defaultConfig, private: { myci_pass: 'abc123' } },
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
)
|
||||
).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(
|
||||
{},
|
||||
{
|
||||
public: {
|
||||
...defaultConfig.public,
|
||||
services: { myci: { authorizedOrigins: ['https://myci.test'] } },
|
||||
},
|
||||
private: {},
|
||||
},
|
||||
{
|
||||
namedParamA: 'bar.bar.bar',
|
||||
}
|
||||
)
|
||||
await AuthService.invoke({}, defaultConfig, {
|
||||
namedParamA: 'bar.bar.bar',
|
||||
})
|
||||
).to.deep.equal({
|
||||
color: 'lightgray',
|
||||
isError: true,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -7,7 +7,7 @@ const defaultErrorMessages = {
|
||||
}
|
||||
|
||||
module.exports = function checkErrorResponse(errorMessages = {}) {
|
||||
return async function ({ buffer, res }) {
|
||||
return async function({ buffer, res }) {
|
||||
let error
|
||||
errorMessages = { ...defaultErrorMessages, ...errorMessages }
|
||||
if (res.statusCode === 404) {
|
||||
|
||||
@@ -4,11 +4,11 @@ const { expect } = require('chai')
|
||||
const { NotFound, InvalidResponse, Inaccessible } = require('./errors')
|
||||
const checkErrorResponse = require('./check-error-response')
|
||||
|
||||
describe('async error handler', function () {
|
||||
describe('async error handler', function() {
|
||||
const buffer = Buffer.from('some stuff')
|
||||
|
||||
context('when status is 200', function () {
|
||||
it('passes through the inputs', async function () {
|
||||
context('when status is 200', function() {
|
||||
it('passes through the inputs', async function() {
|
||||
const res = { statusCode: 200 }
|
||||
expect(await checkErrorResponse()({ res, buffer })).to.deep.equal({
|
||||
res,
|
||||
@@ -17,11 +17,11 @@ describe('async error handler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 404', function () {
|
||||
context('when status is 404', function() {
|
||||
const buffer = Buffer.from('some stuff')
|
||||
const res = { statusCode: 404 }
|
||||
|
||||
it('throws NotFound', async function () {
|
||||
it('throws NotFound', async function() {
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
expect.fail('Expected to throw')
|
||||
@@ -34,7 +34,7 @@ describe('async error handler', function () {
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom not found message', async function () {
|
||||
it('displays the custom not found message', async function() {
|
||||
const notFoundMessage = 'no goblins found'
|
||||
try {
|
||||
await checkErrorResponse({ 404: notFoundMessage })({ res, buffer })
|
||||
@@ -47,8 +47,8 @@ describe('async error handler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 4xx', function () {
|
||||
it('throws InvalidResponse', async function () {
|
||||
context('when status is 4xx', function() {
|
||||
it('throws InvalidResponse', async function() {
|
||||
const res = { statusCode: 499 }
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
@@ -64,7 +64,7 @@ describe('async error handler', function () {
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom error message', async function () {
|
||||
it('displays the custom error message', async function() {
|
||||
const res = { statusCode: 403 }
|
||||
try {
|
||||
await checkErrorResponse({ 403: 'access denied' })({ res })
|
||||
@@ -79,8 +79,8 @@ describe('async error handler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when status is 5xx', function () {
|
||||
it('throws Inaccessible', async function () {
|
||||
context('when status is 5xx', function() {
|
||||
it('throws Inaccessible', async function() {
|
||||
const res = { statusCode: 503 }
|
||||
try {
|
||||
await checkErrorResponse()({ res, buffer })
|
||||
@@ -96,7 +96,7 @@ describe('async error handler', function () {
|
||||
}
|
||||
})
|
||||
|
||||
it('displays the custom error message', async function () {
|
||||
it('displays the custom error message', async function() {
|
||||
const res = { statusCode: 500 }
|
||||
try {
|
||||
await checkErrorResponse({ 500: 'server overloaded' })({ res, buffer })
|
||||
|
||||
@@ -104,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) {
|
||||
|
||||
@@ -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 () {
|
||||
expect(
|
||||
coalesceBadge({}, {}, {}, { category: 'cat' }).text
|
||||
).to.deep.equal(['cat', 'n/a'])
|
||||
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
|
||||
|
||||
@@ -7,16 +7,16 @@ 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([])
|
||||
given(null, undefined, 0, {}).expect(0)
|
||||
|
||||
const a = null
|
||||
const c = 0
|
||||
const d = 1
|
||||
const a = null,
|
||||
c = 0,
|
||||
d = 1
|
||||
let b
|
||||
given(a, b, c, d).expect(0)
|
||||
})
|
||||
|
||||
@@ -26,17 +26,33 @@ function deprecatedService(attrs) {
|
||||
)
|
||||
|
||||
return class DeprecatedService extends BaseService {
|
||||
static name = name
|
||||
? `Deprecated${name}`
|
||||
: `Deprecated${camelcase(route.base.replace(/\//g, '_'), {
|
||||
pascalCase: true,
|
||||
})}`
|
||||
static get name() {
|
||||
return name
|
||||
? `Deprecated${name}`
|
||||
: `Deprecated${camelcase(route.base.replace(/\//g, '_'), {
|
||||
pascalCase: true,
|
||||
})}`
|
||||
}
|
||||
|
||||
static category = category
|
||||
static isDeprecated = true
|
||||
static route = route
|
||||
static examples = examples
|
||||
static defaultBadgeData = { label }
|
||||
static get category() {
|
||||
return category
|
||||
}
|
||||
|
||||
static get isDeprecated() {
|
||||
return true
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return route
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return examples
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label }
|
||||
}
|
||||
|
||||
async handle() {
|
||||
throw new Deprecated({ prettyMessage: message })
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -56,7 +56,6 @@ class NotFound extends ShieldsRuntimeError {
|
||||
get name() {
|
||||
return 'NotFound'
|
||||
}
|
||||
|
||||
get defaultPrettyMessage() {
|
||||
return defaultNotFoundError
|
||||
}
|
||||
@@ -83,7 +82,6 @@ class InvalidResponse extends ShieldsRuntimeError {
|
||||
get name() {
|
||||
return 'InvalidResponse'
|
||||
}
|
||||
|
||||
get defaultPrettyMessage() {
|
||||
return 'invalid'
|
||||
}
|
||||
@@ -109,7 +107,6 @@ class Inaccessible extends ShieldsRuntimeError {
|
||||
get name() {
|
||||
return 'Inaccessible'
|
||||
}
|
||||
|
||||
get defaultPrettyMessage() {
|
||||
return 'inaccessible'
|
||||
}
|
||||
@@ -134,7 +131,6 @@ class ImproperlyConfigured extends ShieldsRuntimeError {
|
||||
get name() {
|
||||
return 'ImproperlyConfigured'
|
||||
}
|
||||
|
||||
get defaultPrettyMessage() {
|
||||
return 'improperly configured'
|
||||
}
|
||||
@@ -160,7 +156,6 @@ class InvalidParameter extends ShieldsRuntimeError {
|
||||
get name() {
|
||||
return 'InvalidParameter'
|
||||
}
|
||||
|
||||
get defaultPrettyMessage() {
|
||||
return 'invalid parameter'
|
||||
}
|
||||
@@ -185,7 +180,6 @@ class Deprecated extends ShieldsRuntimeError {
|
||||
get name() {
|
||||
return 'Deprecated'
|
||||
}
|
||||
|
||||
get defaultPrettyMessage() {
|
||||
return 'no longer available'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { pathToRegexp, compile } = require('path-to-regexp')
|
||||
const pathToRegexp = require('path-to-regexp')
|
||||
const categories = require('../../services/categories')
|
||||
const coalesceBadge = require('./coalesce-badge')
|
||||
const { makeFullUrl } = require('./route')
|
||||
@@ -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()
|
||||
|
||||
@@ -52,9 +59,7 @@ function validateExample(example, index, ServiceClass) {
|
||||
|
||||
// Make sure we can build the full URL using these patterns.
|
||||
try {
|
||||
compile(pattern || ServiceClass.route.pattern, {
|
||||
encode: encodeURIComponent,
|
||||
})(namedParams)
|
||||
pathToRegexp.compile(pattern || ServiceClass.route.pattern)(namedParams)
|
||||
} catch (e) {
|
||||
throw Error(
|
||||
`In example for ${
|
||||
@@ -64,10 +69,7 @@ function validateExample(example, index, ServiceClass) {
|
||||
}
|
||||
// Make sure there are no extra keys.
|
||||
let keys = []
|
||||
pathToRegexp(pattern || ServiceClass.route.pattern, keys, {
|
||||
strict: true,
|
||||
sensitive: true,
|
||||
})
|
||||
pathToRegexp(pattern || ServiceClass.route.pattern, keys)
|
||||
keys = keys.map(({ name }) => name)
|
||||
const extraKeys = Object.keys(namedParams).filter(k => !keys.includes(k))
|
||||
if (extraKeys.length) {
|
||||
|
||||
@@ -4,8 +4,8 @@ const { expect } = require('chai')
|
||||
const { test, given } = require('sazerac')
|
||||
const { validateExample, transformExample } = require('./examples')
|
||||
|
||||
describe('validateExample function', function () {
|
||||
it('passes valid examples', function () {
|
||||
describe('validateExample function', function() {
|
||||
it('passes valid examples', function() {
|
||||
const validExamples = [
|
||||
{
|
||||
title: 'Package manager versioning badge',
|
||||
@@ -23,7 +23,7 @@ describe('validateExample function', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid examples', function () {
|
||||
it('rejects invalid examples', function() {
|
||||
const invalidExamples = [
|
||||
{},
|
||||
{ staticPreview: { message: '123' } },
|
||||
@@ -74,7 +74,7 @@ describe('validateExample function', function () {
|
||||
})
|
||||
})
|
||||
|
||||
test(transformExample, function () {
|
||||
test(transformExample, function() {
|
||||
const ExampleService = {
|
||||
name: 'ExampleService',
|
||||
route: {
|
||||
|
||||
@@ -7,8 +7,8 @@ const { mergeQueries } = require('./graphql')
|
||||
|
||||
require('../register-chai-plugins.spec')
|
||||
|
||||
describe('mergeQueries function', function () {
|
||||
it('merges valid gql queries', function () {
|
||||
describe('mergeQueries function', function() {
|
||||
it('merges valid gql queries', function() {
|
||||
expect(
|
||||
print(
|
||||
mergeQueries(
|
||||
@@ -86,7 +86,7 @@ describe('mergeQueries function', function () {
|
||||
).to.equalIgnoreSpaces('{ foo bar }')
|
||||
})
|
||||
|
||||
it('throws an error when passed invalid params', function () {
|
||||
it('throws an error when passed invalid params', function() {
|
||||
expect(() => mergeQueries('', '')).to.throw(Error)
|
||||
expect(() => mergeQueries(undefined, 17, true)).to.throw(Error)
|
||||
expect(() => mergeQueries(gql``, gql`foo`)).to.throw(Error)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const request = require('request')
|
||||
const queryString = require('query-string')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
const { setCacheHeaders } = require('./cache-headers')
|
||||
const {
|
||||
Inaccessible,
|
||||
@@ -13,8 +13,6 @@ const { makeSend } = require('./legacy-result-sender')
|
||||
const LruCache = require('./lru-cache')
|
||||
const coalesceBadge = require('./coalesce-badge')
|
||||
|
||||
const userAgent = 'Shields.io/2003a'
|
||||
|
||||
// We avoid calling the vendor's server for computation of the information in a
|
||||
// number of badges.
|
||||
const minAccuracy = 0.75
|
||||
@@ -107,20 +105,6 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
} = handlerOptions
|
||||
|
||||
return (queryParams, match, end, ask) => {
|
||||
/*
|
||||
This is here for legacy reasons. The badge server and frontend used to live
|
||||
on two different servers. When we merged them there was a conflict so we
|
||||
did this to avoid moving the endpoint docs to another URL.
|
||||
|
||||
Never ever do this again.
|
||||
*/
|
||||
if (match[0] === '/endpoint' && Object.keys(queryParams).length === 0) {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', '/endpoint/')
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
|
||||
const reqTime = new Date()
|
||||
|
||||
// `defaultCacheLengthSeconds` can be overridden by
|
||||
@@ -220,7 +204,8 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
options = uri
|
||||
}
|
||||
options.headers = options.headers || {}
|
||||
options.headers['User-Agent'] = userAgent
|
||||
options.headers['User-Agent'] =
|
||||
options.headers['User-Agent'] || 'Shields.io'
|
||||
|
||||
let bufferLength = 0
|
||||
const r = request(options, (err, res, body) => {
|
||||
@@ -309,5 +294,4 @@ module.exports = {
|
||||
clearRequestCache,
|
||||
// Expose for testing.
|
||||
_requestCache: requestCache,
|
||||
userAgent,
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3,12 +3,28 @@
|
||||
const BaseJsonService = require('../base-json')
|
||||
|
||||
class GoodServiceOne extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'one' }
|
||||
static get category() {
|
||||
return 'build'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'good',
|
||||
pattern: 'one',
|
||||
}
|
||||
}
|
||||
}
|
||||
class GoodServiceTwo extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'two' }
|
||||
static get category() {
|
||||
return 'build'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'good',
|
||||
pattern: 'two',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = [GoodServiceOne, GoodServiceTwo]
|
||||
|
||||
@@ -3,8 +3,16 @@
|
||||
const BaseJsonService = require('../base-json')
|
||||
|
||||
class GoodService extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'it/is', pattern: 'good' }
|
||||
static get category() {
|
||||
return 'build'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'it/is',
|
||||
pattern: 'good',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoodService
|
||||
|
||||
@@ -3,12 +3,28 @@
|
||||
const BaseJsonService = require('../base-json')
|
||||
|
||||
class GoodServiceOne extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'one' }
|
||||
static get category() {
|
||||
return 'build'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'good',
|
||||
pattern: 'one',
|
||||
}
|
||||
}
|
||||
}
|
||||
class GoodServiceTwo extends BaseJsonService {
|
||||
static category = 'build'
|
||||
static route = { base: 'good', pattern: 'two' }
|
||||
static get category() {
|
||||
return 'build'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'good',
|
||||
pattern: 'two',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GoodServiceOne, GoodServiceTwo }
|
||||
|
||||
@@ -82,12 +82,9 @@ function assertNamesUnique(names, { message }) {
|
||||
|
||||
function checkNames() {
|
||||
const services = loadServiceClasses()
|
||||
assertNamesUnique(
|
||||
services.map(({ name }) => name),
|
||||
{
|
||||
message: 'Duplicate service names found',
|
||||
}
|
||||
)
|
||||
assertNamesUnique(services.map(({ name }) => name), {
|
||||
message: 'Duplicate service names found',
|
||||
})
|
||||
}
|
||||
|
||||
function collectDefinitions() {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const { expect } = require('chai')
|
||||
const { loadServiceClasses, InvalidService } = require('./loader')
|
||||
|
||||
describe('loadServiceClasses function', function () {
|
||||
it('throws if module exports empty', function () {
|
||||
describe('loadServiceClasses function', function() {
|
||||
it('throws if module exports empty', function() {
|
||||
expect(() =>
|
||||
loadServiceClasses(['./loader-test-fixtures/empty-undefined.fixture.js'])
|
||||
).to.throw(InvalidService)
|
||||
@@ -26,7 +26,7 @@ describe('loadServiceClasses function', function () {
|
||||
).to.throw(InvalidService)
|
||||
})
|
||||
|
||||
it('throws if module exports invalid', function () {
|
||||
it('throws if module exports invalid', function() {
|
||||
expect(() =>
|
||||
loadServiceClasses(['./loader-test-fixtures/invalid-no-base.fixture.js'])
|
||||
).to.throw(InvalidService)
|
||||
@@ -47,7 +47,7 @@ describe('loadServiceClasses function', function () {
|
||||
).to.throw(InvalidService)
|
||||
})
|
||||
|
||||
it('registers services if module exports valid service classes', function () {
|
||||
it('registers services if module exports valid service classes', function() {
|
||||
expect(
|
||||
loadServiceClasses([
|
||||
'./loader-test-fixtures/valid-array.fixture.js',
|
||||
|
||||
@@ -126,7 +126,7 @@ Cache.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
clear: function () {
|
||||
clear: function() {
|
||||
this.cache.clear()
|
||||
this.newest = null
|
||||
this.oldest = null
|
||||
|
||||
@@ -25,14 +25,14 @@ function expectCacheSlots(cache, keys) {
|
||||
}
|
||||
}
|
||||
|
||||
describe('The LRU cache', function () {
|
||||
it('should support a zero capacity', function () {
|
||||
describe('The LRU cache', function() {
|
||||
it('should support a zero capacity', function() {
|
||||
const cache = new LRU(0)
|
||||
cache.set('key', 'value')
|
||||
expect(cache.cache.size).to.equal(0)
|
||||
})
|
||||
|
||||
it('should support a one capacity', function () {
|
||||
it('should support a one capacity', function() {
|
||||
const cache = new LRU(1)
|
||||
cache.set('key1', 'value1')
|
||||
expectCacheSlots(cache, ['key1'])
|
||||
@@ -42,7 +42,7 @@ describe('The LRU cache', function () {
|
||||
expect(cache.get('key2')).to.equal('value2')
|
||||
})
|
||||
|
||||
it('should remove the oldest element when reaching capacity', function () {
|
||||
it('should remove the oldest element when reaching capacity', function() {
|
||||
const cache = new LRU(2)
|
||||
|
||||
cache.set('key1', 'value1')
|
||||
@@ -57,7 +57,7 @@ describe('The LRU cache', function () {
|
||||
expect(cache.get('key3')).to.equal('value3')
|
||||
})
|
||||
|
||||
it('should make sure that resetting a key in cache makes it newest', function () {
|
||||
it('should make sure that resetting a key in cache makes it newest', function() {
|
||||
const cache = new LRU(2)
|
||||
|
||||
cache.set('key', 'value')
|
||||
@@ -70,9 +70,9 @@ describe('The LRU cache', function () {
|
||||
expectCacheSlots(cache, ['key2', 'key'])
|
||||
})
|
||||
|
||||
describe('getting a key in the cache', function () {
|
||||
context('when the requested key is oldest', function () {
|
||||
it('should leave the keys in the expected order', function () {
|
||||
describe('getting a key in the cache', function() {
|
||||
context('when the requested key is oldest', function() {
|
||||
it('should leave the keys in the expected order', function() {
|
||||
const cache = new LRU(2)
|
||||
cache.set('key1', 'value1')
|
||||
cache.set('key2', 'value2')
|
||||
@@ -85,8 +85,8 @@ describe('The LRU cache', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the requested key is newest', function () {
|
||||
it('should leave the keys in the expected order', function () {
|
||||
context('when the requested key is newest', function() {
|
||||
it('should leave the keys in the expected order', function() {
|
||||
const cache = new LRU(2)
|
||||
cache.set('key1', 'value1')
|
||||
cache.set('key2', 'value2')
|
||||
@@ -97,8 +97,8 @@ describe('The LRU cache', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the requested key is in the middle', function () {
|
||||
it('should leave the keys in the expected order', function () {
|
||||
context('when the requested key is in the middle', function() {
|
||||
it('should leave the keys in the expected order', function() {
|
||||
const cache = new LRU(3)
|
||||
cache.set('key1', 'value1')
|
||||
cache.set('key2', 'value2')
|
||||
@@ -113,7 +113,7 @@ describe('The LRU cache', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should clear', function () {
|
||||
it('should clear', function() {
|
||||
// Set up.
|
||||
const cache = new LRU(2)
|
||||
cache.set('key1', 'value1')
|
||||
|
||||
@@ -11,17 +11,9 @@ class MetricHelper {
|
||||
serviceFamily,
|
||||
name,
|
||||
})
|
||||
this.serviceResponseSizeHistogram = metricInstance.createServiceResponseSizeHistogram(
|
||||
{
|
||||
category,
|
||||
serviceFamily,
|
||||
name,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.metricInstance = undefined
|
||||
this.serviceRequestCounter = undefined
|
||||
this.serviceResponseSizeHistogram = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,16 +40,6 @@ class MetricHelper {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
noteServiceResponseSize(size) {
|
||||
if (this.serviceResponseSizeHistogram) {
|
||||
return this.serviceResponseSizeHistogram.observe(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MetricNames = Object.freeze({
|
||||
SERVICE_RESPONSE_SIZE: Symbol('service-response-size'),
|
||||
})
|
||||
|
||||
module.exports = { MetricHelper, MetricNames }
|
||||
module.exports = { MetricHelper }
|
||||
|
||||
@@ -41,15 +41,27 @@ module.exports = function redirector(attrs) {
|
||||
} = Joi.attempt(attrs, attrSchema, `Redirector for ${attrs.route.base}`)
|
||||
|
||||
return class Redirector extends BaseService {
|
||||
static name =
|
||||
name ||
|
||||
`${camelcase(route.base.replace(/\//g, '_'), {
|
||||
pascalCase: true,
|
||||
})}Redirect`
|
||||
static get name() {
|
||||
if (name) {
|
||||
return name
|
||||
} else {
|
||||
return `${camelcase(route.base.replace(/\//g, '_'), {
|
||||
pascalCase: true,
|
||||
})}Redirect`
|
||||
}
|
||||
}
|
||||
|
||||
static category = category
|
||||
static isDeprecated = true
|
||||
static route = route
|
||||
static get category() {
|
||||
return category
|
||||
}
|
||||
|
||||
static get isDeprecated() {
|
||||
return true
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return route
|
||||
}
|
||||
|
||||
static register({ camp, metricInstance }, { rasterUrl }) {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
const escapeStringRegexp = require('escape-string-regexp')
|
||||
const Joi = require('@hapi/joi')
|
||||
const { pathToRegexp } = require('path-to-regexp')
|
||||
const pathToRegexp = require('path-to-regexp')
|
||||
|
||||
function makeFullUrl(base, partialUrl) {
|
||||
return `/${[base, partialUrl].filter(Boolean).join('/')}`
|
||||
}
|
||||
|
||||
const isValidRoute = Joi.object({
|
||||
base: Joi.string().allow('').required(),
|
||||
base: Joi.string()
|
||||
.allow('')
|
||||
.required(),
|
||||
pattern: Joi.string().allow(''),
|
||||
format: Joi.string(),
|
||||
capture: Joi.alternatives().conditional('format', {
|
||||
|
||||
@@ -9,8 +9,8 @@ const {
|
||||
getQueryParamNames,
|
||||
} = require('./route')
|
||||
|
||||
describe('Route helpers', function () {
|
||||
context('A `pattern` with a named param is declared', function () {
|
||||
describe('Route helpers', function() {
|
||||
context('A `pattern` with a named param is declared', function() {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
pattern: ':namedParamA',
|
||||
@@ -36,7 +36,7 @@ describe('Route helpers', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('A `format` with a named param is declared', function () {
|
||||
context('A `format` with a named param is declared', function() {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '([^/]+?)',
|
||||
@@ -62,7 +62,7 @@ describe('Route helpers', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('No named params are declared', function () {
|
||||
context('No named params are declared', function() {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '(?:[^/]+)',
|
||||
@@ -78,7 +78,7 @@ describe('Route helpers', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('The wrong number of params are declared', function () {
|
||||
context('The wrong number of params are declared', function() {
|
||||
const { regex, captureNames } = prepareRoute({
|
||||
base: 'foo',
|
||||
format: '([^/]+)/([^/]+)',
|
||||
@@ -94,7 +94,7 @@ describe('Route helpers', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('getQueryParamNames', function () {
|
||||
it('getQueryParamNames', function() {
|
||||
expect(
|
||||
getQueryParamNames({
|
||||
queryParamSchema: Joi.object({ foo: Joi.string() }).required(),
|
||||
|
||||
@@ -5,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) {
|
||||
|
||||
@@ -21,8 +21,8 @@ function validate(
|
||||
}
|
||||
const options = { abortEarly: false }
|
||||
if (allowAndStripUnknownKeys) {
|
||||
options.allowUnknown = true
|
||||
options.stripUnknown = true
|
||||
options['allowUnknown'] = true
|
||||
options['stripUnknown'] = true
|
||||
}
|
||||
const { error, value } = schema.validate(data, options)
|
||||
if (error) {
|
||||
|
||||
@@ -7,19 +7,19 @@ const trace = require('./trace')
|
||||
const { InvalidParameter } = require('./errors')
|
||||
const validate = require('./validate')
|
||||
|
||||
describe('validate', function () {
|
||||
describe('validate', function() {
|
||||
const schema = Joi.object({
|
||||
requiredString: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
let sandbox
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(function () {
|
||||
afterEach(function() {
|
||||
sandbox.restore()
|
||||
})
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(trace, 'logTrace')
|
||||
})
|
||||
|
||||
@@ -35,8 +35,8 @@ describe('validate', function () {
|
||||
traceSuccessMessage,
|
||||
}
|
||||
|
||||
context('schema is not provided', function () {
|
||||
it('throws the expected programmer error', function () {
|
||||
context('schema is not provided', function() {
|
||||
it('throws the expected programmer error', function() {
|
||||
try {
|
||||
validate(options, { requiredString: 'bar' }, undefined)
|
||||
expect.fail('Expected to throw')
|
||||
@@ -47,8 +47,8 @@ describe('validate', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('data matches schema', function () {
|
||||
it('logs the data', function () {
|
||||
context('data matches schema', function() {
|
||||
it('logs the data', function() {
|
||||
validate(options, { requiredString: 'bar' }, schema)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'validate',
|
||||
@@ -60,8 +60,8 @@ describe('validate', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('data does not match schema', function () {
|
||||
it('logs the data and throws the expected error', function () {
|
||||
context('data does not match schema', function() {
|
||||
it('logs the data and throws the expected error', function() {
|
||||
try {
|
||||
validate(
|
||||
options,
|
||||
@@ -84,8 +84,8 @@ describe('validate', function () {
|
||||
)
|
||||
})
|
||||
|
||||
context('with includeKeys: true', function () {
|
||||
it('includes keys in the error text', function () {
|
||||
context('with includeKeys: true', function() {
|
||||
it('includes keys in the error text', function() {
|
||||
try {
|
||||
validate(
|
||||
{ ...options, includeKeys: true },
|
||||
@@ -108,7 +108,7 @@ describe('validate', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('allowAndStripUnknownKeys', function () {
|
||||
it('allowAndStripUnknownKeys', function() {
|
||||
try {
|
||||
validate(
|
||||
{ ...options, allowAndStripUnknownKeys: false, includeKeys: true },
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
'use strict'
|
||||
|
||||
const merge = require('deepmerge')
|
||||
const config = require('config').util.toObject()
|
||||
const portfinder = require('portfinder')
|
||||
const Server = require('./server')
|
||||
|
||||
async function createTestServer(customConfig = {}) {
|
||||
const mergedConfig = merge(config, customConfig)
|
||||
if (!mergedConfig.public.bind.port) {
|
||||
mergedConfig.public.bind.port = await portfinder.getPortPromise()
|
||||
function createTestServer({ port }) {
|
||||
const serverConfig = {
|
||||
...config,
|
||||
public: {
|
||||
...config.public,
|
||||
bind: {
|
||||
...config.public.bind,
|
||||
port,
|
||||
},
|
||||
},
|
||||
}
|
||||
return new Server(mergedConfig)
|
||||
|
||||
return new Server(serverConfig)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const { promisify } = require('util')
|
||||
const { post } = require('request')
|
||||
const postAsync = promisify(post)
|
||||
const generateInstanceId = require('./instance-id-generator')
|
||||
const { promClientJsonToInfluxV2 } = require('./metrics/format-converters')
|
||||
const log = require('./log')
|
||||
|
||||
module.exports = class InfluxMetrics {
|
||||
constructor(metricInstance, config) {
|
||||
this._metricInstance = metricInstance
|
||||
this._config = config
|
||||
this._instanceId = this.getInstanceId()
|
||||
}
|
||||
|
||||
async sendMetrics() {
|
||||
const auth = {
|
||||
user: this._config.username,
|
||||
pass: this._config.password,
|
||||
}
|
||||
const request = {
|
||||
uri: this._config.url,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: this.metrics(),
|
||||
timeout: this._config.timeoutMillseconds,
|
||||
auth,
|
||||
}
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await postAsync(request)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
new Error(`Cannot push metrics. Cause: ${error.name}: ${error.message}`)
|
||||
)
|
||||
}
|
||||
if (response && response.statusCode >= 300) {
|
||||
log.error(
|
||||
new Error(
|
||||
`Cannot push metrics. ${response.request.href} responded with status code ${response.statusCode}`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
startPushingMetrics() {
|
||||
this._intervalId = setInterval(
|
||||
() => this.sendMetrics(),
|
||||
this._config.intervalSeconds * 1000
|
||||
)
|
||||
}
|
||||
|
||||
metrics() {
|
||||
return promClientJsonToInfluxV2(this._metricInstance.metrics(), {
|
||||
env: this._config.envLabel,
|
||||
application: 'shields',
|
||||
instance: this._instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
getInstanceId() {
|
||||
const {
|
||||
hostnameAliases = {},
|
||||
instanceIdFrom,
|
||||
instanceIdEnvVarName,
|
||||
} = this._config
|
||||
let instance
|
||||
if (instanceIdFrom === 'env-var') {
|
||||
instance = process.env[instanceIdEnvVarName]
|
||||
} else if (instanceIdFrom === 'hostname') {
|
||||
const hostname = os.hostname()
|
||||
instance = hostnameAliases[hostname] || hostname
|
||||
} else if (instanceIdFrom === 'random') {
|
||||
instance = generateInstanceId()
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
stopPushingMetrics() {
|
||||
if (this._intervalId) {
|
||||
clearInterval(this._intervalId)
|
||||
this._intervalId = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const nock = require('nock')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const log = require('./log')
|
||||
const InfluxMetrics = require('./influx-metrics')
|
||||
require('../register-chai-plugins.spec')
|
||||
describe('Influx metrics', function () {
|
||||
const metricInstance = {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
describe('"metrics" function', function () {
|
||||
let osHostnameStub
|
||||
afterEach(function () {
|
||||
nock.enableNetConnect()
|
||||
delete process.env.INSTANCE_ID
|
||||
if (osHostnameStub) {
|
||||
osHostnameStub.restore()
|
||||
}
|
||||
})
|
||||
it('should use an environment variable value as an instance label', async function () {
|
||||
process.env.INSTANCE_ID = 'instance3'
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
instanceIdFrom: 'env-var',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
})
|
||||
|
||||
expect(influxMetrics.metrics()).to.contain('instance=instance3')
|
||||
})
|
||||
|
||||
it('should use a hostname as an instance label', async function () {
|
||||
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'hostname',
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname')
|
||||
})
|
||||
|
||||
it('should use a random string as an instance label', async function () {
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'random',
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.match(/instance=\w+ /)
|
||||
})
|
||||
|
||||
it('should use a hostname alias as an instance label', async function () {
|
||||
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'hostname',
|
||||
hostnameAliases: { 'test-hostname': 'test-hostname-alias' },
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.contain(
|
||||
'instance=test-hostname-alias'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('startPushingMetrics', function () {
|
||||
let influxMetrics, clock
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
influxMetrics.stopPushingMetrics()
|
||||
nock.cleanAll()
|
||||
nock.enableNetConnect()
|
||||
delete process.env.INSTANCE_ID
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('should send metrics', async function () {
|
||||
const scope = nock('http://shields-metrics.io/', {
|
||||
reqheaders: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.persist()
|
||||
.post(
|
||||
'/metrics',
|
||||
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11'
|
||||
)
|
||||
.basicAuth({ user: 'metrics-username', pass: 'metrics-password' })
|
||||
.reply(200)
|
||||
process.env.INSTANCE_ID = 'instance2'
|
||||
influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
url: 'http://shields-metrics.io/metrics',
|
||||
timeoutMillseconds: 100,
|
||||
intervalSeconds: 0.001,
|
||||
username: 'metrics-username',
|
||||
password: 'metrics-password',
|
||||
instanceIdFrom: 'env-var',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
envLabel: 'test-env',
|
||||
})
|
||||
|
||||
influxMetrics.startPushingMetrics()
|
||||
|
||||
await clock.tickAsync(10)
|
||||
expect(scope.isDone()).to.be.equal(
|
||||
true,
|
||||
`pending mocks: ${scope.pendingMocks()}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendMetrics', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(log, 'error')
|
||||
})
|
||||
afterEach(function () {
|
||||
log.error.restore()
|
||||
nock.cleanAll()
|
||||
nock.enableNetConnect()
|
||||
})
|
||||
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
url: 'http://shields-metrics.io/metrics',
|
||||
timeoutMillseconds: 50,
|
||||
intervalSeconds: 0,
|
||||
username: 'metrics-username',
|
||||
password: 'metrics-password',
|
||||
})
|
||||
it('should log errors', async function () {
|
||||
nock.disableNetConnect()
|
||||
|
||||
await influxMetrics.sendMetrics()
|
||||
|
||||
expect(log.error).to.be.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. Cause: NetConnectNotAllowedError: Nock: Disallowed net connect for "shields-metrics.io:80/metrics"'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('should log error responses', async function () {
|
||||
nock('http://shields-metrics.io/').persist().post('/metrics').reply(400)
|
||||
|
||||
await influxMetrics.sendMetrics()
|
||||
|
||||
expect(log.error).to.be.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. http://shields-metrics.io/metrics responded with status code 400'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
function generateInstanceId() {
|
||||
// from https://gist.github.com/gordonbrander/2230317
|
||||
return Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
module.exports = generateInstanceId
|
||||
@@ -1,27 +0,0 @@
|
||||
'use strict'
|
||||
const groupBy = require('lodash.groupby')
|
||||
|
||||
function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
|
||||
// TODO Replace with Array.prototype.flatMap() after migrating to Node.js >= 11
|
||||
const flatMap = (f, arr) => arr.reduce((acc, x) => acc.concat(f(x)), [])
|
||||
return flatMap(metric => {
|
||||
const valuesByLabels = groupBy(metric.values, value =>
|
||||
JSON.stringify(Object.entries(value.labels).sort())
|
||||
)
|
||||
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
|
||||
const labels = Object.entries(metricsWithSameLabel[0].labels)
|
||||
.concat(Object.entries(extraLabels))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`)
|
||||
.join(',')
|
||||
const labelsFormatted = labels ? `,${labels}` : ''
|
||||
const values = metricsWithSameLabel
|
||||
.sort((a, b) => a.metricName.localeCompare(b.metricName))
|
||||
.map(value => `${value.metricName || metric.name}=${value.value}`)
|
||||
.join(',')
|
||||
return `prometheus${labelsFormatted} ${values}`
|
||||
})
|
||||
}, metrics).join('\n')
|
||||
}
|
||||
|
||||
module.exports = { promClientJsonToInfluxV2 }
|
||||
@@ -1,213 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const prometheus = require('prom-client')
|
||||
const { promClientJsonToInfluxV2 } = require('./format-converters')
|
||||
|
||||
describe('Metric format converters', function () {
|
||||
describe('prom-client JSON to InfluxDB line protocol (version 2)', function () {
|
||||
it('converts a counter', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter1=11')
|
||||
})
|
||||
|
||||
it('converts a counter (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const counter = new prometheus.Counter({
|
||||
name: 'counter1',
|
||||
help: 'counter 1 help',
|
||||
registers: [register],
|
||||
})
|
||||
counter.inc(11)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter1=11')
|
||||
})
|
||||
|
||||
it('converts a gauge', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'gause 1 help',
|
||||
name: 'gauge1',
|
||||
type: 'gauge',
|
||||
values: [{ value: 20, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||
})
|
||||
|
||||
it('converts a gauge (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const gauge = new prometheus.Gauge({
|
||||
name: 'gauge1',
|
||||
help: 'gauge 1 help',
|
||||
registers: [register],
|
||||
})
|
||||
gauge.inc(20)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||
})
|
||||
|
||||
const sortLines = text => text.split('\n').sort().join('\n')
|
||||
|
||||
it('converts a histogram', function () {
|
||||
const json = [
|
||||
{
|
||||
name: 'histogram1',
|
||||
help: 'histogram 1 help',
|
||||
type: 'histogram',
|
||||
values: [
|
||||
{ labels: { le: 5 }, value: 1, metricName: 'histogram1_bucket' },
|
||||
{ labels: { le: 15 }, value: 2, metricName: 'histogram1_bucket' },
|
||||
{ labels: { le: 50 }, value: 2, metricName: 'histogram1_bucket' },
|
||||
{
|
||||
labels: { le: '+Inf' },
|
||||
value: 3,
|
||||
metricName: 'histogram1_bucket',
|
||||
},
|
||||
{ labels: {}, value: 111, metricName: 'histogram1_sum' },
|
||||
{ labels: {}, value: 3, metricName: 'histogram1_count' },
|
||||
],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||
prometheus,le=50 histogram1_bucket=2
|
||||
prometheus,le=15 histogram1_bucket=2
|
||||
prometheus,le=5 histogram1_bucket=1
|
||||
prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a histogram (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const histogram = new prometheus.Histogram({
|
||||
name: 'histogram1',
|
||||
help: 'histogram 1 help',
|
||||
buckets: [5, 15, 50],
|
||||
registers: [register],
|
||||
})
|
||||
histogram.observe(100)
|
||||
histogram.observe(10)
|
||||
histogram.observe(1)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||
prometheus,le=50 histogram1_bucket=2
|
||||
prometheus,le=15 histogram1_bucket=2
|
||||
prometheus,le=5 histogram1_bucket=1
|
||||
prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a summary', function () {
|
||||
const json = [
|
||||
{
|
||||
name: 'summary1',
|
||||
help: 'summary 1 help',
|
||||
type: 'summary',
|
||||
values: [
|
||||
{ labels: { quantile: 0.1 }, value: 1 },
|
||||
{ labels: { quantile: 0.9 }, value: 100 },
|
||||
{ labels: { quantile: 0.99 }, value: 100 },
|
||||
{ metricName: 'summary1_sum', labels: {}, value: 111 },
|
||||
{ metricName: 'summary1_count', labels: {}, value: 3 },
|
||||
],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||
prometheus,quantile=0.9 summary1=100
|
||||
prometheus,quantile=0.1 summary1=1
|
||||
prometheus summary1_count=3,summary1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a summary (from prometheus registry)', function () {
|
||||
const register = new prometheus.Registry()
|
||||
const summary = new prometheus.Summary({
|
||||
name: 'summary1',
|
||||
help: 'summary 1 help',
|
||||
percentiles: [0.1, 0.9, 0.99],
|
||||
registers: [register],
|
||||
})
|
||||
summary.observe(100)
|
||||
summary.observe(10)
|
||||
summary.observe(1)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||
prometheus,quantile=0.9 summary1=100
|
||||
prometheus,quantile=0.1 summary1=1
|
||||
prometheus summary1_count=3,summary1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a counter and skip a timestamp', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 4 help',
|
||||
name: 'counter4',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {}, timestamp: 1581850552292 }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter4=11')
|
||||
})
|
||||
|
||||
it('converts a counter and adds extra labels', function () {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json, {
|
||||
instance: 'instance1',
|
||||
env: 'production',
|
||||
})
|
||||
|
||||
expect(influx).to.be.equal(
|
||||
'prometheus,env=production,instance=instance1 counter1=11'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -20,12 +20,12 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
||||
const ipRateLimit = new RateLimit({
|
||||
// Exclude IPs for GitHub Camo, determined experimentally by running e.g.
|
||||
// `curl --insecure -u ":shields-secret" https://s0.shields-server.com/sys/rate-limit`
|
||||
safelist: /^(?:192\.30\.252\.\d+)|(?:140\.82\.115\.\d+)$/,
|
||||
whitelist: /^(?:192\.30\.252\.\d+)|(?:140\.82\.115\.\d+)$/,
|
||||
})
|
||||
const badgeTypeRateLimit = new RateLimit({ maxHitsPerPeriod: 3000 })
|
||||
const refererRateLimit = new RateLimit({
|
||||
maxHitsPerPeriod: 300,
|
||||
safelist: /^https?:\/\/shields\.io\/$/,
|
||||
whitelist: /^https?:\/\/shields\.io\/$/,
|
||||
})
|
||||
|
||||
server.handle((req, res, next) => {
|
||||
@@ -39,8 +39,11 @@ 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 referer = req.headers.referer
|
||||
const badgeType = req.url
|
||||
.split(/[/-]/)
|
||||
.slice(0, 3)
|
||||
.join('')
|
||||
const referer = req.headers['referer']
|
||||
|
||||
if (ipRateLimit.isBanned(ip, req, res)) {
|
||||
metricInstance.noteRateLimitExceeded('ip')
|
||||
@@ -88,7 +91,7 @@ function setRoutes({ rateLimit }, { server, metricInstance }) {
|
||||
})
|
||||
})
|
||||
|
||||
return function () {
|
||||
return function() {
|
||||
ipRateLimit.stop()
|
||||
badgeTypeRateLimit.stop()
|
||||
refererRateLimit.stop()
|
||||
|
||||
@@ -4,8 +4,8 @@ const decamelize = require('decamelize')
|
||||
const prometheus = require('prom-client')
|
||||
|
||||
module.exports = class PrometheusMetrics {
|
||||
constructor({ register } = {}) {
|
||||
this.register = register || new prometheus.Registry()
|
||||
constructor() {
|
||||
this.register = new prometheus.Registry()
|
||||
this.counters = {
|
||||
numRequests: new prometheus.Counter({
|
||||
name: 'service_requests_total',
|
||||
@@ -59,22 +59,12 @@ module.exports = class PrometheusMetrics {
|
||||
labelNames: ['rate_limit_type'],
|
||||
registers: [this.register],
|
||||
}),
|
||||
serviceResponseSize: new prometheus.Histogram({
|
||||
name: 'service_response_bytes',
|
||||
help: 'Service response size in bytes',
|
||||
labelNames: ['category', 'family', 'service'],
|
||||
// buckets: 64KiB, 128KiB, 256KiB, 512KiB, 1MiB, 2MiB, 4MiB, 8MiB
|
||||
buckets: prometheus.exponentialBuckets(64 * 1024, 2, 8),
|
||||
registers: [this.register],
|
||||
}),
|
||||
}
|
||||
this.interval = prometheus.collectDefaultMetrics({
|
||||
register: this.register,
|
||||
})
|
||||
}
|
||||
|
||||
async registerMetricsEndpoint(server) {
|
||||
async initialize(server) {
|
||||
const { register } = this
|
||||
this.interval = prometheus.collectDefaultMetrics({ register })
|
||||
|
||||
server.route(/^\/metrics$/, (data, match, end, ask) => {
|
||||
ask.res.setHeader('Content-Type', register.contentType)
|
||||
@@ -90,15 +80,7 @@ module.exports = class PrometheusMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
metrics() {
|
||||
return this.register.getMetricsAsJSON()
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 }) {
|
||||
@@ -113,13 +95,4 @@ module.exports = class PrometheusMetrics {
|
||||
noteRateLimitExceeded(rateLimitType) {
|
||||
return this.counters.rateLimitExceeded.labels(rateLimitType).inc()
|
||||
}
|
||||
|
||||
createServiceResponseSizeHistogram({ category, serviceFamily, name }) {
|
||||
const service = decamelize(name)
|
||||
return this.counters.serviceResponseSize.labels(
|
||||
category,
|
||||
serviceFamily,
|
||||
service
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
'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 () {
|
||||
let port, baseUrl, camp, metrics
|
||||
beforeEach(async function () {
|
||||
describe('Prometheus metrics route', function() {
|
||||
let port, baseUrl
|
||||
beforeEach(async function() {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function() {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function () {
|
||||
if (metrics) {
|
||||
metrics.stop()
|
||||
}
|
||||
afterEach(async function() {
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('returns default metrics', async function () {
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(camp)
|
||||
it('returns metrics', async function() {
|
||||
new Metrics({ enabled: true }).initialize(camp)
|
||||
|
||||
const { statusCode, body } = await got(`${baseUrl}/metrics`)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = class RateLimit {
|
||||
this.maxHitsPerPeriod = options.maxHitsPerPeriod || 500
|
||||
this.banned = new Set()
|
||||
this.bannedUrls = new Set()
|
||||
this.safelist = options.safelist || /(?!)/ // Matches nothing by default.
|
||||
this.whitelist = options.whitelist || /(?!)/ // Matches nothing by default.
|
||||
this.interval = setInterval(this.resetHits.bind(this), this.period * 1000)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ module.exports = class RateLimit {
|
||||
const hitsInCurrentPeriod = this.hits.get(reqParam) || 0
|
||||
if (
|
||||
reqParam != null &&
|
||||
!this.safelist.test(reqParam) &&
|
||||
!this.whitelist.test(reqParam) &&
|
||||
hitsInCurrentPeriod > this.maxHitsPerPeriod
|
||||
) {
|
||||
this.banned.add(reqParam)
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
const { URL } = url
|
||||
const bytes = require('bytes')
|
||||
const Camp = require('@shields_io/camp')
|
||||
const originalJoi = require('@hapi/joi')
|
||||
const makeBadge = require('../../badge-maker/lib/make-badge')
|
||||
const Joi = require('@hapi/joi')
|
||||
const Camp = require('camp')
|
||||
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')
|
||||
@@ -23,88 +22,25 @@ const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
|
||||
const log = require('./log')
|
||||
const sysMonitor = require('./monitor')
|
||||
const PrometheusMetrics = require('./prometheus-metrics')
|
||||
const InfluxMetrics = require('./influx-metrics')
|
||||
|
||||
const Joi = originalJoi
|
||||
.extend(base => ({
|
||||
type: 'arrayFromString',
|
||||
base: base.array(),
|
||||
coerce: (value, state, options) => ({
|
||||
value: typeof value === 'string' ? value.split(' ') : value,
|
||||
}),
|
||||
}))
|
||||
.extend(base => ({
|
||||
type: 'string',
|
||||
base: base.string(),
|
||||
messages: {
|
||||
'string.origin':
|
||||
'needs to be an origin string, e.g. https://host.domain with optional port and no trailing slash',
|
||||
},
|
||||
rules: {
|
||||
origin: {
|
||||
validate(value, helpers) {
|
||||
let origin
|
||||
try {
|
||||
;({ origin } = new URL(value))
|
||||
} catch (e) {}
|
||||
if (origin !== undefined && origin === value) {
|
||||
return value
|
||||
} else {
|
||||
return helpers.error('string.origin')
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
|
||||
const requiredUrl = optionalUrl.required()
|
||||
const origins = Joi.arrayFromString().items(Joi.string().origin())
|
||||
const defaultService = Joi.object({ authorizedOrigins: origins }).default({
|
||||
authorizedOrigins: [],
|
||||
})
|
||||
|
||||
const publicConfigSchema = Joi.object({
|
||||
bind: {
|
||||
port: Joi.alternatives().try(
|
||||
Joi.number().port(),
|
||||
Joi.string().pattern(/^\\\\\.\\pipe\\.+$/)
|
||||
),
|
||||
port: Joi.number().port(),
|
||||
address: Joi.alternatives().try(
|
||||
Joi.string().ip().required(),
|
||||
Joi.string().hostname().required()
|
||||
Joi.string()
|
||||
.ip()
|
||||
.required(),
|
||||
Joi.string()
|
||||
.hostname()
|
||||
.required()
|
||||
),
|
||||
},
|
||||
metrics: {
|
||||
prometheus: {
|
||||
enabled: Joi.boolean().required(),
|
||||
endpointEnabled: Joi.boolean().required(),
|
||||
},
|
||||
influx: {
|
||||
enabled: Joi.boolean().required(),
|
||||
url: Joi.string()
|
||||
.uri()
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
timeoutMilliseconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
intervalSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
instanceIdFrom: Joi.string()
|
||||
.equal('hostname', 'env-var', 'random')
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
|
||||
is: 'env-var',
|
||||
then: Joi.required(),
|
||||
}),
|
||||
envLabel: Joi.string().when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required(),
|
||||
}),
|
||||
hostnameAliases: Joi.object(),
|
||||
},
|
||||
},
|
||||
ssl: {
|
||||
@@ -115,35 +51,33 @@ 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(),
|
||||
},
|
||||
services: Joi.object({
|
||||
bitbucketServer: defaultService,
|
||||
drone: defaultService,
|
||||
services: {
|
||||
github: {
|
||||
baseUri: requiredUrl,
|
||||
debug: {
|
||||
enabled: Joi.boolean().required(),
|
||||
intervalSeconds: Joi.number().integer().min(1).required(),
|
||||
intervalSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.required(),
|
||||
},
|
||||
},
|
||||
jira: defaultService,
|
||||
jenkins: Joi.object({
|
||||
authorizedOrigins: origins,
|
||||
requireStrictSsl: Joi.boolean(),
|
||||
requireStrictSslToAuthenticate: Joi.boolean(),
|
||||
}).default({ authorizedOrigins: [] }),
|
||||
nexus: defaultService,
|
||||
npm: defaultService,
|
||||
sonar: defaultService,
|
||||
teamcity: defaultService,
|
||||
trace: Joi.boolean().required(),
|
||||
}).required(),
|
||||
},
|
||||
profiling: {
|
||||
makeBadge: Joi.boolean().required(),
|
||||
},
|
||||
cacheHeaders: {
|
||||
defaultCacheLengthSeconds: Joi.number().integer().required(),
|
||||
defaultCacheLengthSeconds: Joi.number()
|
||||
.integer()
|
||||
.required(),
|
||||
},
|
||||
rateLimit: Joi.boolean().required(),
|
||||
handleInternalErrors: Joi.boolean().required(),
|
||||
@@ -154,8 +88,6 @@ const privateConfigSchema = Joi.object({
|
||||
azure_devops_token: Joi.string(),
|
||||
bintray_user: Joi.string(),
|
||||
bintray_apikey: Joi.string(),
|
||||
discord_bot_token: Joi.string(),
|
||||
drone_token: Joi.string(),
|
||||
gh_client_id: Joi.string(),
|
||||
gh_client_secret: Joi.string(),
|
||||
gh_token: Joi.string(),
|
||||
@@ -173,19 +105,11 @@ const privateConfigSchema = Joi.object({
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
sonarqube_token: Joi.string(),
|
||||
teamcity_user: Joi.string(),
|
||||
teamcity_pass: Joi.string(),
|
||||
twitch_client_id: Joi.string(),
|
||||
twitch_client_secret: Joi.string(),
|
||||
wheelmap_token: Joi.string(),
|
||||
influx_username: Joi.string(),
|
||||
influx_password: Joi.string(),
|
||||
youtube_api_key: Joi.string(),
|
||||
}).required()
|
||||
const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
||||
influx_username: Joi.string().required(),
|
||||
influx_password: Joi.string().required(),
|
||||
})
|
||||
|
||||
/**
|
||||
* The Server is based on the web framework Scoutcamp. It creates
|
||||
* an http server, sets up helpers for token persistence and monitoring.
|
||||
@@ -197,25 +121,22 @@ class Server {
|
||||
* Badge Server Constructor
|
||||
*
|
||||
* @param {object} config Configuration object read from config yaml files
|
||||
* by https://www.npmjs.com/package/config and validated against
|
||||
* publicConfigSchema and privateConfigSchema
|
||||
* by https://www.npmjs.com/package/config and validated against
|
||||
* publicConfigSchema and privateConfigSchema
|
||||
* @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
|
||||
* @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
|
||||
*/
|
||||
constructor(config) {
|
||||
const publicConfig = Joi.attempt(config.public, publicConfigSchema)
|
||||
const privateConfig = this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateConfigSchema
|
||||
)
|
||||
// We want to require an username and a password for the influx metrics
|
||||
// only if the influx metrics are enabled. The private config schema
|
||||
// and the public config schema are two separate schemas so we have to run
|
||||
// validation manually.
|
||||
if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
|
||||
this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateMetricsInfluxConfigSchema
|
||||
let privateConfig
|
||||
try {
|
||||
privateConfig = Joi.attempt(config.private, privateConfigSchema)
|
||||
} catch (e) {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
)
|
||||
}
|
||||
this.config = {
|
||||
@@ -228,31 +149,8 @@ class Server {
|
||||
service: publicConfig.services.github,
|
||||
private: privateConfig,
|
||||
})
|
||||
|
||||
if (publicConfig.metrics.prometheus.enabled) {
|
||||
this.metricInstance = new PrometheusMetrics()
|
||||
if (publicConfig.metrics.influx.enabled) {
|
||||
this.influxMetrics = new InfluxMetrics(
|
||||
this.metricInstance,
|
||||
Object.assign({}, publicConfig.metrics.influx, {
|
||||
username: privateConfig.influx_username,
|
||||
password: privateConfig.influx_password,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePrivateConfig(privateConfig, privateConfigSchema) {
|
||||
try {
|
||||
return Joi.attempt(privateConfig, privateConfigSchema)
|
||||
} catch (e) {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,11 +187,7 @@ class Server {
|
||||
|
||||
camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
|
||||
const [, format] = match
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeSend('svg', request.res, end)(
|
||||
makeBadge({
|
||||
text: ['410', `${format} no longer available`],
|
||||
color: 'lightgray',
|
||||
@@ -304,11 +198,7 @@ class Server {
|
||||
|
||||
if (!rasterUrl) {
|
||||
camp.route(/\.png$/, (query, match, end, request) => {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeSend('svg', request.res, end)(
|
||||
makeBadge({
|
||||
text: ['404', 'raster badges not available'],
|
||||
color: 'lightgray',
|
||||
@@ -322,11 +212,7 @@ class Server {
|
||||
const [, extension] = match
|
||||
const format = (extension || '.svg').replace(/^\./, '')
|
||||
|
||||
makeSend(
|
||||
format,
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeSend(format, request.res, end)(
|
||||
makeBadge({
|
||||
text: ['404', 'badge not found'],
|
||||
color: 'red',
|
||||
@@ -388,10 +274,10 @@ class Server {
|
||||
{
|
||||
handleInternalErrors: config.public.handleInternalErrors,
|
||||
cacheHeaders: config.public.cacheHeaders,
|
||||
profiling: config.public.profiling,
|
||||
fetchLimitBytes: bytes(config.public.fetchLimit),
|
||||
rasterUrl: config.public.rasterUrl,
|
||||
private: config.private,
|
||||
public: config.public,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -413,7 +299,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,
|
||||
@@ -429,14 +315,9 @@ class Server {
|
||||
)
|
||||
|
||||
const { githubConstellation } = this
|
||||
await githubConstellation.initialize(camp)
|
||||
githubConstellation.initialize(camp)
|
||||
if (metricInstance) {
|
||||
if (this.config.public.metrics.prometheus.endpointEnabled) {
|
||||
metricInstance.registerMetricsEndpoint(camp)
|
||||
}
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.startPushingMetrics()
|
||||
}
|
||||
metricInstance.initialize(camp)
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
@@ -446,8 +327,6 @@ class Server {
|
||||
this.registerRedirects()
|
||||
this.registerServices()
|
||||
|
||||
camp.listenAsConfigured()
|
||||
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
}
|
||||
|
||||
@@ -482,9 +361,6 @@ class Server {
|
||||
}
|
||||
|
||||
if (this.metricInstance) {
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.stopPushingMetrics()
|
||||
}
|
||||
this.metricInstance.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,331 +2,153 @@
|
||||
|
||||
const { expect } = require('chai')
|
||||
const isSvg = require('is-svg')
|
||||
const config = require('config')
|
||||
const portfinder = require('portfinder')
|
||||
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 () {
|
||||
let server, baseUrl
|
||||
before('Start the server', async function () {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
server = await createTestServer()
|
||||
baseUrl = server.baseUrl
|
||||
await server.start()
|
||||
})
|
||||
after('Shut down the server', async function () {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
server = undefined
|
||||
})
|
||||
|
||||
it('should allow strings for port', async function () {
|
||||
// fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
|
||||
const pipeServer = await createTestServer({
|
||||
public: {
|
||||
bind: {
|
||||
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(pipeServer).to.not.be.undefined
|
||||
})
|
||||
|
||||
it('should produce colorscheme badges', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should redirect colorscheme PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/:fruit-apple-green.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect modern PNG badges as configured', async function () {
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce json badges', async function () {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}twitter/follow/_Pyves.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
})
|
||||
|
||||
it('should preserve label case', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body).to.satisfy(isSvg).and.to.include('fRuiT')
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/pull/1319
|
||||
it('should not crash with a numeric logo', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?logo=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should not crash with a numeric link', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should not crash with a boolean link', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=true`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should return the 404 badge for unknown badges', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/not/a/badge.svg`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should return the 404 badge page for rando links', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should redirect the root as configured', async function () {
|
||||
const { statusCode, headers } = await got(baseUrl, {
|
||||
followRedirect: false,
|
||||
})
|
||||
|
||||
expect(statusCode).to.equal(302)
|
||||
// This value is set in `config/test.yml`
|
||||
expect(headers.location).to.equal('http://frontend.example.test')
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('410')
|
||||
.and.to.include('jpg no longer available')
|
||||
})
|
||||
describe('The server', function() {
|
||||
let server, baseUrl
|
||||
before('Start the server', async function() {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
const port = await portfinder.getPortPromise()
|
||||
server = createTestServer({ port })
|
||||
baseUrl = server.baseUrl
|
||||
await server.start()
|
||||
})
|
||||
after('Shut down the server', async function() {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
server = undefined
|
||||
})
|
||||
|
||||
describe('configuration', function () {
|
||||
let server
|
||||
afterEach(async function () {
|
||||
if (server) {
|
||||
server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('should allow to enable prometheus metrics', async function () {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
metrics: { prometheus: { enabled: true, endpointEnabled: true } },
|
||||
},
|
||||
})
|
||||
await server.start()
|
||||
|
||||
const { statusCode } = await got(`${server.baseUrl}metrics`)
|
||||
|
||||
expect(statusCode).to.be.equal(200)
|
||||
})
|
||||
|
||||
it('should allow to disable prometheus metrics', async function () {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
metrics: { prometheus: { enabled: true, endpointEnabled: false } },
|
||||
},
|
||||
})
|
||||
await server.start()
|
||||
|
||||
const { statusCode } = await got(`${server.baseUrl}metrics`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
|
||||
expect(statusCode).to.be.equal(404)
|
||||
})
|
||||
it('should produce colorscheme badges', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
describe('configuration validation', function () {
|
||||
describe('influx', function () {
|
||||
let customConfig
|
||||
beforeEach(function () {
|
||||
customConfig = config.util.toObject()
|
||||
customConfig.public.metrics.influx = {
|
||||
enabled: true,
|
||||
url: 'http://localhost:8081/telegraf',
|
||||
timeoutMilliseconds: 1000,
|
||||
intervalSeconds: 2,
|
||||
instanceIdFrom: 'random',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
hostnameAliases: { 'metrics-hostname': 'metrics-hostname-alias' },
|
||||
envLabel: 'test-env',
|
||||
}
|
||||
customConfig.private = {
|
||||
influx_username: 'telegraf',
|
||||
influx_password: 'telegrafpass',
|
||||
}
|
||||
})
|
||||
it('should redirect colorscheme PNG badges as configured', async function() {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/:fruit-apple-green.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not require influx configuration', function () {
|
||||
delete customConfig.public.metrics.influx
|
||||
expect(() => new Server(config.util.toObject())).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require url when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.url
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.url" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not require url when influx configuration is disabled', function () {
|
||||
customConfig.public.metrics.influx.enabled = false
|
||||
delete customConfig.public.metrics.influx.url
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require timeoutMilliseconds when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.timeoutMilliseconds
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.timeoutMilliseconds" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require intervalSeconds when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.intervalSeconds
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.intervalSeconds" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require instanceIdFrom when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.instanceIdFrom
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.instanceIdFrom" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require instanceIdEnvVarName when instanceIdFrom is env-var', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
|
||||
delete customConfig.public.metrics.influx.instanceIdEnvVarName
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.instanceIdEnvVarName" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow instanceIdFrom = hostname', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'hostname'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should allow instanceIdFrom = env-var', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should allow instanceIdFrom = random', function () {
|
||||
customConfig.public.metrics.influx.instanceIdFrom = 'random'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require envLabel when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.envLabel
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.envLabel" is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not require hostnameAliases', function () {
|
||||
delete customConfig.public.metrics.influx.hostnameAliases
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should allow empty hostnameAliases', function () {
|
||||
customConfig.public.metrics.influx.hostnameAliases = {}
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
|
||||
it('should require username when influx configuration is enabled', function () {
|
||||
delete customConfig.private.influx_username
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'Private configuration is invalid. Check these paths: influx_username'
|
||||
)
|
||||
})
|
||||
|
||||
it('should require password when influx configuration is enabled', function () {
|
||||
delete customConfig.private.influx_password
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'Private configuration is invalid. Check these paths: influx_password'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow other private keys', function () {
|
||||
customConfig.private.gh_token = 'my-token'
|
||||
expect(() => new Server(customConfig)).to.not.throw()
|
||||
})
|
||||
it('should redirect modern PNG badges as configured', async function() {
|
||||
const { statusCode, headers } = await got(`${baseUrl}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce json badges', async function() {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}npm/v/express.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
})
|
||||
|
||||
it('should preserve label case', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fRuiT')
|
||||
})
|
||||
|
||||
// https://github.com/badges/shields/pull/1319
|
||||
it('should not crash with a numeric logo', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?logo=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should not crash with a numeric link', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=1`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should not crash with a boolean link', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=true`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('fruit')
|
||||
.and.to.include('apple')
|
||||
})
|
||||
|
||||
it('should return the 404 badge for unknown badges', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/not/a/badge.svg`,
|
||||
{ throwHttpErrors: false }
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should return the 404 badge page for rando links', async function() {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
|
||||
{ throwHttpErrors: false }
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('404')
|
||||
.and.to.include('badge not found')
|
||||
})
|
||||
|
||||
it('should redirect the root as configured', async function() {
|
||||
const { statusCode, headers } = await got(baseUrl, {
|
||||
followRedirect: false,
|
||||
})
|
||||
|
||||
expect(statusCode).to.equal(302)
|
||||
// This value is set in `config/test.yml`
|
||||
expect(headers.location).to.equal('http://frontend.example.test')
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
})
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
.to.satisfy(isSvg)
|
||||
.and.to.include('410')
|
||||
.and.to.include('jpg no longer available')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,17 +73,11 @@ if (process.env.TESTED_SERVER_URL) {
|
||||
} else {
|
||||
const port = 1111
|
||||
baseUrl = 'http://localhost:1111'
|
||||
before('Start running the server', async function () {
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
bind: {
|
||||
port,
|
||||
},
|
||||
},
|
||||
})
|
||||
before('Start running the server', function() {
|
||||
server = createTestServer({ port })
|
||||
server.start()
|
||||
})
|
||||
after('Shut down the server', async function () {
|
||||
after('Shut down the server', async function() {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
@@ -131,3 +125,5 @@ if (typeof onlyServices === 'undefined' || onlyServices.includes('*****')) {
|
||||
}
|
||||
|
||||
runner.toss()
|
||||
// Invoke run() asynchronously, because Mocha will not start otherwise.
|
||||
process.nextTick(run)
|
||||
|
||||
@@ -22,14 +22,6 @@ const factory = superclass =>
|
||||
this.intercepted = false
|
||||
}
|
||||
|
||||
get(uri, options = { followRedirect: false }) {
|
||||
if (!options.followRedirect) {
|
||||
options.followRedirect = false
|
||||
}
|
||||
super.get(uri, options)
|
||||
return this
|
||||
}
|
||||
|
||||
intercept(setup) {
|
||||
super.intercept(setup)
|
||||
this.intercepted = true
|
||||
@@ -59,10 +51,6 @@ const factory = superclass =>
|
||||
})
|
||||
}
|
||||
|
||||
expectRedirect(location) {
|
||||
return this.expectStatus(301).expectHeader('Location', location)
|
||||
}
|
||||
|
||||
static _expectField(json, name, expected) {
|
||||
if (typeof expected === 'undefined') return
|
||||
if (typeof expected === 'string' || typeof expected === 'number') {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -25,11 +25,9 @@ async function getTitle(owner, repo, pullRequest) {
|
||||
} = await got(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullRequest}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'badges/shields',
|
||||
Authorization: `token ${process.env.GITHUB_TOKEN}`,
|
||||
},
|
||||
responseType: 'json',
|
||||
headers: { 'User-Agent': 'badges/shields' },
|
||||
query: { access_token: process.env.GITHUB_TOKEN },
|
||||
json: true,
|
||||
}
|
||||
)
|
||||
return title
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { test, given } = require('sazerac')
|
||||
const servicesForTitle = require('./services-for-title')
|
||||
|
||||
describe('Services from PR title', function () {
|
||||
describe('Services from PR title', function() {
|
||||
test(servicesForTitle, () => {
|
||||
given('[Travis] Fix timeout issues').expect(['travis'])
|
||||
given('[Travis Sonar] Support user token authentication').expect([
|
||||
|
||||
@@ -6,20 +6,20 @@ const readFile = require('fs-readfile-promise')
|
||||
const { expect } = require('chai')
|
||||
const FsTokenPersistence = require('./fs-token-persistence')
|
||||
|
||||
describe('File system token persistence', function () {
|
||||
describe('File system token persistence', function() {
|
||||
let path, persistence
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
path = tmp.tmpNameSync()
|
||||
persistence = new FsTokenPersistence({ path })
|
||||
})
|
||||
|
||||
context('when the file does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
context('when the file does not exist', function() {
|
||||
it('does nothing', async function() {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
|
||||
it('saving creates an empty file', async function () {
|
||||
it('saving creates an empty file', async function() {
|
||||
await persistence.initialize()
|
||||
|
||||
await persistence.save()
|
||||
@@ -29,20 +29,20 @@ describe('File system token persistence', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when the file exists', function () {
|
||||
context('when the file exists', function() {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function () {
|
||||
beforeEach(async function() {
|
||||
fs.writeFileSync(path, JSON.stringify(initialTokens))
|
||||
})
|
||||
|
||||
it('loads the contents', async function () {
|
||||
it('loads the contents', async function() {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
context('when tokens are added', function() {
|
||||
it('saves the change', async function() {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = Array.from(initialTokens)
|
||||
expected.push(newToken)
|
||||
@@ -55,8 +55,8 @@ describe('File system token persistence', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
context('when tokens are removed', function() {
|
||||
it('saves the change', async function() {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ const Redis = require('ioredis')
|
||||
const { expect } = require('chai')
|
||||
const RedisTokenPersistence = require('./redis-token-persistence')
|
||||
|
||||
describe('Redis token persistence', function () {
|
||||
describe('Redis token persistence', function() {
|
||||
let server
|
||||
// In CI, expect redis already to be running.
|
||||
if (!process.env.CI) {
|
||||
beforeEach(async function () {
|
||||
beforeEach(async function() {
|
||||
server = new RedisServer({ config: { host: 'localhost' } })
|
||||
await server.open()
|
||||
})
|
||||
@@ -18,11 +18,11 @@ describe('Redis token persistence', function () {
|
||||
const key = 'tokenPersistenceIntegrationTest'
|
||||
|
||||
let redis
|
||||
beforeEach(async function () {
|
||||
beforeEach(async function() {
|
||||
redis = new Redis()
|
||||
await redis.del(key)
|
||||
})
|
||||
afterEach(async function () {
|
||||
afterEach(async function() {
|
||||
if (redis) {
|
||||
await redis.quit()
|
||||
redis = undefined
|
||||
@@ -30,44 +30,44 @@ describe('Redis token persistence', function () {
|
||||
})
|
||||
|
||||
if (!process.env.CI) {
|
||||
afterEach(async function () {
|
||||
afterEach(async function() {
|
||||
await server.close()
|
||||
server = undefined
|
||||
})
|
||||
}
|
||||
|
||||
let persistence
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
persistence = new RedisTokenPersistence({ key })
|
||||
})
|
||||
afterEach(async function () {
|
||||
afterEach(async function() {
|
||||
if (persistence) {
|
||||
await persistence.stop()
|
||||
persistence = undefined
|
||||
}
|
||||
})
|
||||
|
||||
context('when the key does not exist', function () {
|
||||
it('does nothing', async function () {
|
||||
context('when the key does not exist', function() {
|
||||
it('does nothing', async function() {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens).to.deep.equal([])
|
||||
})
|
||||
})
|
||||
|
||||
context('when the key exists', function () {
|
||||
context('when the key exists', function() {
|
||||
const initialTokens = ['a', 'b', 'c'].map(char => char.repeat(40))
|
||||
|
||||
beforeEach(async function () {
|
||||
beforeEach(async function() {
|
||||
await redis.sadd(key, initialTokens)
|
||||
})
|
||||
|
||||
it('loads the contents', async function () {
|
||||
it('loads the contents', async function() {
|
||||
const tokens = await persistence.initialize()
|
||||
expect(tokens.sort()).to.deep.equal(initialTokens)
|
||||
})
|
||||
|
||||
context('when tokens are added', function () {
|
||||
it('saves the change', async function () {
|
||||
context('when tokens are added', function() {
|
||||
it('saves the change', async function() {
|
||||
const newToken = 'e'.repeat(40)
|
||||
const expected = initialTokens.slice()
|
||||
expected.push(newToken)
|
||||
@@ -80,8 +80,8 @@ describe('Redis token persistence', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when tokens are removed', function () {
|
||||
it('saves the change', async function () {
|
||||
context('when tokens are removed', function() {
|
||||
it('saves the change', async function() {
|
||||
const expected = Array.from(initialTokens)
|
||||
const toRemove = expected.pop()
|
||||
|
||||
|
||||
@@ -13,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() {
|
||||
@@ -52,23 +55,18 @@ class Token {
|
||||
get id() {
|
||||
return this._id
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data
|
||||
}
|
||||
|
||||
get usesRemaining() {
|
||||
return this._usesRemaining
|
||||
}
|
||||
|
||||
get nextReset() {
|
||||
return this._nextReset
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return this._isValid
|
||||
}
|
||||
|
||||
get isFrozen() {
|
||||
return this._isFrozen
|
||||
}
|
||||
|
||||
@@ -11,27 +11,27 @@ function expectPoolToBeExhausted(pool) {
|
||||
}).to.throw(Error, /^Token pool is exhausted$/)
|
||||
}
|
||||
|
||||
describe('The token pool', function () {
|
||||
describe('The token pool', function() {
|
||||
const ids = ['1', '2', '3', '4', '5']
|
||||
const batchSize = 3
|
||||
|
||||
let tokenPool
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
tokenPool = new TokenPool({ batchSize })
|
||||
ids.forEach(id => tokenPool.add(id))
|
||||
})
|
||||
|
||||
it('allValidTokenIds() should return the full list', function () {
|
||||
it('allValidTokenIds() should return the full list', function() {
|
||||
expect(tokenPool.allValidTokenIds()).to.deep.equal(ids)
|
||||
})
|
||||
|
||||
it('should yield the expected tokens', function () {
|
||||
it('should yield the expected tokens', function() {
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
)
|
||||
})
|
||||
|
||||
it('should repeat when reaching the end', function () {
|
||||
it('should repeat when reaching the end', function() {
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
)
|
||||
@@ -40,17 +40,17 @@ describe('The token pool', function () {
|
||||
)
|
||||
})
|
||||
|
||||
describe('serializeDebugInfo should initially return the expected', function () {
|
||||
beforeEach(function () {
|
||||
describe('serializeDebugInfo should initially return the expected', function() {
|
||||
beforeEach(function() {
|
||||
sinon.useFakeTimers({ now: 1544307744484 })
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
afterEach(function() {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
context('sanitize is not specified', function () {
|
||||
it('returns fully sanitized results', function () {
|
||||
context('sanitize is not specified', function() {
|
||||
it('returns fully sanitized results', function() {
|
||||
// This is `sha()` of '1', '2', '3', '4', '5'. These are written
|
||||
// literally for avoidance of doubt as to whether sanitization is
|
||||
// happening.
|
||||
@@ -79,8 +79,8 @@ describe('The token pool', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('with sanitize: false', function () {
|
||||
it('returns unsanitized results', function () {
|
||||
context('with sanitize: false', function() {
|
||||
it('returns unsanitized results', function() {
|
||||
expect(tokenPool.serializeDebugInfo({ sanitize: false })).to.deep.equal(
|
||||
{
|
||||
allValidTokenIds: ids,
|
||||
@@ -101,8 +101,8 @@ describe('The token pool', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens are marked exhausted immediately', function () {
|
||||
it('should be exhausted', function () {
|
||||
context('tokens are marked exhausted immediately', function() {
|
||||
it('should be exhausted', function() {
|
||||
ids.forEach(() => {
|
||||
const token = tokenPool.next()
|
||||
token.update(0, Token.nextResetNever)
|
||||
@@ -112,8 +112,8 @@ describe('The token pool', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens are marked after the last request', function () {
|
||||
it('should be exhausted', function () {
|
||||
context('tokens are marked after the last request', function() {
|
||||
it('should be exhausted', function() {
|
||||
ids.forEach(() => {
|
||||
const token = times(batchSize, () => tokenPool.next()).pop()
|
||||
token.update(0, Token.nextResetNever)
|
||||
@@ -123,8 +123,8 @@ describe('The token pool', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens are renewed', function () {
|
||||
it('should keep using them', function () {
|
||||
context('tokens are renewed', function() {
|
||||
it('should keep using them', function() {
|
||||
const tokensToRenew = ['2', '4']
|
||||
const renewalCount = 3
|
||||
|
||||
@@ -149,16 +149,16 @@ describe('The token pool', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('tokens reset', function () {
|
||||
context('tokens reset', function() {
|
||||
let clock
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
afterEach(function() {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('should start using them', function () {
|
||||
it('should start using them', function() {
|
||||
const tokensToReset = ['2', '4']
|
||||
const futureTime = 1440
|
||||
|
||||
@@ -183,8 +183,8 @@ describe('The token pool', function () {
|
||||
})
|
||||
})
|
||||
|
||||
context('when empty', function () {
|
||||
it('next() should return the expected error', function () {
|
||||
context('when empty', function() {
|
||||
it('next() should return the expected error', function() {
|
||||
const tokenPool = new TokenPool()
|
||||
expect(() => tokenPool.next()).to.throw('Token pool is exhausted')
|
||||
})
|
||||
|
||||
@@ -1,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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -25,7 +25,7 @@ and learn about the [Github workflow](http://try.github.io/).
|
||||
|
||||
#### Node, NPM
|
||||
|
||||
Node 12 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
|
||||
@@ -116,10 +116,17 @@ const { BaseService } = require('..')
|
||||
// (3)
|
||||
module.exports = class Example extends BaseService {
|
||||
// (4)
|
||||
static category = 'build'
|
||||
static get category() {
|
||||
return 'build'
|
||||
}
|
||||
|
||||
// (5)
|
||||
static route = { base: 'example', pattern: ':text' }
|
||||
static get route() {
|
||||
return {
|
||||
base: 'example',
|
||||
pattern: ':text',
|
||||
}
|
||||
}
|
||||
|
||||
// (6)
|
||||
async handle({ text }) {
|
||||
@@ -144,7 +151,6 @@ Description of the code:
|
||||
number of named parameters. These are converted into
|
||||
regular expressions by [`path-to-regexp`][path-to-regexp].
|
||||
Because a service instance won't be created until it's time to handle a request, the route and other metadata must be obtained by examining the classes themselves. [That's why they're marked `static`.][static]
|
||||
- There is additional documentation on conventions for [designing badge URLs](./badge-urls.md)
|
||||
6. All badges must implement the `async handle()` function that receives parameters to render the badge. Parameters of `handle()` will match the name defined in `route()` Because we're capturing a single variable called `text` our function signature is `async handle({ text })`. `async` is needed to let JavaScript do other things while we are waiting for result from external API. Although in this simple case, we don't make any external calls. Our `handle()` function should return an object with 3 properties:
|
||||
- `label`: the text on the left side of the badge
|
||||
- `message`: the text on the right side of the badge - here we are passing through the parameter we captured in the route regex
|
||||
@@ -187,13 +193,22 @@ const schema = Joi.object({
|
||||
// (5)
|
||||
module.exports = class GemVersion extends BaseJsonService {
|
||||
// (6)
|
||||
static category = 'version'
|
||||
static get category() {
|
||||
return 'version'
|
||||
}
|
||||
|
||||
// (7)
|
||||
static route = { base: 'gem/v', pattern: ':gem' }
|
||||
static get route() {
|
||||
return {
|
||||
base: 'gem/v',
|
||||
pattern: ':gem',
|
||||
}
|
||||
}
|
||||
|
||||
// (8)
|
||||
static defaultBadgeData = { label: 'gem' }
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'gem' }
|
||||
}
|
||||
|
||||
// (11)
|
||||
static render({ version }) {
|
||||
@@ -282,19 +297,23 @@ Once we have implemented our badge, we can add it to the index so that users can
|
||||
module.exports = class GemVersion extends BaseJsonService {
|
||||
// ...
|
||||
|
||||
// (1)
|
||||
static category = 'version'
|
||||
static get category() {
|
||||
// (1)
|
||||
return 'version'
|
||||
}
|
||||
|
||||
// (2)
|
||||
static examples = [
|
||||
{
|
||||
// (3)
|
||||
title: 'Gem',
|
||||
namedParams: { gem: 'formatador' },
|
||||
staticPreview: this.render({ version: '2.1.0' }),
|
||||
keywords: ['ruby'],
|
||||
},
|
||||
]
|
||||
static get examples() {
|
||||
// (2)
|
||||
return [
|
||||
{
|
||||
// (3)
|
||||
title: 'Gem',
|
||||
namedParams: { gem: 'formatador' },
|
||||
staticPreview: this.render({ version: '2.1.0' }),
|
||||
keywords: ['ruby'],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user