Compare commits
137 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d71c1c2bf | ||
|
|
7e15b9fd10 | ||
|
|
b428490db4 | ||
|
|
57c2ba0d68 | ||
|
|
1bfda7a54b | ||
|
|
631ca1ccb6 | ||
|
|
2e2959a12c | ||
|
|
fcb83cacec | ||
|
|
398565d780 | ||
|
|
c2fc381449 | ||
|
|
c7e842cd3a | ||
|
|
66225a62f6 | ||
|
|
943ee3ca23 | ||
|
|
ff98bb6365 | ||
|
|
5e585c364e | ||
|
|
e7294845c7 | ||
|
|
c7efb27086 | ||
|
|
d882433fd9 | ||
|
|
436f3a1a3f | ||
|
|
ef2649bd4b | ||
|
|
d5c1bdc8ec | ||
|
|
3dfef0011c | ||
|
|
df436b17a6 | ||
|
|
8880ec5171 | ||
|
|
ad873fb8db | ||
|
|
3c4f913f75 | ||
|
|
f0f2dd74d6 | ||
|
|
cbcad6dd4e | ||
|
|
c8ec665493 | ||
|
|
f84de2788a | ||
|
|
398288b86c | ||
|
|
af7b7f3597 | ||
|
|
528165f65c | ||
|
|
f84ccb24ae | ||
|
|
667dd43cf4 | ||
|
|
9a698a85a9 | ||
|
|
7d2bb7f51d | ||
|
|
4f5c40d12a | ||
|
|
819500354c | ||
|
|
a1b613fbd4 | ||
|
|
27644c9862 | ||
|
|
b0e8fe0084 | ||
|
|
f0e5ad2d3c | ||
|
|
d00659e42b | ||
|
|
5e963a48f3 | ||
|
|
40f2b9e1a7 | ||
|
|
13fd752ada | ||
|
|
e7fc6d56d3 | ||
|
|
83c22ccd55 | ||
|
|
ffcce04deb | ||
|
|
b9d96755ec | ||
|
|
fb794ec479 | ||
|
|
5ff782219b | ||
|
|
b28b4d6414 | ||
|
|
877cf628a6 | ||
|
|
f2cafb8464 | ||
|
|
2cad636f6f | ||
|
|
749d05cf9d | ||
|
|
71cd657e3c | ||
|
|
e02c8aa884 | ||
|
|
1251adaec2 | ||
|
|
1543d0f363 | ||
|
|
e7197f6db4 | ||
|
|
9fafb3110c | ||
|
|
7bad3f5902 | ||
|
|
1afa3b7871 | ||
|
|
c37dd08349 | ||
|
|
bf0395dc00 | ||
|
|
547e528ba7 | ||
|
|
037a77ec23 | ||
|
|
dbb993e270 | ||
|
|
dc36aeffdb | ||
|
|
ef8e9cc31f | ||
|
|
2cad078b66 | ||
|
|
eb7d3bcad4 | ||
|
|
7aeb481eed | ||
|
|
80e27a7a04 | ||
|
|
04a6344efe | ||
|
|
640cd5643e | ||
|
|
53cce8f1f1 | ||
|
|
c47f93219b | ||
|
|
53efe52801 | ||
|
|
cdf6317f14 | ||
|
|
3d04209405 | ||
|
|
c2b379f1f5 | ||
|
|
75eb794945 | ||
|
|
a5b816760b | ||
|
|
c0d6fa699d | ||
|
|
4a03b86300 | ||
|
|
9137b46bc3 | ||
|
|
1e85cf7926 | ||
|
|
5085ef0209 | ||
|
|
86f0a86094 | ||
|
|
6f78e6f81d | ||
|
|
a2ca06e033 | ||
|
|
08e6933f0e | ||
|
|
e7737ce050 | ||
|
|
f24b00fecd | ||
|
|
1c64ec6528 | ||
|
|
30377f6f33 | ||
|
|
4dc6f51650 | ||
|
|
2b881f330d | ||
|
|
c6f157b7ef | ||
|
|
86de4190e4 | ||
|
|
dad1e43532 | ||
|
|
3fe013eec3 | ||
|
|
a5f91a40e5 | ||
|
|
50ea7068a8 | ||
|
|
67d935492d | ||
|
|
35dfd75ef5 | ||
|
|
14892e3943 | ||
|
|
2651e5fe87 | ||
|
|
554c01097d | ||
|
|
42603e139f | ||
|
|
72bdc64f8a | ||
|
|
6337206a36 | ||
|
|
ec307324a1 | ||
|
|
7fbe7bb286 | ||
|
|
7e7758c0ff | ||
|
|
6593927f23 | ||
|
|
8cb9adc23d | ||
|
|
00aef7e63a | ||
|
|
3b7495a985 | ||
|
|
bf1bea8b4a | ||
|
|
150334b141 | ||
|
|
07b657497a | ||
|
|
e3b1d3c8d9 | ||
|
|
164b5c31a0 | ||
|
|
a7902eaeb3 | ||
|
|
c422d01085 | ||
|
|
909b45e80e | ||
|
|
96520ab33f | ||
|
|
d7531353aa | ||
|
|
e8233fbfaf | ||
|
|
1a108ac03a | ||
|
|
cd036ab50f | ||
|
|
c1a9ddde2b |
@@ -2,7 +2,6 @@ extends:
|
||||
- standard
|
||||
- standard-jsx
|
||||
- standard-react
|
||||
- plugin:@typescript-eslint/recommended
|
||||
- prettier
|
||||
- eslint:recommended
|
||||
|
||||
@@ -18,7 +17,7 @@ settings:
|
||||
react:
|
||||
version: '16.8'
|
||||
jsdoc:
|
||||
mode: jsdoc
|
||||
mode: typescript
|
||||
|
||||
plugins:
|
||||
- chai-friendly
|
||||
@@ -37,39 +36,33 @@ overrides:
|
||||
# rules listed here are only ones which conflict.
|
||||
|
||||
- files:
|
||||
- '**/*.js'
|
||||
- '!frontend/**/*.js'
|
||||
- 'badge-maker/**/*.js'
|
||||
- '**/*.cjs'
|
||||
env:
|
||||
node: true
|
||||
es6: true
|
||||
|
||||
- files:
|
||||
- '**/*.js'
|
||||
- '!frontend/**/*.js'
|
||||
- '!badge-maker/**/*.js'
|
||||
env:
|
||||
node: true
|
||||
es6: true
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
rules:
|
||||
no-console: 'off'
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off'
|
||||
|
||||
- files:
|
||||
- '**/*.@(ts|tsx)'
|
||||
- '**/*.ts'
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
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/no-object-literal-type-assertion': 'off'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'@typescript-eslint/ban-ts-ignore': 'off'
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off'
|
||||
|
||||
- files:
|
||||
- core/**/*.ts
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
parser: '@typescript-eslint/parser'
|
||||
- files:
|
||||
- gatsby-browser.js
|
||||
- 'frontend/**/*.@(js|ts|tsx)'
|
||||
- 'frontend/**/*.js'
|
||||
parserOptions:
|
||||
sourceType: 'module'
|
||||
env:
|
||||
@@ -128,14 +121,6 @@ rules:
|
||||
# Disable some rules from eslint:recommended.
|
||||
no-empty: ['error', { 'allowEmptyCatch': true }]
|
||||
|
||||
# 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'
|
||||
|
||||
'@typescript-eslint/no-var-requires': 'off'
|
||||
|
||||
'@typescript-eslint/no-use-before-define': 'error'
|
||||
no-use-before-define: 'off'
|
||||
|
||||
# These should be disabled by eslint-config-prettier, but are not.
|
||||
@@ -197,11 +182,7 @@ rules:
|
||||
jsdoc/require-returns-type: 'error'
|
||||
jsdoc/valid-types: 'error'
|
||||
|
||||
# Disable some from TypeScript.
|
||||
'@typescript-eslint/camelcase': off
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
'@typescript-eslint/no-empty-function': 'off'
|
||||
|
||||
react/prop-types: 'off'
|
||||
react/jsx-sort-props: 'error'
|
||||
react-hooks/rules-of-hooks: 'error'
|
||||
react-hooks/exhaustive-deps: 'error'
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
4
.github/ISSUE_TEMPLATE/3_Badge_request.yml
vendored
@@ -12,7 +12,7 @@ body:
|
||||
**fetch and display data from an upstream service**.
|
||||
If your suggestion is for a static badge
|
||||
(which shows the same information every time it is requested), it is
|
||||
[already possible to make these](https://github.com/badges/shields/blob/master/doc/static-badges.md).
|
||||
[already possible to make these](https://shields.io/docs/static-badges).
|
||||
We don't add specific routes for badges which only show static information.
|
||||
|
||||
- type: textarea
|
||||
@@ -25,7 +25,7 @@ body:
|
||||
- Which service is this badge for e.g: GitHub, Travis CI
|
||||
- What sort of information should this badge show?
|
||||
Provide an example in plain text e.g: "version | v1.01" or as a static badge
|
||||
(static badge generator can be found at https://shields.io/#your-badge )
|
||||
(static badge generator can be found at https://shields.io/badges/static-badge )
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
10
.github/actions/close-bot/helpers.js
vendored
10
.github/actions/close-bot/helpers.js
vendored
@@ -28,14 +28,13 @@ function allChangelogLinesAreVersionBump(changelogLines) {
|
||||
changelogLines.length > 0 &&
|
||||
changelogLines.length ===
|
||||
changelogLines.filter(line =>
|
||||
line.includes('Version bump only for package')
|
||||
line.includes('Version bump only for package'),
|
||||
).length
|
||||
)
|
||||
}
|
||||
|
||||
function isPointlessVersionBump(body) {
|
||||
const pointlessBumpLinks = [
|
||||
'https://github.com/gatsbyjs/gatsby',
|
||||
'https://github.com/typescript-eslint/typescript-eslint',
|
||||
]
|
||||
|
||||
@@ -53,14 +52,15 @@ function isPointlessVersionBump(body) {
|
||||
.filter(line => !line.startsWith('<h'))
|
||||
.filter(line => !line.startsWith('<p>All notable changes'))
|
||||
.filter(
|
||||
line => !line.startsWith('See <a href="https://conventionalcommits.org">')
|
||||
line =>
|
||||
!line.startsWith('See <a href="https://conventionalcommits.org">'),
|
||||
)
|
||||
.filter(line => !line.startsWith('<!--'))
|
||||
.filter(
|
||||
line =>
|
||||
!line.startsWith(
|
||||
'<p><a href="https://www.gatsbyjs.com/docs/reference/release-notes/'
|
||||
)
|
||||
'<p><a href="https://www.gatsbyjs.com/docs/reference/release-notes/',
|
||||
),
|
||||
)
|
||||
return allChangelogLinesAreVersionBump(changelogLines)
|
||||
}
|
||||
|
||||
2
.github/actions/draft-release/Dockerfile
vendored
2
.github/actions/draft-release/Dockerfile
vendored
@@ -1,4 +1,4 @@
|
||||
FROM node:12-buster
|
||||
FROM node:18-bullseye
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y jq
|
||||
|
||||
9
.github/actions/draft-release/entrypoint.sh
vendored
9
.github/actions/draft-release/entrypoint.sh
vendored
@@ -2,14 +2,17 @@
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Set up a git user
|
||||
git config user.name "release[bot]"
|
||||
git config user.email "actions@users.noreply.github.com"
|
||||
# mark workspace dir as 'safe'
|
||||
git config --system --add safe.directory '/github/workspace'
|
||||
|
||||
# Find last server-YYYY-MM-DD tag
|
||||
git fetch --unshallow --tags
|
||||
LAST_TAG=$(git tag | grep server | tail -n 1)
|
||||
|
||||
# Set up a git user
|
||||
git config user.name "release[bot]"
|
||||
git config user.email "actions@users.noreply.github.com"
|
||||
|
||||
# Find the marker in CHANGELOG.md
|
||||
INSERT_POINT=$(grep -n "^\-\-\-$" CHANGELOG.md | cut -f1 -d:)
|
||||
INSERT_POINT=$((INSERT_POINT+1))
|
||||
|
||||
31
.github/actions/frontend-tests/action.yml
vendored
31
.github/actions/frontend-tests/action.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: 'Frontend tests'
|
||||
description: 'Run frontend tests and check types'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Prepare frontend tests
|
||||
if: always()
|
||||
run: npm run defs && npm run features
|
||||
shell: bash
|
||||
|
||||
- name: Tests
|
||||
if: always()
|
||||
run: npm run test:frontend -- --reporter json --reporter-option 'output=reports/frontend-tests.json'
|
||||
shell: bash
|
||||
|
||||
- name: Type Checks
|
||||
if: always()
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm run check-types:frontend 2>&1 | tee reports/frontend-types.txt
|
||||
shell: bash
|
||||
|
||||
- name: Write Markdown Summary
|
||||
if: always()
|
||||
run: |
|
||||
node scripts/mocha2md.js 'Frontend Tests' reports/frontend-tests.json >> $GITHUB_STEP_SUMMARY
|
||||
echo '# Frontend Types' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat reports/frontend-types.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
shell: bash
|
||||
4
.github/actions/setup/action.yml
vendored
4
.github/actions/setup/action.yml
vendored
@@ -19,6 +19,10 @@ runs:
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install NPM 9
|
||||
run: npm install -g npm@^9.0.0
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.cypress == 'false' }}
|
||||
env:
|
||||
|
||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -8,6 +8,7 @@ updates:
|
||||
day: friday
|
||||
time: '12:00'
|
||||
open-pull-requests-limit: 99
|
||||
rebase-strategy: disabled
|
||||
ignore:
|
||||
# https://github.com/badges/shields/issues/7324
|
||||
# https://github.com/badges/shields/issues/7447
|
||||
@@ -27,6 +28,7 @@ updates:
|
||||
day: friday
|
||||
time: '12:00'
|
||||
open-pull-requests-limit: 99
|
||||
rebase-strategy: disabled
|
||||
|
||||
# close-bot package dependencies
|
||||
- package-ecosystem: npm
|
||||
@@ -36,8 +38,12 @@ updates:
|
||||
day: friday
|
||||
time: '12:00'
|
||||
open-pull-requests-limit: 99
|
||||
rebase-strategy: disabled
|
||||
|
||||
# GH actions
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
rebase-strategy: disabled
|
||||
|
||||
5
.github/scripts/deploy-review-app.sh
vendored
5
.github/scripts/deploy-review-app.sh
vendored
@@ -10,12 +10,11 @@ org="shields-io"
|
||||
# This will fail if $PR_NUMBER is not a valid PR
|
||||
pr_json=$(curl --fail "https://api.github.com/repos/badges/shields/pulls/$PR_NUMBER")
|
||||
|
||||
# Attempt to apply the PR diff to the target branch
|
||||
# This will fail if it does not merge cleanly
|
||||
# Checkout the PR branch
|
||||
git config user.name "actions[bot]"
|
||||
git config user.email "actions@users.noreply.github.com"
|
||||
git fetch origin "pull/$PR_NUMBER/head:pr-$PR_NUMBER"
|
||||
git merge "pr-$PR_NUMBER"
|
||||
git checkout "pr-$PR_NUMBER"
|
||||
|
||||
# If the app does not already exist, create it
|
||||
if ! flyctl status --app "$app"; then
|
||||
|
||||
3
.github/workflows/build-docker-image.yml
vendored
3
.github/workflows/build-docker-image.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Build Docker Image
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'gh-readonly-queue/**'
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
|
||||
67
.github/workflows/test-bug-run-badge.yml
vendored
Normal file
67
.github/workflows/test-bug-run-badge.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Test new bug report badge
|
||||
run-name: Test bug report on issue ${{ github.event.issue.number }}
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
extract-bug-badge-url:
|
||||
if: ${{ contains(github.event.issue.labels.*.name, 'question') }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
runBadgeTest: ${{ steps.testCondition.outputs.runNext }}
|
||||
link: ${{ steps.testCondition.outputs.link }}
|
||||
steps:
|
||||
- name: Test badge test run conditions
|
||||
id: testCondition
|
||||
run: |
|
||||
product=$(echo "${{ github.event.issue.body }}" | grep -A2 "Are you experiencing an issue with.*" | tail -n 1)
|
||||
link=$(echo "${{ github.event.issue.body }}" | grep -A2 "Link to the badge.*" | tail -n 1)
|
||||
|
||||
if [[ "$product" == "shields.io" && "$link" == "https://img.shields.io"* ]]; then
|
||||
echo "runNext=true" >> "$GITHUB_OUTPUT"
|
||||
echo "link=$link" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Conditions not met. Skipping the workflow..."
|
||||
echo "runNext=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
run-bug-badge-url-test:
|
||||
needs: extract-bug-badge-url
|
||||
if: needs.extract-bug-badge-url.outputs.runBadgeTest == 'true'
|
||||
permissions:
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
cypress: false
|
||||
|
||||
- name: Output debug info
|
||||
env:
|
||||
TEST_BADGE_LINK: '${{ needs.extract-bug-badge-url.outputs.link }}'
|
||||
run: npm run badge $TEST_BADGE_LINK
|
||||
|
||||
- name: Add Comment to Issue
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const runId = context.runId;
|
||||
const jobUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`;
|
||||
const issueComment = `
|
||||
Badge tested using \`npm run badge ${{ needs.extract-bug-badge-url.outputs.link }}\`
|
||||
Output is available [here](${jobUrl})
|
||||
`;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: issueNumber,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
body: issueComment
|
||||
});
|
||||
5
.github/workflows/test-e2e.yml
vendored
5
.github/workflows/test-e2e.yml
vendored
@@ -29,13 +29,10 @@ jobs:
|
||||
node-version: 16
|
||||
cypress: true
|
||||
|
||||
- name: Frontend build
|
||||
run: GATSBY_BASE_URL=http://localhost:8080 npm run build
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: npm run e2e-on-build
|
||||
run: npm run e2e
|
||||
|
||||
- name: Archive videos
|
||||
if: always()
|
||||
|
||||
26
.github/workflows/test-frontend.yml
vendored
26
.github/workflows/test-frontend.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Frontend
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'gh-pages'
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Frontend tests
|
||||
uses: ./.github/actions/frontend-tests
|
||||
|
||||
- name: Frontend build
|
||||
run: npm run build
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Integration@node 17
|
||||
name: Integration@node 18
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-integration-17:
|
||||
test-integration-18:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
node-version: 18
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Main@node 17
|
||||
name: Main@node 18
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- 'dependabot/**'
|
||||
|
||||
jobs:
|
||||
test-main-17:
|
||||
test-main-18:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
node-version: 18
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
8
.github/workflows/test-package-cli.yml
vendored
8
.github/workflows/test-package-cli.yml
vendored
@@ -16,12 +16,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node: '14'
|
||||
engine-strict: 'false'
|
||||
- node: '16'
|
||||
engine-strict: 'false'
|
||||
- node: '18'
|
||||
engine-strict: 'true'
|
||||
- node: '20'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -32,9 +29,6 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
NPM_CONFIG_ENGINE_STRICT: ${{ matrix.engine-strict }}
|
||||
run: |
|
||||
cd badge-maker
|
||||
npm install
|
||||
|
||||
4
.github/workflows/test-package-lib.yml
vendored
4
.github/workflows/test-package-lib.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- node: '14'
|
||||
engine-strict: 'false'
|
||||
- node: '16'
|
||||
engine-strict: 'true'
|
||||
- node: '18'
|
||||
engine-strict: 'false'
|
||||
- node: '20'
|
||||
engine-strict: 'false'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
name: Services@node 17
|
||||
name: Services@node 18
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches:
|
||||
- 'gh-readonly-queue/**'
|
||||
|
||||
jobs:
|
||||
test-services-17:
|
||||
test-services-18:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -14,7 +17,7 @@ jobs:
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: 17
|
||||
node-version: 18
|
||||
env:
|
||||
NPM_CONFIG_ENGINE_STRICT: 'false'
|
||||
|
||||
3
.github/workflows/test-services.yml
vendored
3
.github/workflows/test-services.yml
vendored
@@ -2,6 +2,9 @@ name: Services
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
push:
|
||||
branches:
|
||||
- 'gh-readonly-queue/**'
|
||||
|
||||
jobs:
|
||||
test-services:
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -92,10 +92,6 @@ typings/
|
||||
|
||||
# Temporary build artifacts.
|
||||
/build
|
||||
.next
|
||||
badge-examples.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/categories/*.yaml
|
||||
|
||||
# Local runtime configuration.
|
||||
@@ -104,11 +100,6 @@ frontend/categories/*.yaml
|
||||
# Template for the local runtime configuration.
|
||||
!/config/local*.template.yml
|
||||
|
||||
# Gatsby
|
||||
/frontend/.cache
|
||||
/frontend/public
|
||||
/public
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@@ -121,3 +112,8 @@ flamegraph.html
|
||||
|
||||
# config file for node-pg-migrate
|
||||
migrations-config.json
|
||||
|
||||
# Frontend/Docusaurus
|
||||
frontend/.docusaurus
|
||||
frontend/.cache-loader
|
||||
/public
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
reporter: mocha-env-reporter
|
||||
require:
|
||||
- '@babel/polyfill'
|
||||
- '@babel/register'
|
||||
- mocha-yaml-loader
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"reporter": ["lcov"],
|
||||
"all": false,
|
||||
"silent": true,
|
||||
"clean": false,
|
||||
"sourceMap": false,
|
||||
"instrument": false,
|
||||
"include": ["frontend/**/*.js"],
|
||||
"exclude": ["**/*.spec.js", "**/mocha-*.js"]
|
||||
}
|
||||
@@ -10,5 +10,5 @@ public
|
||||
private/*.json
|
||||
/.nyc_output
|
||||
analytics.json
|
||||
supported-features.json
|
||||
service-definitions.yml
|
||||
frontend/.docusaurus
|
||||
frontend/categories
|
||||
|
||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -4,6 +4,51 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2023-08-01
|
||||
|
||||
- Convert `examples` arrays to `openApi` objects (part 1) [#9320](https://github.com/badges/shields/issues/9320)
|
||||
- Migrate from docs.rs' builds API to status API [#9422](https://github.com/badges/shields/issues/9422)
|
||||
- [OpenVSX] Fix OpenVSX API call for unversioned package URLs [#9408](https://github.com/badges/shields/issues/9408)
|
||||
- Add support for [Lemmy] [#9368](https://github.com/badges/shields/issues/9368)
|
||||
- upgrade to npm 9 [#9323](https://github.com/badges/shields/issues/9323)
|
||||
- Go back to default YouTube cache [#9372](https://github.com/badges/shields/issues/9372)
|
||||
- Add [GitHubDiscussionsSearch] and GitHubRepoDiscussionsSearch service [#9340](https://github.com/badges/shields/issues/9340)
|
||||
- Allow user to filter github tags and releases [#9193](https://github.com/badges/shields/issues/9193)
|
||||
- don't URL encode slash in [githubactionsworkflow] badge [#9322](https://github.com/badges/shields/issues/9322)
|
||||
- add a bit of border to select boxes [#9348](https://github.com/badges/shields/issues/9348)
|
||||
- deprecate [snyk] badges [#9349](https://github.com/badges/shields/issues/9349)
|
||||
- increase max-age on [docker] badges, again [#9350](https://github.com/badges/shields/issues/9350) [#9369](https://github.com/badges/shields/issues/9369)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-07-02
|
||||
|
||||
By far the most significant change in this release is the long-awaited launch of the re-designed frontend:
|
||||
|
||||
- migrate frontend to docusaurus [#9014](https://github.com/badges/shields/issues/9014)
|
||||
- fix a load of spacing issues in frontend content [#9281](https://github.com/badges/shields/issues/9281)
|
||||
- set a sensible meta description [#9283](https://github.com/badges/shields/issues/9283)
|
||||
- chore(frontend): open homepage feature links in new tab [#9300](https://github.com/badges/shields/issues/9300)
|
||||
- adapt opencollective images to theme background [#9298](https://github.com/badges/shields/issues/9298)
|
||||
- temp fix: wrap code examples tabs in narrow browser windows [#9302](https://github.com/badges/shields/issues/9302)
|
||||
- add a bit of border to text boxes [#9324](https://github.com/badges/shields/issues/9324)
|
||||
|
||||
Other changes in this release:
|
||||
|
||||
- cache [dockerpulls] badges for an hour [#9343](https://github.com/badges/shields/issues/9343)
|
||||
- Mention YouTube API services and link to Google Privacy Policy [#9339](https://github.com/badges/shields/issues/9339)
|
||||
- allow negative timestamps in relative [date] badge [#9321](https://github.com/badges/shields/issues/9321)
|
||||
- upgrade to graphql 16 [#9290](https://github.com/badges/shields/issues/9290)
|
||||
- remove obsolete travis .org examples [#9284](https://github.com/badges/shields/issues/9284)
|
||||
- increase max age on reddit badges [#9282](https://github.com/badges/shields/issues/9282)
|
||||
- feat: Add author filter option for [GithubCommitActivity] [#9251](https://github.com/badges/shields/issues/9251)
|
||||
- Fix: [GithubCommitActivity] invalid branch error handling [#9258](https://github.com/badges/shields/issues/9258)
|
||||
- Implement a pattern for dealing with upstream APIs which are slow on the first hit; affects [endpoint] [#9233](https://github.com/badges/shields/issues/9233)
|
||||
- Delete old deprecated services [#9254](https://github.com/badges/shields/issues/9254)
|
||||
- feat: add 'canceled' status to netlify deploy badge [#9240](https://github.com/badges/shields/issues/9240)
|
||||
- increase default cache on youtube badges [#9238](https://github.com/badges/shields/issues/9238)
|
||||
- embiggen youtube cache, again [#9250](https://github.com/badges/shields/issues/9250)
|
||||
- Dependency updates
|
||||
|
||||
## server-2023-06-01
|
||||
|
||||
- feat: Add total commits to [GitHubCommitActivity] [#9196](https://github.com/badges/shields/issues/9196)
|
||||
|
||||
@@ -9,7 +9,7 @@ COPY package.json package-lock.json /usr/src/app/
|
||||
COPY badge-maker /usr/src/app/badge-maker/
|
||||
|
||||
RUN apk add python3 make g++
|
||||
RUN npm install -g "npm@>=8"
|
||||
RUN npm install -g "npm@^9.0.0"
|
||||
# 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
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ This repo hosts:
|
||||
[Make your own badges!][custom badges]
|
||||
(Quick example: `https://img.shields.io/badge/left-right-f39f37`)
|
||||
|
||||
[custom badges]: https://shields.io/#your-badge
|
||||
[custom badges]: http://localhost:3000/badges/static-badge
|
||||
|
||||
### Quickstart
|
||||
|
||||
@@ -107,7 +107,7 @@ You can read a [tutorial on how to add a badge][tutorial].
|
||||
|
||||
When server source files change, the badge server should automatically restart
|
||||
itself (using [nodemon][]). When the frontend files change, the frontend dev
|
||||
server (`gatsby dev`) should also automatically reload. However the badge
|
||||
server (`docusaurus start`) should also automatically reload. However the badge
|
||||
definitions are built only before the server first starts. To regenerate those,
|
||||
either run `npm run defs` or manually restart the server.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 4.0.0 [WIP]
|
||||
|
||||
- Drop compatibility with Node < 14
|
||||
- Drop compatibility with Node < 16
|
||||
|
||||
## 3.3.1
|
||||
|
||||
@@ -275,7 +275,7 @@ badge.loadFont('/path/to/Verdana.ttf', err => {
|
||||
{ text: ['build', 'passed'], colorscheme: 'green', template: 'flat' },
|
||||
(svg, err) => {
|
||||
// svg is a string containing your badge
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -7,19 +7,19 @@ expectError(
|
||||
makeBadge({
|
||||
message: 'passed',
|
||||
style: 'invalid style',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expectType<string>(
|
||||
makeBadge({
|
||||
message: 'passed',
|
||||
})
|
||||
}),
|
||||
)
|
||||
expectType<string>(
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
})
|
||||
}),
|
||||
)
|
||||
expectType<string>(
|
||||
makeBadge({
|
||||
@@ -28,7 +28,7 @@ expectType<string>(
|
||||
labelColor: 'green',
|
||||
color: 'red',
|
||||
style: 'flat',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const error = new ValidationError()
|
||||
|
||||
@@ -69,7 +69,7 @@ function getLogoElement({ logo, horizPadding, badgeHeight, logoWidth }) {
|
||||
|
||||
function renderBadge(
|
||||
{ links, leftWidth, rightWidth, height, accessibleText },
|
||||
content
|
||||
content,
|
||||
) {
|
||||
const width = leftWidth + rightWidth
|
||||
const leftLink = links[0]
|
||||
@@ -397,7 +397,7 @@ class Plastic extends Badge {
|
||||
accessibleText: this.accessibleText,
|
||||
height: this.constructor.height,
|
||||
},
|
||||
[gradient, clipPath, backgroundGroup, this.foregroundGroupElement]
|
||||
[gradient, clipPath, backgroundGroup, this.foregroundGroupElement],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -446,7 +446,7 @@ class Flat extends Badge {
|
||||
accessibleText: this.accessibleText,
|
||||
height: this.constructor.height,
|
||||
},
|
||||
[gradient, clipPath, backgroundGroup, this.foregroundGroupElement]
|
||||
[gradient, clipPath, backgroundGroup, this.foregroundGroupElement],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -478,7 +478,7 @@ class FlatSquare extends Badge {
|
||||
accessibleText: this.accessibleText,
|
||||
height: this.constructor.height,
|
||||
},
|
||||
[backgroundGroup, this.foregroundGroupElement]
|
||||
[backgroundGroup, this.foregroundGroupElement],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -748,7 +748,7 @@ function social({
|
||||
accessibleText,
|
||||
height: externalHeight,
|
||||
},
|
||||
[style, gradients, backgroundGroup, logoElement, foregroundGroup]
|
||||
[style, gradients, backgroundGroup, logoElement, foregroundGroup],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -978,7 +978,7 @@ function forTheBadge({
|
||||
accessibleText: createAccessibleText({ label, message }),
|
||||
height: BADGE_HEIGHT,
|
||||
},
|
||||
[backgroundGroup, foregroundGroup]
|
||||
[backgroundGroup, foregroundGroup],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ test(toSvgColor, () => {
|
||||
given('papayawhip').expect('papayawhip')
|
||||
given('purple').expect('purple')
|
||||
forCases([given(''), given(undefined), given('not-a-color')]).expect(
|
||||
undefined
|
||||
undefined,
|
||||
)
|
||||
given('lightgray').expect('#9f9f9f')
|
||||
given('informational').expect('#007ec6')
|
||||
|
||||
@@ -32,7 +32,7 @@ function _validate(format) {
|
||||
]
|
||||
if ('style' in format && !styleValues.includes(format.style)) {
|
||||
throw new ValidationError(
|
||||
`Field \`style\` must be one of (${styleValues.toString()})`
|
||||
`Field \`style\` must be one of (${styleValues.toString()})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function _clean(format) {
|
||||
cleaned[key] = format[key]
|
||||
} else {
|
||||
throw new ValidationError(
|
||||
`Unexpected field '${key}'. Allowed values are (${expectedKeys.toString()})`
|
||||
`Unexpected field '${key}'. Allowed values are (${expectedKeys.toString()})`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,12 +10,12 @@ describe('makeBadge function', function () {
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
})
|
||||
}),
|
||||
).to.satisfy(isSvg)
|
||||
expect(
|
||||
makeBadge({
|
||||
message: 'passed',
|
||||
})
|
||||
}),
|
||||
).to.satisfy(isSvg)
|
||||
expect(
|
||||
makeBadge({
|
||||
@@ -23,7 +23,7 @@ describe('makeBadge function', function () {
|
||||
message: 'passed',
|
||||
color: 'green',
|
||||
style: 'flat',
|
||||
})
|
||||
}),
|
||||
).to.satisfy(isSvg)
|
||||
})
|
||||
|
||||
@@ -32,44 +32,44 @@ describe('makeBadge function', function () {
|
||||
console.log(x)
|
||||
expect(() => makeBadge(x)).to.throw(
|
||||
ValidationError,
|
||||
'makeBadge takes an argument of type object'
|
||||
'makeBadge takes an argument of type object',
|
||||
)
|
||||
})
|
||||
expect(() => makeBadge({})).to.throw(
|
||||
ValidationError,
|
||||
'Field `message` is required'
|
||||
'Field `message` is required',
|
||||
)
|
||||
expect(() => makeBadge({ label: 'build' })).to.throw(
|
||||
ValidationError,
|
||||
'Field `message` is required'
|
||||
'Field `message` is required',
|
||||
)
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', labelColor: 7 })
|
||||
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' })
|
||||
makeBadge({ label: 'build', message: 'passed', format: 'png' }),
|
||||
).to.throw(ValidationError, "Unexpected field 'format'")
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', template: 'flat' })
|
||||
makeBadge({ label: 'build', message: 'passed', template: 'flat' }),
|
||||
).to.throw(ValidationError, "Unexpected field 'template'")
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', foo: 'bar' })
|
||||
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)'
|
||||
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)',
|
||||
)
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', style: 'popout' })
|
||||
makeBadge({ label: 'build', message: 'passed', style: 'popout' }),
|
||||
).to.throw(
|
||||
ValidationError,
|
||||
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)'
|
||||
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,6 +58,6 @@ module.exports = function makeBadge({
|
||||
logoPadding: logo && label.length ? 3 : 0,
|
||||
color: toSvgColor(color),
|
||||
labelColor: toSvgColor(labelColor),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ const snapshot = require('snap-shot-it')
|
||||
const prettier = require('prettier')
|
||||
const makeBadge = require('./make-badge')
|
||||
|
||||
function expectBadgeToMatchSnapshot(format) {
|
||||
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
|
||||
async function expectBadgeToMatchSnapshot(format) {
|
||||
snapshot(await prettier.format(makeBadge(format), { parser: 'html' }))
|
||||
}
|
||||
|
||||
function testColor(color = '', colorAttr = 'color') {
|
||||
@@ -17,7 +17,7 @@ function testColor(color = '', colorAttr = 'color') {
|
||||
message: 'Bob',
|
||||
[colorAttr]: color,
|
||||
format: 'json',
|
||||
})
|
||||
}),
|
||||
).color
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('The badge generator', function () {
|
||||
given('bluish'),
|
||||
given('almostred'),
|
||||
given('brightmaroon'),
|
||||
given('cactus')
|
||||
given('cactus'),
|
||||
).expect(undefined)
|
||||
})
|
||||
})
|
||||
@@ -87,8 +87,8 @@ describe('The badge generator', function () {
|
||||
.and.to.include('grown')
|
||||
})
|
||||
|
||||
it('should match snapshot', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshot', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -138,14 +138,14 @@ describe('The badge generator', function () {
|
||||
message: 'Bob',
|
||||
format: 'svg',
|
||||
style: 'unknown_style',
|
||||
})
|
||||
}),
|
||||
).to.throw(Error, "Unknown badge style: 'unknown_style'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('"flat" template badge generation', function () {
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -155,8 +155,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -167,8 +167,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -177,8 +177,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -188,8 +188,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo and labelColor', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -200,8 +200,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with links', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -212,8 +212,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the label color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the label color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -223,8 +223,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the message color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the message color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -236,8 +236,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
|
||||
describe('"flat-square" template badge generation', function () {
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -247,8 +247,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -259,8 +259,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -269,8 +269,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -280,8 +280,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo and labelColor', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -292,8 +292,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with links', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -304,8 +304,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the label color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the label color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -315,8 +315,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the message color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the message color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -328,8 +328,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
|
||||
describe('"plastic" template badge generation', function () {
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -339,8 +339,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -351,8 +351,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -361,8 +361,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -372,8 +372,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo and labelColor', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -384,8 +384,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with links', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -396,8 +396,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the label color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the label color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -407,8 +407,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the message color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the message color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -428,7 +428,7 @@ describe('The badge generator', function () {
|
||||
message: 1999,
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
}),
|
||||
)
|
||||
.to.include('1998')
|
||||
.and.to.include('1999')
|
||||
@@ -441,14 +441,14 @@ describe('The badge generator', function () {
|
||||
message: '1 string',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
})
|
||||
}),
|
||||
)
|
||||
.to.include('LABEL')
|
||||
.and.to.include('1 STRING')
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -458,8 +458,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -470,8 +470,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -480,8 +480,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -491,8 +491,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo and labelColor', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -503,8 +503,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with links', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -515,8 +515,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the label color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the label color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -526,8 +526,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: black text when the message color is light', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: black text when the message color is light', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -546,7 +546,7 @@ describe('The badge generator', function () {
|
||||
message: 'some-value',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
})
|
||||
}),
|
||||
)
|
||||
.to.include('Some-key')
|
||||
.and.to.include('some-value')
|
||||
@@ -560,14 +560,14 @@ describe('The badge generator', function () {
|
||||
message: 'some-value',
|
||||
format: 'json',
|
||||
style: 'social',
|
||||
})
|
||||
}),
|
||||
)
|
||||
.to.include('""')
|
||||
.and.to.include('some-value')
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -577,8 +577,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -589,8 +589,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -599,8 +599,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -610,8 +610,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, with logo and labelColor', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message only, with logo and labelColor', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -622,8 +622,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message/label, with links', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('should match snapshots: message/label, with links', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
@@ -636,8 +636,8 @@ describe('The badge generator', function () {
|
||||
})
|
||||
|
||||
describe('badges with logos should always produce the same badge', function () {
|
||||
it('badge with logo', function () {
|
||||
expectBadgeToMatchSnapshot({
|
||||
it('badge with logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'label',
|
||||
message: 'message',
|
||||
format: 'svg',
|
||||
|
||||
@@ -66,7 +66,7 @@ class XmlElement {
|
||||
})
|
||||
.join(' ')
|
||||
return stripXmlWhitespace(
|
||||
`<${this.name}${attrsStr}>${content}</${this.name}>`
|
||||
`<${this.name}${attrsStr}>${content}</${this.name}>`,
|
||||
)
|
||||
}
|
||||
return stripXmlWhitespace(`<${this.name}${attrsStr}/>`)
|
||||
@@ -88,7 +88,7 @@ class ElementList {
|
||||
typeof el.render === 'function'
|
||||
? acc + el.render()
|
||||
: acc + escapeXml(el),
|
||||
''
|
||||
'',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
"badge": "lib/badge-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14",
|
||||
"npm": ">= 6"
|
||||
"node": ">=16"
|
||||
},
|
||||
"collective": {
|
||||
"type": "opencollective",
|
||||
|
||||
94
core/badge-urls/make-badge-url.d.ts
vendored
94
core/badge-urls/make-badge-url.d.ts
vendored
@@ -1,94 +0,0 @@
|
||||
export function badgeUrlFromPath({
|
||||
baseUrl,
|
||||
path,
|
||||
queryParams,
|
||||
style,
|
||||
format,
|
||||
longCache,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
path: string
|
||||
queryParams: { [k: string]: string | number | boolean }
|
||||
style?: string
|
||||
format?: string
|
||||
longCache?: boolean
|
||||
}): string
|
||||
|
||||
export function encodeField(s: string): string
|
||||
|
||||
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({
|
||||
baseUrl,
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
style,
|
||||
namedLogo,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
format,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
label: string
|
||||
message: string
|
||||
color?: string
|
||||
labelColor?: string
|
||||
style?: string
|
||||
namedLogo?: string
|
||||
logoColor?: string
|
||||
logoWidth?: number
|
||||
logoPosition?: number
|
||||
format?: string
|
||||
}): string
|
||||
|
||||
export function dynamicBadgeUrl({
|
||||
baseUrl,
|
||||
datatype,
|
||||
label,
|
||||
dataUrl,
|
||||
query,
|
||||
prefix,
|
||||
suffix,
|
||||
color,
|
||||
style,
|
||||
format,
|
||||
}: {
|
||||
baseUrl?: string
|
||||
datatype: string
|
||||
label: string
|
||||
dataUrl: string
|
||||
query: string
|
||||
prefix: string
|
||||
suffix: string
|
||||
color?: string
|
||||
style?: string
|
||||
format?: string
|
||||
}): string
|
||||
|
||||
export function rasterRedirectUrl(
|
||||
{ rasterUrl }: { rasterUrl: string },
|
||||
badgeUrl: string
|
||||
): string
|
||||
@@ -1,119 +1,5 @@
|
||||
// Avoid "Attempted import error: 'URL' is not exported from 'url'" in frontend.
|
||||
import url from 'url'
|
||||
import queryString from 'query-string'
|
||||
|
||||
function badgeUrlFromPath({
|
||||
baseUrl = '',
|
||||
path,
|
||||
queryParams,
|
||||
style,
|
||||
format = '',
|
||||
longCache = false,
|
||||
}) {
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
|
||||
const outQueryString = queryString.stringify({
|
||||
cacheSeconds: longCache ? '2592000' : undefined,
|
||||
style,
|
||||
...queryParams,
|
||||
})
|
||||
const suffix = outQueryString ? `?${outQueryString}` : ''
|
||||
|
||||
return `${baseUrl}${path}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function encodeField(s) {
|
||||
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'))
|
||||
}
|
||||
|
||||
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}` : ''
|
||||
return `${baseUrl}/badge/${path}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function queryStringStaticBadgeUrl({
|
||||
baseUrl = '',
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
style,
|
||||
namedLogo,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
format = '',
|
||||
}) {
|
||||
// schemaVersion could be a parameter if we iterate on it,
|
||||
// for now it's hardcoded to the only supported version.
|
||||
const schemaVersion = '1'
|
||||
const suffix = `?${queryString.stringify({
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
style,
|
||||
logo: namedLogo,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
})}`
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
return `${baseUrl}/static/v${schemaVersion}${outExt}${suffix}`
|
||||
}
|
||||
|
||||
function dynamicBadgeUrl({
|
||||
baseUrl,
|
||||
datatype,
|
||||
label,
|
||||
dataUrl,
|
||||
query,
|
||||
prefix,
|
||||
suffix,
|
||||
color,
|
||||
style,
|
||||
format = '',
|
||||
}) {
|
||||
const outExt = format.length ? `.${format}` : ''
|
||||
|
||||
const queryParams = {
|
||||
label,
|
||||
url: dataUrl,
|
||||
query,
|
||||
style,
|
||||
}
|
||||
|
||||
if (color) {
|
||||
queryParams.color = color
|
||||
}
|
||||
if (prefix) {
|
||||
queryParams.prefix = prefix
|
||||
}
|
||||
if (suffix) {
|
||||
queryParams.suffix = suffix
|
||||
}
|
||||
|
||||
const outQueryString = queryString.stringify(queryParams)
|
||||
return `${baseUrl}/badge/dynamic/${datatype}${outExt}?${outQueryString}`
|
||||
}
|
||||
|
||||
function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
|
||||
// Ensure we're always using the `rasterUrl` by using just the path from
|
||||
@@ -124,11 +10,4 @@ function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
|
||||
return result
|
||||
}
|
||||
|
||||
export {
|
||||
badgeUrlFromPath,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
dynamicBadgeUrl,
|
||||
rasterRedirectUrl,
|
||||
}
|
||||
export { rasterRedirectUrl }
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import {
|
||||
badgeUrlFromPath,
|
||||
encodeField,
|
||||
staticBadgeUrl,
|
||||
queryStringStaticBadgeUrl,
|
||||
dynamicBadgeUrl,
|
||||
} from './make-badge-url.js'
|
||||
|
||||
describe('Badge URL generation functions', function () {
|
||||
test(badgeUrlFromPath, () => {
|
||||
given({
|
||||
baseUrl: 'http://example.com',
|
||||
path: '/npm/v/gh-badges',
|
||||
style: 'flat-square',
|
||||
longCache: true,
|
||||
}).expect(
|
||||
'http://example.com/npm/v/gh-badges?cacheSeconds=2592000&style=flat-square'
|
||||
)
|
||||
})
|
||||
|
||||
test(encodeField, () => {
|
||||
given('foo').expect('foo')
|
||||
given('').expect('')
|
||||
given('happy go lucky').expect('happy%20go%20lucky')
|
||||
given('do-right').expect('do--right')
|
||||
given('it_is_a_snake').expect('it__is__a__snake')
|
||||
})
|
||||
|
||||
test(staticBadgeUrl, () => {
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
}).expect('/badge/foo-bar-blue?style=flat-square')
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
format: 'png',
|
||||
namedLogo: 'github',
|
||||
}).expect('/badge/foo-bar-blue.png?logo=github&style=flat-square')
|
||||
given({
|
||||
label: 'Hello World',
|
||||
message: 'Привет Мир',
|
||||
color: '#aabbcc',
|
||||
}).expect(
|
||||
'/badge/Hello%20World-%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80-%23aabbcc'
|
||||
)
|
||||
given({
|
||||
label: '123-123',
|
||||
message: 'abc-abc',
|
||||
color: 'blue',
|
||||
}).expect('/badge/123--123-abc--abc-blue')
|
||||
given({
|
||||
label: '123-123',
|
||||
message: '',
|
||||
color: 'blue',
|
||||
style: 'social',
|
||||
}).expect('/badge/123--123--blue?style=social')
|
||||
given({
|
||||
label: '',
|
||||
message: 'blue',
|
||||
color: 'blue',
|
||||
}).expect('/badge/-blue-blue')
|
||||
})
|
||||
|
||||
test(queryStringStaticBadgeUrl, () => {
|
||||
// the query-string library sorts parameters by name
|
||||
given({
|
||||
label: 'foo',
|
||||
message: 'bar',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
}).expect('/static/v1?color=blue&label=foo&message=bar&style=flat-square')
|
||||
given({
|
||||
label: 'foo Bar',
|
||||
message: 'bar Baz',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
format: 'png',
|
||||
namedLogo: 'github',
|
||||
}).expect(
|
||||
'/static/v1.png?color=blue&label=foo%20Bar&logo=github&message=bar%20Baz&style=flat-square'
|
||||
)
|
||||
given({
|
||||
label: 'Hello World',
|
||||
message: 'Привет Мир',
|
||||
color: '#aabbcc',
|
||||
}).expect(
|
||||
'/static/v1?color=%23aabbcc&label=Hello%20World&message=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80'
|
||||
)
|
||||
})
|
||||
|
||||
test(dynamicBadgeUrl, () => {
|
||||
const dataUrl = 'http://example.com/foo.json'
|
||||
const query = '$.bar'
|
||||
const prefix = 'value: '
|
||||
given({
|
||||
baseUrl: 'http://img.example.com',
|
||||
datatype: 'json',
|
||||
label: 'foo',
|
||||
dataUrl,
|
||||
query,
|
||||
prefix,
|
||||
style: 'plastic',
|
||||
}).expect(
|
||||
[
|
||||
'http://img.example.com/badge/dynamic/json',
|
||||
'?label=foo',
|
||||
`&prefix=${encodeURIComponent(prefix)}`,
|
||||
`&query=${encodeURIComponent(query)}`,
|
||||
'&style=plastic',
|
||||
`&url=${encodeURIComponent(dataUrl)}`,
|
||||
].join('')
|
||||
)
|
||||
const suffix = '<- value'
|
||||
const color = 'blue'
|
||||
given({
|
||||
baseUrl: 'http://img.example.com',
|
||||
datatype: 'json',
|
||||
label: 'foo',
|
||||
dataUrl,
|
||||
query,
|
||||
suffix,
|
||||
color,
|
||||
style: 'plastic',
|
||||
}).expect(
|
||||
[
|
||||
'http://img.example.com/badge/dynamic/json',
|
||||
'?color=blue',
|
||||
'&label=foo',
|
||||
`&query=${encodeURIComponent(query)}`,
|
||||
'&style=plastic',
|
||||
`&suffix=${encodeURIComponent(suffix)}`,
|
||||
`&url=${encodeURIComponent(dataUrl)}`,
|
||||
].join('')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@ describe('Badge URL helper functions', function () {
|
||||
given('single trailing underscore_').expect('single trailing underscore ')
|
||||
given('__double leading underscores').expect('_double leading underscores')
|
||||
given('double trailing underscores__').expect(
|
||||
'double trailing underscores_'
|
||||
'double trailing underscores_',
|
||||
)
|
||||
given('treble___underscores').expect('treble_ underscores')
|
||||
given('fourfold____underscores').expect('fourfold__underscores')
|
||||
|
||||
@@ -11,7 +11,7 @@ class AuthHelper {
|
||||
isRequired = false,
|
||||
defaultToEmptyStringForUser = false,
|
||||
},
|
||||
config
|
||||
config,
|
||||
) {
|
||||
if (!userKey && !passKey) {
|
||||
throw Error('Expected userKey or passKey to be set')
|
||||
@@ -142,7 +142,7 @@ class AuthHelper {
|
||||
|
||||
withBasicAuth(requestParams) {
|
||||
return this._withAnyAuth(requestParams, requestParams =>
|
||||
this.constructor._mergeAuth(requestParams, this._basicAuth)
|
||||
this.constructor._mergeAuth(requestParams, this._basicAuth),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -172,13 +172,13 @@ class AuthHelper {
|
||||
|
||||
withBearerAuthHeader(
|
||||
requestParams,
|
||||
bearerKey = 'Bearer' // lgtm [js/hardcoded-credentials]
|
||||
bearerKey = 'Bearer', // lgtm [js/hardcoded-credentials]
|
||||
) {
|
||||
return this._withAnyAuth(requestParams, requestParams =>
|
||||
this.constructor._mergeHeaders(
|
||||
requestParams,
|
||||
this._bearerAuthHeader(bearerKey)
|
||||
)
|
||||
this._bearerAuthHeader(bearerKey),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ class AuthHelper {
|
||||
this.constructor._mergeQueryParams(requestParams, {
|
||||
...(userKey ? { [userKey]: this._user } : undefined),
|
||||
...(passKey ? { [passKey]: this._pass } : undefined),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ describe('AuthHelper', function () {
|
||||
it('throws without userKey or passKey', function () {
|
||||
expect(() => new AuthHelper({}, {})).to.throw(
|
||||
Error,
|
||||
'Expected userKey or passKey to be set'
|
||||
'Expected userKey or passKey to be set',
|
||||
)
|
||||
})
|
||||
it('throws without serviceKey or authorizedOrigins', function () {
|
||||
expect(
|
||||
() => new AuthHelper({ userKey: 'myci_user', passKey: 'myci_pass' }, {})
|
||||
() =>
|
||||
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 () {
|
||||
@@ -25,8 +26,8 @@ describe('AuthHelper', function () {
|
||||
passKey: 'myci_pass',
|
||||
authorizedOrigins: true,
|
||||
},
|
||||
{ private: {} }
|
||||
)
|
||||
{ private: {} },
|
||||
),
|
||||
).to.throw(Error, 'Expected authorizedOrigins to be an array of origins')
|
||||
})
|
||||
})
|
||||
@@ -35,7 +36,7 @@ describe('AuthHelper', function () {
|
||||
function validate(config, privateConfig) {
|
||||
return new AuthHelper(
|
||||
{ authorizedOrigins: ['https://example.test'], ...config },
|
||||
{ private: privateConfig }
|
||||
{ private: privateConfig },
|
||||
).isValid
|
||||
}
|
||||
test(validate, () => {
|
||||
@@ -43,20 +44,20 @@ describe('AuthHelper', function () {
|
||||
// Fully configured user + pass.
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' }
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' },
|
||||
),
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass' },
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' }
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' },
|
||||
),
|
||||
// Fully configured user or pass.
|
||||
given(
|
||||
{ userKey: 'myci_user', isRequired: true },
|
||||
{ myci_user: 'admin' }
|
||||
{ myci_user: 'admin' },
|
||||
),
|
||||
given(
|
||||
{ passKey: 'myci_pass', isRequired: true },
|
||||
{ myci_pass: 'abc123' }
|
||||
{ myci_pass: 'abc123' },
|
||||
),
|
||||
given({ userKey: 'myci_user' }, { myci_user: 'admin' }),
|
||||
given({ passKey: 'myci_pass' }, { myci_pass: 'abc123' }),
|
||||
@@ -70,16 +71,16 @@ describe('AuthHelper', function () {
|
||||
// Partly configured.
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
|
||||
{ myci_user: 'admin' }
|
||||
{ myci_user: 'admin' },
|
||||
),
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass' },
|
||||
{ myci_user: 'admin' }
|
||||
{ myci_user: 'admin' },
|
||||
),
|
||||
// Missing required config.
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
|
||||
{}
|
||||
{},
|
||||
),
|
||||
given({ userKey: 'myci_user', isRequired: true }, {}),
|
||||
given({ passKey: 'myci_pass', isRequired: true }, {}),
|
||||
@@ -91,18 +92,18 @@ describe('AuthHelper', function () {
|
||||
function validate(config, privateConfig) {
|
||||
return new AuthHelper(
|
||||
{ authorizedOrigins: ['https://example.test'], ...config },
|
||||
{ private: privateConfig }
|
||||
{ private: privateConfig },
|
||||
)._basicAuth
|
||||
}
|
||||
test(validate, () => {
|
||||
forCases([
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass', isRequired: true },
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' }
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' },
|
||||
),
|
||||
given(
|
||||
{ userKey: 'myci_user', passKey: 'myci_pass' },
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' }
|
||||
{ myci_user: 'admin', myci_pass: 'abc123' },
|
||||
),
|
||||
]).expect({ username: 'admin', password: 'abc123' })
|
||||
given({ userKey: 'myci_user' }, { myci_user: 'admin' }).expect({
|
||||
@@ -114,11 +115,11 @@ describe('AuthHelper', function () {
|
||||
password: 'abc123',
|
||||
})
|
||||
given({ userKey: 'myci_user', passKey: 'myci_pass' }, {}).expect(
|
||||
undefined
|
||||
undefined,
|
||||
)
|
||||
given(
|
||||
{ passKey: 'myci_pass', defaultToEmptyStringForUser: true },
|
||||
{ myci_pass: 'abc123' }
|
||||
{ myci_pass: 'abc123' },
|
||||
).expect({
|
||||
username: '',
|
||||
password: 'abc123',
|
||||
@@ -168,7 +169,7 @@ describe('AuthHelper', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
})
|
||||
}),
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
})
|
||||
@@ -192,7 +193,7 @@ describe('AuthHelper', function () {
|
||||
expect(() =>
|
||||
authHelper.enforceStrictSsl({
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
})
|
||||
}),
|
||||
).not.to.throw()
|
||||
})
|
||||
})
|
||||
@@ -318,7 +319,7 @@ describe('AuthHelper', function () {
|
||||
},
|
||||
},
|
||||
private: { myci_user: 'admin', myci_pass: 'abc123' },
|
||||
}
|
||||
},
|
||||
)
|
||||
const withBasicAuth = requestOptions =>
|
||||
authHelper.withBasicAuth(requestOptions)
|
||||
@@ -376,7 +377,7 @@ describe('AuthHelper', function () {
|
||||
withBasicAuth({
|
||||
url: 'https://myci.test/api',
|
||||
options: { https: { rejectUnauthorized: false } },
|
||||
})
|
||||
}),
|
||||
).to.throw(InvalidParameter)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,6 +44,12 @@ 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 {object} [attrs.systemErrors={}] Key-value map of got network exception codes
|
||||
* and an object of params to pass when we construct an Inaccessible exception object
|
||||
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
|
||||
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
|
||||
* for allowed keys
|
||||
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
|
||||
* @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.
|
||||
@@ -62,6 +68,7 @@ class BaseGraphqlService extends BaseService {
|
||||
variables = {},
|
||||
options = {},
|
||||
httpErrorMessages = {},
|
||||
systemErrors = {},
|
||||
transformJson = data => data,
|
||||
transformErrors = defaultTransformErrors,
|
||||
}) {
|
||||
@@ -74,7 +81,8 @@ class BaseGraphqlService extends BaseService {
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
options: mergedOptions,
|
||||
errorMessages: httpErrorMessages,
|
||||
httpErrors: httpErrorMessages,
|
||||
systemErrors,
|
||||
})
|
||||
const json = transformJson(this._parseJson(buffer))
|
||||
if (json.errors) {
|
||||
@@ -83,7 +91,7 @@ class BaseGraphqlService extends BaseService {
|
||||
throw exception
|
||||
} else {
|
||||
throw Error(
|
||||
`transformErrors() must return a ShieldsRuntimeError; got ${exception}`
|
||||
`transformErrors() must return a ShieldsRuntimeError; got ${exception}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,23 +35,23 @@ describe('BaseGraphqlService', function () {
|
||||
Promise.resolve({
|
||||
buffer: '{"some": "json"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyGraphqlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/graphql',
|
||||
{
|
||||
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
|
||||
body: '{"query":"{\\n requiredString\\n}","variables":{}}',
|
||||
headers: { Accept: 'application/json' },
|
||||
method: 'POST',
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -74,17 +74,17 @@ describe('BaseGraphqlService', function () {
|
||||
|
||||
await WithOptions.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/graphql',
|
||||
{
|
||||
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}',
|
||||
body: '{"query":"{\\n requiredString\\n}","variables":{}}',
|
||||
headers: { Accept: 'application/json' },
|
||||
method: 'POST',
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -98,8 +98,8 @@ describe('BaseGraphqlService', function () {
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'some-string',
|
||||
})
|
||||
@@ -113,8 +113,8 @@ describe('BaseGraphqlService', function () {
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -130,8 +130,8 @@ describe('BaseGraphqlService', function () {
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -149,8 +149,8 @@ describe('BaseGraphqlService', function () {
|
||||
expect(
|
||||
await DummyGraphqlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -188,8 +188,8 @@ describe('BaseGraphqlService', function () {
|
||||
expect(
|
||||
await WithErrorHandler.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -30,14 +30,26 @@ class BaseJsonService extends BaseService {
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* @param {object} [attrs.httpErrors={}] Key-value map of status codes
|
||||
* 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 {object} [attrs.systemErrors={}] Key-value map of got network exception codes
|
||||
* and an object of params to pass when we construct an Inaccessible exception object
|
||||
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
|
||||
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
|
||||
* for allowed keys
|
||||
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
*/
|
||||
async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
|
||||
async _requestJson({
|
||||
schema,
|
||||
url,
|
||||
options = {},
|
||||
httpErrors = {},
|
||||
systemErrors = {},
|
||||
}) {
|
||||
const mergedOptions = {
|
||||
...{ headers: { Accept: 'application/json' } },
|
||||
...options,
|
||||
@@ -45,7 +57,8 @@ class BaseJsonService extends BaseService {
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
options: mergedOptions,
|
||||
errorMessages,
|
||||
httpErrors,
|
||||
systemErrors,
|
||||
})
|
||||
const json = this._parseJson(buffer)
|
||||
return this.constructor._validate(json, schema)
|
||||
|
||||
@@ -28,21 +28,21 @@ describe('BaseJsonService', function () {
|
||||
Promise.resolve({
|
||||
buffer: '{"some": "json"}',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyJsonService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.json',
|
||||
{
|
||||
headers: { Accept: 'application/json' },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('BaseJsonService', function () {
|
||||
|
||||
await WithOptions.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
@@ -69,7 +69,7 @@ describe('BaseJsonService', function () {
|
||||
headers: { Accept: 'application/json' },
|
||||
method: 'POST',
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -83,8 +83,8 @@ describe('BaseJsonService', function () {
|
||||
expect(
|
||||
await DummyJsonService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'some-string',
|
||||
})
|
||||
@@ -98,8 +98,8 @@ describe('BaseJsonService', function () {
|
||||
expect(
|
||||
await DummyJsonService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -115,8 +115,8 @@ describe('BaseJsonService', function () {
|
||||
expect(
|
||||
await DummyJsonService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -33,14 +33,14 @@ export default class BaseStaticService extends BaseService {
|
||||
{},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
queryParams
|
||||
queryParams,
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
this,
|
||||
)
|
||||
|
||||
// The final capture group is the extension.
|
||||
|
||||
@@ -53,10 +53,16 @@ class BaseSvgScrapingService extends BaseService {
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* @param {object} [attrs.httpErrors={}] Key-value map of status codes
|
||||
* 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 {object} [attrs.systemErrors={}] Key-value map of got network exception codes
|
||||
* and an object of params to pass when we construct an Inaccessible exception object
|
||||
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
|
||||
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
|
||||
* for allowed keys
|
||||
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
*/
|
||||
@@ -65,7 +71,8 @@ class BaseSvgScrapingService extends BaseService {
|
||||
valueMatcher,
|
||||
url,
|
||||
options = {},
|
||||
errorMessages = {},
|
||||
httpErrors = {},
|
||||
systemErrors = {},
|
||||
}) {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
const mergedOptions = {
|
||||
@@ -75,7 +82,8 @@ class BaseSvgScrapingService extends BaseService {
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
options: mergedOptions,
|
||||
errorMessages,
|
||||
httpErrors,
|
||||
systemErrors,
|
||||
})
|
||||
logTrace(emojic.dart, 'Response SVG', buffer)
|
||||
const data = {
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('BaseSvgScrapingService', function () {
|
||||
describe('valueFromSvgBadge', function () {
|
||||
it('should find the correct value', function () {
|
||||
expect(BaseSvgScrapingService.valueFromSvgBadge(exampleSvg)).to.equal(
|
||||
exampleMessage
|
||||
exampleMessage,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -40,21 +40,21 @@ describe('BaseSvgScrapingService', function () {
|
||||
Promise.resolve({
|
||||
buffer: exampleSvg,
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _requestFetcher with the expected header', async function () {
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.svg',
|
||||
{
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('BaseSvgScrapingService', function () {
|
||||
|
||||
await WithCustomOptions.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
@@ -84,7 +84,7 @@ describe('BaseSvgScrapingService', function () {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'image/svg+xml' },
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -98,8 +98,8 @@ describe('BaseSvgScrapingService', function () {
|
||||
expect(
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: exampleMessage,
|
||||
})
|
||||
@@ -124,8 +124,8 @@ describe('BaseSvgScrapingService', function () {
|
||||
expect(
|
||||
await WithValueMatcher.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'a different message',
|
||||
})
|
||||
@@ -139,8 +139,8 @@ describe('BaseSvgScrapingService', function () {
|
||||
expect(
|
||||
await DummySvgScrapingService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -24,10 +24,16 @@ class BaseXmlService extends BaseService {
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* @param {object} [attrs.httpErrors={}] Key-value map of status codes
|
||||
* 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 {object} [attrs.systemErrors={}] Key-value map of got network exception codes
|
||||
* and an object of params to pass when we construct an Inaccessible exception object
|
||||
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
|
||||
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
|
||||
* for allowed keys
|
||||
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
|
||||
* @param {object} [attrs.parserOptions={}] Options to pass to fast-xml-parser. See
|
||||
* [documentation](https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json)
|
||||
* @returns {object} Parsed response
|
||||
@@ -38,7 +44,8 @@ class BaseXmlService extends BaseService {
|
||||
schema,
|
||||
url,
|
||||
options = {},
|
||||
errorMessages = {},
|
||||
httpErrors = {},
|
||||
systemErrors = {},
|
||||
parserOptions = {},
|
||||
}) {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
@@ -49,7 +56,8 @@ class BaseXmlService extends BaseService {
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
options: mergedOptions,
|
||||
errorMessages,
|
||||
httpErrors,
|
||||
systemErrors,
|
||||
})
|
||||
const validateResult = XMLValidator.validate(buffer)
|
||||
if (validateResult !== true) {
|
||||
|
||||
@@ -28,21 +28,21 @@ describe('BaseXmlService', function () {
|
||||
Promise.resolve({
|
||||
buffer: '<requiredString>some-string</requiredString>',
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyXmlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
'http://example.com/foo.xml',
|
||||
{
|
||||
headers: { Accept: 'application/xml, text/xml' },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('BaseXmlService', function () {
|
||||
|
||||
await WithCustomOptions.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
@@ -71,7 +71,7 @@ describe('BaseXmlService', function () {
|
||||
headers: { Accept: 'application/xml, text/xml' },
|
||||
method: 'POST',
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -85,8 +85,8 @@ describe('BaseXmlService', function () {
|
||||
expect(
|
||||
await DummyXmlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'some-string',
|
||||
})
|
||||
@@ -112,8 +112,8 @@ describe('BaseXmlService', function () {
|
||||
expect(
|
||||
await DummyXmlServiceWithParserOption.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'some-string with trailing whitespace ',
|
||||
})
|
||||
@@ -127,8 +127,8 @@ describe('BaseXmlService', function () {
|
||||
expect(
|
||||
await DummyXmlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -144,8 +144,8 @@ describe('BaseXmlService', function () {
|
||||
expect(
|
||||
await DummyXmlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -23,10 +23,16 @@ class BaseYamlService extends BaseService {
|
||||
* @param {string} attrs.url URL to request
|
||||
* @param {object} [attrs.options={}] Options to pass to got. See
|
||||
* [documentation](https://github.com/sindresorhus/got/blob/main/documentation/2-options.md)
|
||||
* @param {object} [attrs.errorMessages={}] Key-value map of status codes
|
||||
* @param {object} [attrs.httpErrors={}] Key-value map of status codes
|
||||
* 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 {object} [attrs.systemErrors={}] Key-value map of got network exception codes
|
||||
* and an object of params to pass when we construct an Inaccessible exception object
|
||||
* e.g: `{ ECONNRESET: { prettyMessage: 'connection reset' } }`.
|
||||
* See {@link https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md#errorcodes got error codes}
|
||||
* for allowed keys
|
||||
* and {@link module:core/base-service/errors~RuntimeErrorProps} for allowed values
|
||||
* @param {object} [attrs.encoding='utf8'] Character encoding
|
||||
* @returns {object} Parsed response
|
||||
* @see https://github.com/sindresorhus/got/blob/main/documentation/2-options.md
|
||||
@@ -35,7 +41,8 @@ class BaseYamlService extends BaseService {
|
||||
schema,
|
||||
url,
|
||||
options = {},
|
||||
errorMessages = {},
|
||||
httpErrors = {},
|
||||
systemErrors = {},
|
||||
encoding = 'utf8',
|
||||
}) {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
@@ -51,7 +58,8 @@ class BaseYamlService extends BaseService {
|
||||
const { buffer } = await this._request({
|
||||
url,
|
||||
options: mergedOptions,
|
||||
errorMessages,
|
||||
httpErrors,
|
||||
systemErrors,
|
||||
})
|
||||
let parsed
|
||||
try {
|
||||
|
||||
@@ -44,14 +44,14 @@ describe('BaseYamlService', function () {
|
||||
Promise.resolve({
|
||||
buffer: expectedYaml,
|
||||
res: { statusCode: 200 },
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('invokes _requestFetcher', async function () {
|
||||
await DummyYamlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
@@ -61,7 +61,7 @@ describe('BaseYamlService', function () {
|
||||
Accept:
|
||||
'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('BaseYamlService', function () {
|
||||
|
||||
await WithOptions.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
{ handleInternalErrors: false },
|
||||
)
|
||||
|
||||
expect(requestFetcher).to.have.been.calledOnceWith(
|
||||
@@ -91,7 +91,7 @@ describe('BaseYamlService', function () {
|
||||
},
|
||||
method: 'POST',
|
||||
searchParams: { queryParam: 123 },
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -105,8 +105,8 @@ describe('BaseYamlService', function () {
|
||||
expect(
|
||||
await DummyYamlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'some-string',
|
||||
})
|
||||
@@ -120,8 +120,8 @@ describe('BaseYamlService', function () {
|
||||
expect(
|
||||
await DummyYamlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -137,8 +137,8 @@ describe('BaseYamlService', function () {
|
||||
expect(
|
||||
await DummyYamlService.invoke(
|
||||
{ requestFetcher },
|
||||
{ handleInternalErrors: false }
|
||||
)
|
||||
{ handleInternalErrors: false },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
|
||||
@@ -44,7 +44,7 @@ const optionalStringWhenNamedLogoPresent = Joi.alternatives().conditional(
|
||||
{
|
||||
is: Joi.string().required(),
|
||||
then: Joi.string(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
|
||||
@@ -183,12 +183,28 @@ class BaseService {
|
||||
Joi.assert(
|
||||
this.defaultBadgeData,
|
||||
defaultBadgeDataSchema,
|
||||
`Default badge data for ${this.name}`
|
||||
`Default badge data for ${this.name}`,
|
||||
)
|
||||
|
||||
this.examples.forEach((example, index) =>
|
||||
validateExample(example, index, this)
|
||||
validateExample(example, index, this),
|
||||
)
|
||||
|
||||
// ensure openApi spec matches route
|
||||
if (this.openApi) {
|
||||
const preparedRoute = prepareRoute(this.route)
|
||||
for (const [key, value] of Object.entries(this.openApi)) {
|
||||
let example = key
|
||||
for (const param of value.get.parameters) {
|
||||
example = example.replace(`{${param.name}}`, param.example)
|
||||
}
|
||||
if (!example.match(preparedRoute.regex)) {
|
||||
throw new Error(
|
||||
`Inconsistent Open Api spec and Route found for service ${this.name}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getDefinition() {
|
||||
@@ -197,7 +213,7 @@ class BaseService {
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
const examples = this.examples.map((example, index) =>
|
||||
transformExample(example, index, this)
|
||||
transformExample(example, index, this),
|
||||
)
|
||||
|
||||
let route
|
||||
@@ -218,7 +234,7 @@ class BaseService {
|
||||
|
||||
constructor(
|
||||
{ requestFetcher, authHelper, metricHelper },
|
||||
{ handleInternalErrors }
|
||||
{ handleInternalErrors },
|
||||
) {
|
||||
this._requestFetcher = requestFetcher
|
||||
this.authHelper = authHelper
|
||||
@@ -226,7 +242,7 @@ class BaseService {
|
||||
this._metricHelper = metricHelper
|
||||
}
|
||||
|
||||
async _request({ url, options = {}, errorMessages = {} }) {
|
||||
async _request({ url, options = {}, httpErrors = {}, systemErrors = {} }) {
|
||||
const logTrace = (...args) => trace.logTrace('fetch', ...args)
|
||||
let logUrl = url
|
||||
const logOptions = Object.assign({}, options)
|
||||
@@ -234,9 +250,9 @@ class BaseService {
|
||||
const params = new URLSearchParams(
|
||||
Object.fromEntries(
|
||||
Object.entries(options.searchParams).filter(
|
||||
([k, v]) => v !== undefined
|
||||
)
|
||||
)
|
||||
([k, v]) => v !== undefined,
|
||||
),
|
||||
),
|
||||
)
|
||||
logUrl = `${url}?${params.toString()}`
|
||||
delete logOptions.searchParams
|
||||
@@ -244,12 +260,16 @@ class BaseService {
|
||||
logTrace(
|
||||
emojic.bowAndArrow,
|
||||
'Request',
|
||||
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}`
|
||||
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}`,
|
||||
)
|
||||
const { res, buffer } = await this._requestFetcher(
|
||||
url,
|
||||
options,
|
||||
systemErrors,
|
||||
)
|
||||
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 })
|
||||
return checkErrorResponse(httpErrors)({ buffer, res })
|
||||
}
|
||||
|
||||
static enabledMetrics = []
|
||||
@@ -275,7 +295,7 @@ class BaseService {
|
||||
prettyErrorMessage = 'invalid response data',
|
||||
includeKeys = false,
|
||||
allowAndStripUnknownKeys = true,
|
||||
} = {}
|
||||
} = {},
|
||||
) {
|
||||
return validate(
|
||||
{
|
||||
@@ -287,7 +307,7 @@ class BaseService {
|
||||
allowAndStripUnknownKeys,
|
||||
},
|
||||
data,
|
||||
schema
|
||||
schema,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -328,18 +348,22 @@ class BaseService {
|
||||
error instanceof Deprecated
|
||||
) {
|
||||
trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error)
|
||||
return {
|
||||
const serviceData = {
|
||||
isError: true,
|
||||
message: error.prettyMessage,
|
||||
color: 'lightgray',
|
||||
}
|
||||
if (error.cacheSeconds !== undefined) {
|
||||
serviceData.cacheSeconds = error.cacheSeconds
|
||||
}
|
||||
return serviceData
|
||||
} else if (this._handleInternalErrors) {
|
||||
if (
|
||||
!trace.logTrace(
|
||||
'unhandledError',
|
||||
emojic.boom,
|
||||
'Unhandled internal error',
|
||||
error
|
||||
error,
|
||||
)
|
||||
) {
|
||||
// This is where we end up if an unhandled exception is thrown in
|
||||
@@ -357,7 +381,7 @@ class BaseService {
|
||||
'unhandledError',
|
||||
emojic.boom,
|
||||
'Unhandled internal error',
|
||||
error
|
||||
error,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
@@ -367,7 +391,7 @@ class BaseService {
|
||||
context = {},
|
||||
config = {},
|
||||
namedParams = {},
|
||||
queryParams = {}
|
||||
queryParams = {},
|
||||
) {
|
||||
trace.logTrace('inbound', emojic.womanCook, 'Service class', this.name)
|
||||
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
|
||||
@@ -401,13 +425,13 @@ class BaseService {
|
||||
traceSuccessMessage: 'Query params after validation',
|
||||
},
|
||||
queryParams,
|
||||
queryParamSchema
|
||||
queryParamSchema,
|
||||
)
|
||||
trace.logTrace(
|
||||
'inbound',
|
||||
emojic.crayon,
|
||||
'Query params after validation',
|
||||
queryParams
|
||||
queryParams,
|
||||
)
|
||||
} catch (error) {
|
||||
serviceError = error
|
||||
@@ -421,7 +445,7 @@ class BaseService {
|
||||
try {
|
||||
serviceData = await serviceInstance.handle(
|
||||
namedParams,
|
||||
transformedQueryParams
|
||||
transformedQueryParams,
|
||||
)
|
||||
serviceInstance._validateServiceData(serviceData)
|
||||
} catch (error) {
|
||||
@@ -446,7 +470,7 @@ class BaseService {
|
||||
librariesIoApiProvider,
|
||||
metricInstance,
|
||||
},
|
||||
serviceConfig
|
||||
serviceConfig,
|
||||
) {
|
||||
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
@@ -474,14 +498,14 @@ class BaseService {
|
||||
},
|
||||
serviceConfig,
|
||||
namedParams,
|
||||
queryParams
|
||||
queryParams,
|
||||
)
|
||||
|
||||
const badgeData = coalesceBadge(
|
||||
queryParams,
|
||||
serviceData,
|
||||
this.defaultBadgeData,
|
||||
this
|
||||
this,
|
||||
)
|
||||
// The final capture group is the extension.
|
||||
const format = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
@@ -490,7 +514,7 @@ class BaseService {
|
||||
metricHandle.noteResponseSent()
|
||||
},
|
||||
cacheLength: this._cacheLength,
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ describe('BaseService', function () {
|
||||
{},
|
||||
defaultConfig,
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
{ queryParamA: '!' }
|
||||
)
|
||||
{ queryParamA: '!' },
|
||||
),
|
||||
).to.deep.equal({
|
||||
message: 'Hello namedParamA: bar.bar.bar with queryParamA: !',
|
||||
})
|
||||
@@ -85,8 +85,8 @@ describe('BaseService', function () {
|
||||
{},
|
||||
defaultConfig,
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
{ queryParamA: ['foo', 'bar'] }
|
||||
)
|
||||
{ queryParamA: ['foo', 'bar'] },
|
||||
),
|
||||
).to.deep.equal({
|
||||
color: 'red',
|
||||
isError: true,
|
||||
@@ -97,13 +97,13 @@ describe('BaseService', 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$/
|
||||
/^Route not defined for BaseService$/,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -112,13 +112,13 @@ describe('BaseService', function () {
|
||||
}
|
||||
it('Should throw if handle() is not overridden', function () {
|
||||
return expect(WithRoute.invoke({}, {}, {})).to.be.rejectedWith(
|
||||
/^Handler not implemented for WithRoute$/
|
||||
/^Handler not implemented for WithRoute$/,
|
||||
)
|
||||
})
|
||||
|
||||
it('Should throw if category is not overridden', function () {
|
||||
expect(() => BaseService.category).to.throw(
|
||||
/^Category not set for BaseService$/
|
||||
/^Category not set for BaseService$/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -135,25 +135,25 @@ describe('BaseService', function () {
|
||||
{},
|
||||
defaultConfig,
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
{ queryParamA: '!' }
|
||||
{ queryParamA: '!' },
|
||||
)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'inbound',
|
||||
sinon.match.string,
|
||||
'Service class',
|
||||
'DummyService'
|
||||
'DummyService',
|
||||
)
|
||||
expect(trace.logTrace).to.be.calledWith(
|
||||
'inbound',
|
||||
sinon.match.string,
|
||||
'Named params',
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
)
|
||||
expect(trace.logTrace).to.be.calledWith(
|
||||
'inbound',
|
||||
sinon.match.string,
|
||||
'Query params after validation',
|
||||
{ queryParamA: '!' }
|
||||
{ queryParamA: '!' },
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -171,7 +171,7 @@ describe('BaseService', function () {
|
||||
const serviceData = await LinkService.invoke(
|
||||
{},
|
||||
{ handleInternalErrors: false },
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
)
|
||||
|
||||
expect(serviceData).to.deep.equal({
|
||||
@@ -194,7 +194,7 @@ describe('BaseService', function () {
|
||||
await ThrowingService.invoke(
|
||||
{},
|
||||
{ handleInternalErrors: false },
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
@@ -212,7 +212,7 @@ describe('BaseService', function () {
|
||||
await ThrowingService.invoke(
|
||||
{},
|
||||
{ handleInternalErrors: false },
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
@@ -233,8 +233,8 @@ describe('BaseService', function () {
|
||||
await ThrowingService.invoke(
|
||||
{},
|
||||
{ handleInternalErrors: true },
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
)
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -251,7 +251,7 @@ describe('BaseService', function () {
|
||||
}
|
||||
}
|
||||
expect(
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' })
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' }),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'red',
|
||||
@@ -266,7 +266,7 @@ describe('BaseService', function () {
|
||||
}
|
||||
}
|
||||
expect(
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' })
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' }),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -281,7 +281,7 @@ describe('BaseService', function () {
|
||||
}
|
||||
}
|
||||
expect(
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' })
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' }),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -296,7 +296,7 @@ describe('BaseService', function () {
|
||||
}
|
||||
}
|
||||
expect(
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' })
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' }),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'lightgray',
|
||||
@@ -311,7 +311,7 @@ describe('BaseService', function () {
|
||||
}
|
||||
}
|
||||
expect(
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' })
|
||||
await ThrowingService.invoke({}, {}, { namedParamA: 'bar.bar.bar' }),
|
||||
).to.deep.equal({
|
||||
isError: true,
|
||||
color: 'red',
|
||||
@@ -336,7 +336,7 @@ describe('BaseService', function () {
|
||||
mockHandleRequest = sinon.spy()
|
||||
DummyService.register(
|
||||
{ camp: mockCamp, handleRequest: mockHandleRequest },
|
||||
defaultConfig
|
||||
defaultConfig,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -413,8 +413,8 @@ describe('BaseService', function () {
|
||||
expect(() =>
|
||||
DummyService._validate(
|
||||
{ requiredString: ['this', "shouldn't", 'work'] },
|
||||
dummySchema
|
||||
)
|
||||
dummySchema,
|
||||
),
|
||||
)
|
||||
.to.throw()
|
||||
.instanceof(InvalidResponse)
|
||||
@@ -436,7 +436,7 @@ describe('BaseService', function () {
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ requestFetcher },
|
||||
defaultConfig
|
||||
defaultConfig,
|
||||
)
|
||||
|
||||
const url = 'some-url'
|
||||
@@ -453,14 +453,14 @@ describe('BaseService', function () {
|
||||
`${url}?param1=foobar\n${JSON.stringify(
|
||||
{ headers: options.headers },
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
2,
|
||||
)}`,
|
||||
)
|
||||
expect(trace.logTrace).to.be.calledWithMatch(
|
||||
'fetch',
|
||||
sinon.match.string,
|
||||
'Response status code',
|
||||
200
|
||||
200,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -471,7 +471,7 @@ describe('BaseService', function () {
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ requestFetcher },
|
||||
defaultConfig
|
||||
defaultConfig,
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -504,17 +504,17 @@ describe('BaseService', function () {
|
||||
const serviceInstance =
|
||||
new DummyServiceWithServiceResponseSizeMetricEnabled(
|
||||
{ requestFetcher, metricHelper },
|
||||
defaultConfig
|
||||
defaultConfig,
|
||||
)
|
||||
|
||||
await serviceInstance._request({ url })
|
||||
|
||||
expect(await 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'
|
||||
'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'
|
||||
'service_response_bytes_bucket{le="131072",category="other",family="undefined",service="dummy_service_with_service_response_size_metric_enabled"} 1\n',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -529,13 +529,13 @@ describe('BaseService', function () {
|
||||
})
|
||||
const serviceInstance = new DummyService(
|
||||
{ requestFetcher, metricHelper },
|
||||
defaultConfig
|
||||
defaultConfig,
|
||||
)
|
||||
|
||||
await serviceInstance._request({ url })
|
||||
|
||||
expect(
|
||||
await register.getSingleMetricAsString('service_response_bytes')
|
||||
await register.getSingleMetricAsString('service_response_bytes'),
|
||||
).to.not.contain('service_response_bytes_bucket')
|
||||
})
|
||||
})
|
||||
@@ -565,8 +565,8 @@ describe('BaseService', function () {
|
||||
},
|
||||
private: { myci_pass: 'abc123' },
|
||||
},
|
||||
{ namedParamA: 'bar.bar.bar' }
|
||||
)
|
||||
{ namedParamA: 'bar.bar.bar' },
|
||||
),
|
||||
).to.deep.equal({ message: 'The CI password is abc123' })
|
||||
})
|
||||
|
||||
@@ -583,8 +583,8 @@ describe('BaseService', function () {
|
||||
},
|
||||
{
|
||||
namedParamA: 'bar.bar.bar',
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
color: 'lightgray',
|
||||
isError: true,
|
||||
|
||||
@@ -39,14 +39,14 @@ function coalesceCacheLength({
|
||||
assert(defaultCacheLengthSeconds !== undefined)
|
||||
|
||||
const cacheLength = coalesce(
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
serviceDefaultCacheLengthSeconds,
|
||||
defaultCacheLengthSeconds
|
||||
defaultCacheLengthSeconds,
|
||||
)
|
||||
|
||||
// Overrides can apply _more_ caching, but not less. Query param overriding
|
||||
// can request more overriding than service override, but not less.
|
||||
const candidateOverrides = [
|
||||
serviceOverrideCacheLengthSeconds,
|
||||
overrideCacheLengthFromQueryParams(queryParams),
|
||||
].filter(x => x !== undefined)
|
||||
|
||||
|
||||
@@ -74,12 +74,12 @@ describe('Cache header functions', function () {
|
||||
serviceDefaultCacheLengthSeconds: 900,
|
||||
serviceOverrideCacheLengthSeconds: 400,
|
||||
queryParams: {},
|
||||
}).expect(900)
|
||||
}).expect(400)
|
||||
given({
|
||||
cacheHeaderConfig,
|
||||
serviceOverrideCacheLengthSeconds: 400,
|
||||
queryParams: {},
|
||||
}).expect(777)
|
||||
}).expect(400)
|
||||
given({
|
||||
cacheHeaderConfig,
|
||||
serviceOverrideCacheLengthSeconds: 900,
|
||||
@@ -125,7 +125,7 @@ describe('Cache header functions', function () {
|
||||
|
||||
it('should set the expected Cache-Control header', function () {
|
||||
expect(res._headers['cache-control']).to.equal(
|
||||
'no-cache, no-store, must-revalidate'
|
||||
'no-cache, no-store, must-revalidate',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('Cache header functions', function () {
|
||||
|
||||
it('should set the expected Cache-Control header', function () {
|
||||
expect(res._headers['cache-control']).to.equal(
|
||||
'max-age=123, s-maxage=123'
|
||||
'max-age=123, s-maxage=123',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('Cache header functions', function () {
|
||||
it('sets the expected fields', function () {
|
||||
const expectedFields = ['date', 'cache-control', 'expires']
|
||||
expectedFields.forEach(field =>
|
||||
expect(res._headers[field]).to.equal(undefined)
|
||||
expect(res._headers[field]).to.equal(undefined),
|
||||
)
|
||||
|
||||
setCacheHeaders({
|
||||
@@ -169,7 +169,7 @@ describe('Cache header functions', function () {
|
||||
expectedFields.forEach(field =>
|
||||
expect(res._headers[field])
|
||||
.to.be.a('string')
|
||||
.and.have.lengthOf.at.least(1)
|
||||
.and.have.lengthOf.at.least(1),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -181,7 +181,7 @@ describe('Cache header functions', function () {
|
||||
|
||||
it('should set the expected Cache-Control header', function () {
|
||||
expect(res._headers['cache-control']).to.equal(
|
||||
`max-age=${24 * 3600}, s-maxage=${24 * 3600}`
|
||||
`max-age=${24 * 3600}, s-maxage=${24 * 3600}`,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -190,7 +190,7 @@ describe('Cache header functions', function () {
|
||||
expect(new Date(lastModified)).to.be.withinTime(
|
||||
// Within the last 60 seconds.
|
||||
new Date(Date.now() - 60 * 1000),
|
||||
new Date()
|
||||
new Date(),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -221,7 +221,7 @@ describe('Cache header functions', function () {
|
||||
})
|
||||
expect(serverHasBeenUpSinceResourceCached(req)).to.equal(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
context(
|
||||
'when the If-Modified-Since header is after the process started',
|
||||
@@ -233,7 +233,7 @@ describe('Cache header functions', function () {
|
||||
})
|
||||
expect(serverHasBeenUpSinceResourceCached(req)).to.equal(true)
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,19 +5,19 @@ const defaultErrorMessages = {
|
||||
429: 'rate limited by upstream service',
|
||||
}
|
||||
|
||||
export default function checkErrorResponse(errorMessages = {}) {
|
||||
export default function checkErrorResponse(httpErrors = {}) {
|
||||
return async function ({ buffer, res }) {
|
||||
let error
|
||||
errorMessages = { ...defaultErrorMessages, ...errorMessages }
|
||||
httpErrors = { ...defaultErrorMessages, ...httpErrors }
|
||||
if (res.statusCode === 404) {
|
||||
error = new NotFound({ prettyMessage: errorMessages[404] })
|
||||
error = new NotFound({ prettyMessage: httpErrors[404] })
|
||||
} else if (res.statusCode !== 200) {
|
||||
const underlying = Error(
|
||||
`Got status code ${res.statusCode} (expected 200)`
|
||||
`Got status code ${res.statusCode} (expected 200)`,
|
||||
)
|
||||
const props = { underlyingError: underlying }
|
||||
if (errorMessages[res.statusCode] !== undefined) {
|
||||
props.prettyMessage = errorMessages[res.statusCode]
|
||||
if (httpErrors[res.statusCode] !== undefined) {
|
||||
props.prettyMessage = httpErrors[res.statusCode]
|
||||
}
|
||||
if (res.statusCode >= 500) {
|
||||
error = new Inaccessible(props)
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('async error handler', function () {
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Response: Got status code 429 (expected 200)'
|
||||
'Invalid Response: Got status code 429 (expected 200)',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal('rate limited by upstream service')
|
||||
expect(e.response).to.equal(res)
|
||||
@@ -72,10 +72,10 @@ describe('async error handler', function () {
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Response: Got status code 429 (expected 200)'
|
||||
'Invalid Response: Got status code 429 (expected 200)',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal(
|
||||
"terribly sorry but that's one too many requests"
|
||||
"terribly sorry but that's one too many requests",
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -90,7 +90,7 @@ describe('async error handler', function () {
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Response: Got status code 499 (expected 200)'
|
||||
'Invalid Response: Got status code 499 (expected 200)',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal('invalid')
|
||||
expect(e.response).to.equal(res)
|
||||
@@ -106,7 +106,7 @@ describe('async error handler', function () {
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidResponse)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Response: Got status code 403 (expected 200)'
|
||||
'Invalid Response: Got status code 403 (expected 200)',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal('access denied')
|
||||
}
|
||||
@@ -122,7 +122,7 @@ describe('async error handler', function () {
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(Inaccessible)
|
||||
expect(e.message).to.equal(
|
||||
'Inaccessible: Got status code 503 (expected 200)'
|
||||
'Inaccessible: Got status code 503 (expected 200)',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal('inaccessible')
|
||||
expect(e.response).to.equal(res)
|
||||
@@ -138,7 +138,7 @@ describe('async error handler', function () {
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(Inaccessible)
|
||||
expect(e.message).to.equal(
|
||||
'Inaccessible: Got status code 500 (expected 200)'
|
||||
'Inaccessible: Got status code 500 (expected 200)',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal('server overloaded')
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function coalesceBadge(
|
||||
serviceData,
|
||||
// These two parameters were kept separate to make tests clearer.
|
||||
defaultBadgeData,
|
||||
{ category, _cacheLength: defaultCacheSeconds } = {}
|
||||
{ category, _cacheLength: defaultCacheSeconds } = {},
|
||||
) {
|
||||
// The "overrideX" naming is based on services that provide badge
|
||||
// parameters themselves, which can be overridden by a query string
|
||||
@@ -141,7 +141,7 @@ export default function coalesceBadge(
|
||||
} else {
|
||||
namedLogo = coalesce(
|
||||
serviceNamedLogo,
|
||||
style === 'social' ? defaultNamedLogo : undefined
|
||||
style === 'social' ? defaultNamedLogo : undefined,
|
||||
)
|
||||
namedLogoColor = coalesce(overrideLogoColor, serviceLogoColor)
|
||||
}
|
||||
@@ -166,13 +166,13 @@ export default function coalesceBadge(
|
||||
isError ? undefined : overrideColor,
|
||||
serviceColor,
|
||||
defaultColor,
|
||||
'lightgrey'
|
||||
'lightgrey',
|
||||
),
|
||||
labelColor: coalesce(
|
||||
// In case of an error, disregard user's color override.
|
||||
isError ? undefined : overrideLabelColor,
|
||||
serviceLabelColor,
|
||||
defaultLabelColor
|
||||
defaultLabelColor,
|
||||
),
|
||||
style,
|
||||
namedLogo,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('coalesceBadge', function () {
|
||||
|
||||
it('overrides the label', function () {
|
||||
expect(
|
||||
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {})
|
||||
coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {}),
|
||||
).to.include({ label: 'purr count' })
|
||||
})
|
||||
})
|
||||
@@ -54,11 +54,11 @@ describe('coalesceBadge', function () {
|
||||
|
||||
it('overrides the color', function () {
|
||||
expect(
|
||||
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {})
|
||||
coalesceBadge({ color: '10ADED' }, { color: 'red' }, {}),
|
||||
).to.include({ color: '10ADED' })
|
||||
// also expected for legacy name
|
||||
expect(
|
||||
coalesceBadge({ colorB: 'B0ADED' }, { color: 'red' }, {})
|
||||
coalesceBadge({ colorB: 'B0ADED' }, { color: 'red' }, {}),
|
||||
).to.include({ color: 'B0ADED' })
|
||||
})
|
||||
|
||||
@@ -68,16 +68,16 @@ describe('coalesceBadge', function () {
|
||||
coalesceBadge(
|
||||
{ color: '10ADED' },
|
||||
{ isError: true, color: 'lightgray' },
|
||||
{}
|
||||
)
|
||||
{},
|
||||
),
|
||||
).to.include({ color: 'lightgray' })
|
||||
// also expected for legacy name
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ colorB: 'B0ADED' },
|
||||
{ isError: true, color: 'lightgray' },
|
||||
{}
|
||||
)
|
||||
{},
|
||||
),
|
||||
).to.include({ color: 'lightgray' })
|
||||
})
|
||||
})
|
||||
@@ -102,11 +102,11 @@ describe('coalesceBadge', function () {
|
||||
|
||||
it('overrides the label color', function () {
|
||||
expect(
|
||||
coalesceBadge({ labelColor: '42f483' }, { color: 'green' }, {})
|
||||
coalesceBadge({ labelColor: '42f483' }, { color: 'green' }, {}),
|
||||
).to.include({ labelColor: '42f483' })
|
||||
// also expected for legacy name
|
||||
expect(
|
||||
coalesceBadge({ colorA: 'B2f483' }, { color: 'green' }, {})
|
||||
coalesceBadge({ colorA: 'B2f483' }, { color: 'green' }, {}),
|
||||
).to.include({ labelColor: 'B2f483' })
|
||||
})
|
||||
|
||||
@@ -116,8 +116,8 @@ describe('coalesceBadge', function () {
|
||||
// Scoutcamp converts numeric query params to numbers.
|
||||
{ color: 123 },
|
||||
{ color: 'green' },
|
||||
{}
|
||||
)
|
||||
{},
|
||||
),
|
||||
).to.include({ color: '123' })
|
||||
// also expected for legacy name
|
||||
expect(
|
||||
@@ -125,8 +125,8 @@ describe('coalesceBadge', function () {
|
||||
// Scoutcamp converts numeric query params to numbers.
|
||||
{ colorB: 123 },
|
||||
{ color: 'green' },
|
||||
{}
|
||||
)
|
||||
{},
|
||||
),
|
||||
).to.include({ color: '123' })
|
||||
})
|
||||
})
|
||||
@@ -140,7 +140,7 @@ describe('coalesceBadge', 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
|
||||
coalesceBadge({ style: 'social' }, {}, { namedLogo: 'appveyor' }).logo,
|
||||
).to.equal(getSimpleIcon({ name: 'appveyor' })).and.not.be.empty
|
||||
})
|
||||
|
||||
@@ -149,28 +149,28 @@ describe('coalesceBadge', function () {
|
||||
namedLogo: 'npm',
|
||||
})
|
||||
expect(coalesceBadge({}, { namedLogo: 'npm' }, {}).logo).to.equal(
|
||||
getShieldsIcon({ name: 'npm' })
|
||||
getShieldsIcon({ name: 'npm' }),
|
||||
).and.not.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named monochrome logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'dependabot', logoColor: 'blue' }, {})
|
||||
.logo
|
||||
.logo,
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.to.be.empty
|
||||
})
|
||||
|
||||
it('applies the named multicolored logo with color', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoColor: 'blue' }, {}).logo,
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.to.be
|
||||
.empty
|
||||
})
|
||||
|
||||
it('overrides the logo', function () {
|
||||
expect(
|
||||
coalesceBadge({ logo: 'npm' }, { namedLogo: 'appveyor' }, {}).logo
|
||||
coalesceBadge({ logo: 'npm' }, { namedLogo: 'appveyor' }, {}).logo,
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
@@ -179,8 +179,8 @@ describe('coalesceBadge', function () {
|
||||
coalesceBadge(
|
||||
{ logo: 'dependabot', logoColor: 'blue' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
).logo
|
||||
{},
|
||||
).logo,
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.be.empty
|
||||
})
|
||||
@@ -190,8 +190,8 @@ describe('coalesceBadge', function () {
|
||||
coalesceBadge(
|
||||
{ logo: 'npm', logoColor: 'blue' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
).logo
|
||||
{},
|
||||
).logo,
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
})
|
||||
|
||||
@@ -205,8 +205,8 @@ describe('coalesceBadge', function () {
|
||||
logoPosition: -3,
|
||||
logoWidth: 100,
|
||||
},
|
||||
{}
|
||||
).logo
|
||||
{},
|
||||
).logo,
|
||||
).to.equal(getShieldsIcon({ name: 'npm' })).and.not.be.empty
|
||||
})
|
||||
|
||||
@@ -215,8 +215,8 @@ describe('coalesceBadge', function () {
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
{ namedLogo: 'dependabot', logoColor: 'red' },
|
||||
{}
|
||||
).logo
|
||||
{},
|
||||
).logo,
|
||||
).to.equal(getShieldsIcon({ name: 'dependabot', color: 'blue' })).and.not
|
||||
.be.empty
|
||||
})
|
||||
@@ -226,8 +226,8 @@ describe('coalesceBadge', function () {
|
||||
coalesceBadge(
|
||||
{ logoColor: 'blue' },
|
||||
{ namedLogo: 'npm', logoColor: 'red' },
|
||||
{}
|
||||
).logo
|
||||
{},
|
||||
).logo,
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
})
|
||||
|
||||
@@ -235,7 +235,7 @@ describe('coalesceBadge', function () {
|
||||
it('overrides logoSvg', function () {
|
||||
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
||||
expect(coalesceBadge({ logo: 'npm' }, { logoSvg }, {}).logo).to.equal(
|
||||
getShieldsIcon({ name: 'npm' })
|
||||
getShieldsIcon({ name: 'npm' }),
|
||||
).and.not.be.empty
|
||||
})
|
||||
})
|
||||
@@ -244,7 +244,7 @@ describe('coalesceBadge', function () {
|
||||
it('overrides the logo with custom svg', function () {
|
||||
const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
|
||||
expect(
|
||||
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {})
|
||||
coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {}),
|
||||
).to.include({ logo: logoSvg })
|
||||
})
|
||||
|
||||
@@ -254,8 +254,8 @@ describe('coalesceBadge', function () {
|
||||
coalesceBadge(
|
||||
{ logo: logoSvg, logoColor: 'brightgreen' },
|
||||
{ namedLogo: 'appveyor' },
|
||||
{}
|
||||
)
|
||||
{},
|
||||
),
|
||||
).to.include({ logo: logoSvg })
|
||||
})
|
||||
})
|
||||
@@ -269,7 +269,7 @@ describe('coalesceBadge', function () {
|
||||
|
||||
it('applies the logo width', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {})
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoWidth: 275 }, {}),
|
||||
).to.include({ logoWidth: 275 })
|
||||
})
|
||||
})
|
||||
@@ -283,7 +283,7 @@ describe('coalesceBadge', function () {
|
||||
|
||||
it('applies the logo position', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {})
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {}),
|
||||
).to.include({ logoPosition: -10 })
|
||||
})
|
||||
})
|
||||
@@ -296,8 +296,8 @@ describe('coalesceBadge', function () {
|
||||
{
|
||||
link: 'https://circleci.com/workflow-run/184ef3de-4836-4805-a2e4-0ceba099f92d',
|
||||
},
|
||||
{}
|
||||
).links
|
||||
{},
|
||||
).links,
|
||||
).to.deep.equal(['https://circleci.com/gh/badges/daily-tests'])
|
||||
})
|
||||
})
|
||||
@@ -328,7 +328,7 @@ describe('coalesceBadge', function () {
|
||||
describe('Cache length', function () {
|
||||
it('overrides the cache length', function () {
|
||||
expect(
|
||||
coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {})
|
||||
coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {}),
|
||||
).to.include({ cacheLengthSeconds: 123 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ function deprecatedService(attrs) {
|
||||
const { route, name, label, category, examples, message } = Joi.attempt(
|
||||
attrs,
|
||||
attrSchema,
|
||||
`Deprecated service for ${attrs.route.base}`
|
||||
`Deprecated service for ${attrs.route.base}`,
|
||||
)
|
||||
|
||||
return class DeprecatedService extends BaseService {
|
||||
|
||||
@@ -42,6 +42,7 @@ class ShieldsRuntimeError extends Error {
|
||||
if (props.underlyingError) {
|
||||
this.stack = props.underlyingError.stack
|
||||
}
|
||||
this.cacheSeconds = props.cacheSeconds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +207,9 @@ class Deprecated extends ShieldsRuntimeError {
|
||||
* @property {string} prettyMessage User-facing error message to override the
|
||||
* value of `defaultPrettyMessage()`. This is the text that will appear on the
|
||||
* badge when we catch and render the exception (Optional)
|
||||
* @property {number} cacheSeconds Length of time to cache this error response
|
||||
* for. Defaults to the cacheLength of the service class throwing the error
|
||||
* (Optional)
|
||||
*/
|
||||
|
||||
export {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { makeFullUrl } from './route.js'
|
||||
|
||||
const optionalObjectOfKeyValues = Joi.object().pattern(
|
||||
/./,
|
||||
Joi.string().allow(null)
|
||||
Joi.string().allow(null),
|
||||
)
|
||||
|
||||
const schema = Joi.object({
|
||||
@@ -32,19 +32,19 @@ function validateExample(example, index, ServiceClass) {
|
||||
const result = Joi.attempt(
|
||||
example,
|
||||
schema,
|
||||
`Example for ${ServiceClass.name} at index ${index}`
|
||||
`Example for ${ServiceClass.name} at index ${index}`,
|
||||
)
|
||||
|
||||
const { pattern, namedParams } = result
|
||||
|
||||
if (!pattern && !ServiceClass.route.pattern) {
|
||||
throw new Error(
|
||||
`Example for ${ServiceClass.name} at index ${index} does not declare a pattern`
|
||||
`Example for ${ServiceClass.name} at index ${index} does not declare a pattern`,
|
||||
)
|
||||
}
|
||||
if (pattern === ServiceClass.route.pattern) {
|
||||
throw new Error(
|
||||
`Example for ${ServiceClass.name} at index ${index} declares a redundant pattern which should be removed`
|
||||
`Example for ${ServiceClass.name} at index ${index} declares a redundant pattern which should be removed`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ function validateExample(example, index, ServiceClass) {
|
||||
throw Error(
|
||||
`In example for ${
|
||||
ServiceClass.name
|
||||
} at index ${index}, ${e.message.toLowerCase()}`
|
||||
} at index ${index}, ${e.message.toLowerCase()}`,
|
||||
)
|
||||
}
|
||||
// Make sure there are no extra keys.
|
||||
@@ -73,8 +73,8 @@ function validateExample(example, index, ServiceClass) {
|
||||
`In example for ${
|
||||
ServiceClass.name
|
||||
} at index ${index}, namedParams contains unknown keys: ${extraKeys.join(
|
||||
', '
|
||||
)}`
|
||||
', ',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,22 +86,22 @@ function validateExample(example, index, ServiceClass) {
|
||||
`In example for ${
|
||||
ServiceClass.name
|
||||
} at index ${index}, keywords contains words that are less than two characters long: ${tinyKeywords.join(
|
||||
', '
|
||||
)}`
|
||||
', ',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
// Make sure none of the keywords are already included in the title.
|
||||
const title = (example.title || ServiceClass.name).toLowerCase()
|
||||
const redundantKeywords = example.keywords.filter(k =>
|
||||
title.includes(k.toLowerCase())
|
||||
title.includes(k.toLowerCase()),
|
||||
)
|
||||
if (redundantKeywords.length) {
|
||||
throw Error(
|
||||
`In example for ${
|
||||
ServiceClass.name
|
||||
} at index ${index}, keywords contains words that are already in the title: ${redundantKeywords.join(
|
||||
', '
|
||||
)}`
|
||||
', ',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
{},
|
||||
staticPreview,
|
||||
ServiceClass.defaultBadgeData,
|
||||
ServiceClass
|
||||
ServiceClass,
|
||||
)
|
||||
|
||||
const category = categories.find(c => c.id === ServiceClass.category)
|
||||
@@ -135,7 +135,7 @@ function transformExample(inExample, index, ServiceClass) {
|
||||
example: {
|
||||
pattern: makeFullUrl(
|
||||
ServiceClass.route.base,
|
||||
pattern || ServiceClass.route.pattern
|
||||
pattern || ServiceClass.route.pattern,
|
||||
),
|
||||
namedParams,
|
||||
queryParams,
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('validateExample function', function () {
|
||||
|
||||
validExamples.forEach(example => {
|
||||
expect(() =>
|
||||
validateExample(example, 0, { route: {}, name: 'mockService' })
|
||||
validateExample(example, 0, { route: {}, name: 'mockService' }),
|
||||
).not.to.throw(Error)
|
||||
})
|
||||
})
|
||||
@@ -66,7 +66,7 @@ describe('validateExample function', function () {
|
||||
|
||||
invalidExamples.forEach(example => {
|
||||
expect(() =>
|
||||
validateExample(example, 0, { route: {}, name: 'mockService' })
|
||||
validateExample(example, 0, { route: {}, name: 'mockService' }),
|
||||
).to.throw(Error)
|
||||
})
|
||||
})
|
||||
@@ -93,7 +93,7 @@ test(transformExample, function () {
|
||||
keywords: ['hello'],
|
||||
},
|
||||
0,
|
||||
ExampleService
|
||||
ExampleService,
|
||||
).expect({
|
||||
title: 'ExampleService',
|
||||
example: {
|
||||
@@ -119,7 +119,7 @@ test(transformExample, function () {
|
||||
keywords: ['hello'],
|
||||
},
|
||||
0,
|
||||
ExampleService
|
||||
ExampleService,
|
||||
).expect({
|
||||
title: 'ExampleService',
|
||||
example: {
|
||||
@@ -146,7 +146,7 @@ test(transformExample, function () {
|
||||
keywords: ['hello'],
|
||||
},
|
||||
0,
|
||||
ExampleService
|
||||
ExampleService,
|
||||
).expect({
|
||||
title: 'ExampleService',
|
||||
example: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
const userAgent = getUserAgent()
|
||||
|
||||
async function sendRequest(gotWrapper, url, options) {
|
||||
async function sendRequest(gotWrapper, url, options = {}, systemErrors = {}) {
|
||||
const gotOptions = Object.assign({}, options)
|
||||
gotOptions.throwHttpErrors = false
|
||||
gotOptions.retry = { limit: 0 }
|
||||
@@ -22,6 +22,12 @@ async function sendRequest(gotWrapper, url, options) {
|
||||
underlyingError: new Error('Maximum response size exceeded'),
|
||||
})
|
||||
}
|
||||
if (err.code in systemErrors) {
|
||||
throw new Inaccessible({
|
||||
...systemErrors[err.code],
|
||||
underlyingError: err,
|
||||
})
|
||||
}
|
||||
throw new Inaccessible({ underlyingError: err })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('got wrapper', function () {
|
||||
.reply(200, 'x'.repeat(101))
|
||||
const sendRequest = _fetchFactory(100)
|
||||
return expect(
|
||||
sendRequest('https://www.google.com/foo/bar')
|
||||
sendRequest('https://www.google.com/foo/bar'),
|
||||
).to.be.rejectedWith(InvalidResponse, 'Maximum response size exceeded')
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('got wrapper', function () {
|
||||
nock('https://www.google.com').get('/foo/bar').replyWithError('oh no')
|
||||
const sendRequest = _fetchFactory(1024)
|
||||
return expect(
|
||||
sendRequest('https://www.google.com/foo/bar')
|
||||
sendRequest('https://www.google.com/foo/bar'),
|
||||
).to.be.rejectedWith(Inaccessible, 'oh no')
|
||||
})
|
||||
|
||||
@@ -38,10 +38,40 @@ describe('got wrapper', function () {
|
||||
nock.disableNetConnect()
|
||||
const sendRequest = _fetchFactory(1024)
|
||||
return expect(
|
||||
sendRequest('https://www.google.com/foo/bar')
|
||||
sendRequest('https://www.google.com/foo/bar'),
|
||||
).to.be.rejectedWith(
|
||||
Inaccessible,
|
||||
'Nock: Disallowed net connect for "www.google.com:443/foo/bar"'
|
||||
'Nock: Disallowed net connect for "www.google.com:443/foo/bar"',
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw a custom error if provided', async function () {
|
||||
const sendRequest = _fetchFactory(1024)
|
||||
return (
|
||||
expect(
|
||||
sendRequest(
|
||||
'https://www.google.com/foo/bar',
|
||||
{ timeout: { request: 1 } },
|
||||
{
|
||||
ETIMEDOUT: {
|
||||
prettyMessage: 'Oh no! A terrible thing has happened',
|
||||
cacheSeconds: 10,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
.to.be.rejectedWith(
|
||||
Inaccessible,
|
||||
"Inaccessible: Timeout awaiting 'request' for 1ms",
|
||||
)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then(error => {
|
||||
expect(error).to.have.property(
|
||||
'prettyMessage',
|
||||
'Oh no! A terrible thing has happened',
|
||||
)
|
||||
expect(error).to.have.property('cacheSeconds', 10)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -9,18 +9,16 @@ describe('mergeQueries function', function () {
|
||||
it('merges valid gql queries', function () {
|
||||
expect(
|
||||
print(
|
||||
mergeQueries(
|
||||
gql`
|
||||
query ($param: String!) {
|
||||
foo(param: $param) {
|
||||
bar
|
||||
}
|
||||
mergeQueries(gql`
|
||||
query ($param: String!) {
|
||||
foo(param: $param) {
|
||||
bar
|
||||
}
|
||||
`
|
||||
)
|
||||
)
|
||||
}
|
||||
`),
|
||||
),
|
||||
).to.equalIgnoreSpaces(
|
||||
'query ($param: String!) { foo(param: $param) { bar } }'
|
||||
'query ($param: String!) { foo(param: $param) { bar } }',
|
||||
)
|
||||
|
||||
expect(
|
||||
@@ -37,11 +35,11 @@ describe('mergeQueries function', function () {
|
||||
query {
|
||||
baz
|
||||
}
|
||||
`
|
||||
)
|
||||
)
|
||||
`,
|
||||
),
|
||||
),
|
||||
).to.equalIgnoreSpaces(
|
||||
'query ($param: String!) { foo(param: $param) { bar } baz }'
|
||||
'query ($param: String!) { foo(param: $param) { bar } baz }',
|
||||
)
|
||||
|
||||
expect(
|
||||
@@ -61,9 +59,9 @@ describe('mergeQueries function', function () {
|
||||
query {
|
||||
baz
|
||||
}
|
||||
`
|
||||
)
|
||||
)
|
||||
`,
|
||||
),
|
||||
),
|
||||
).to.equalIgnoreSpaces('{ foo bar baz }')
|
||||
|
||||
expect(
|
||||
@@ -78,9 +76,9 @@ describe('mergeQueries function', function () {
|
||||
{
|
||||
bar
|
||||
}
|
||||
`
|
||||
)
|
||||
)
|
||||
`,
|
||||
),
|
||||
),
|
||||
).to.equalIgnoreSpaces('{ foo bar }')
|
||||
})
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Deprecated,
|
||||
ImproperlyConfigured,
|
||||
} from './errors.js'
|
||||
import { pathParam, pathParams, queryParam, queryParams } from './openapi.js'
|
||||
|
||||
export {
|
||||
BaseService,
|
||||
@@ -32,4 +33,8 @@ export {
|
||||
InvalidParameter,
|
||||
ImproperlyConfigured,
|
||||
Deprecated,
|
||||
pathParam,
|
||||
pathParams,
|
||||
queryParam,
|
||||
queryParams,
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
*/
|
||||
if (match[0] === '/endpoint' && Object.keys(queryParams).length === 0) {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader('Location', '/endpoint/')
|
||||
ask.res.setHeader('Location', '/badges/endpoint-badge')
|
||||
ask.res.end()
|
||||
return
|
||||
}
|
||||
@@ -73,11 +73,9 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
// `defaultCacheLengthSeconds` can be overridden by
|
||||
// `serviceDefaultCacheLengthSeconds` (either by category or on a badge-
|
||||
// by-badge basis). Then in turn that can be overridden by
|
||||
// `serviceOverrideCacheLengthSeconds` (which we expect to be used only in
|
||||
// the dynamic badge) but only if `serviceOverrideCacheLengthSeconds` is
|
||||
// longer than `serviceDefaultCacheLengthSeconds` and then the `cacheSeconds`
|
||||
// query param can also override both of those but again only if `cacheSeconds`
|
||||
// is longer.
|
||||
// `serviceOverrideCacheLengthSeconds`.
|
||||
// Then the `cacheSeconds` query param can also override both of those
|
||||
// but only if `cacheSeconds` is longer.
|
||||
//
|
||||
// When the legacy services have been rewritten, all the code in here
|
||||
// will go away, which should achieve this goal in a simpler way.
|
||||
@@ -106,7 +104,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
const badgeData = coalesceBadge(
|
||||
filteredQueryParams,
|
||||
{ label: 'vendor', message: 'unresponsive' },
|
||||
{}
|
||||
{},
|
||||
)
|
||||
const svg = makeBadge(badgeData)
|
||||
const extension = (match.slice(-1)[0] || '.svg').replace(/^\./, '')
|
||||
@@ -128,7 +126,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
|
||||
const svg = makeBadge(badgeData)
|
||||
setCacheHeadersOnResponse(ask.res, badgeData.cacheLengthSeconds)
|
||||
makeSend(format, ask.res, end)(svg)
|
||||
}
|
||||
},
|
||||
)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
if (result && result.catch) {
|
||||
|
||||
@@ -18,7 +18,7 @@ function fakeHandler(queryParams, match, sendBadge, request) {
|
||||
label: 'testing',
|
||||
message: someValue,
|
||||
},
|
||||
{}
|
||||
{},
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ function createFakeHandlerWithCacheLength(cacheLengthSeconds) {
|
||||
{},
|
||||
{
|
||||
_cacheLength: cacheLengthSeconds,
|
||||
}
|
||||
},
|
||||
)
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ describe('The request handler', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, { handler: fakeHandler })
|
||||
handleRequest(standardCacheHeaders, { handler: fakeHandler }),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('The request handler', function () {
|
||||
beforeEach(function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(standardCacheHeaders, fakeHandler)
|
||||
handleRequest(standardCacheHeaders, fakeHandler),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -119,8 +119,8 @@ describe('The request handler', function () {
|
||||
cacheHeaderConfig,
|
||||
(queryParams, match, sendBadge, request) => {
|
||||
fakeHandler(queryParams, match, sendBadge, request)
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ describe('The request handler', function () {
|
||||
register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
+new Date(headers.date) + 900000,
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
|
||||
@@ -142,13 +142,13 @@ describe('The request handler', function () {
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 900000
|
||||
+new Date(headers.date) + 900000,
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=900, s-maxage=900')
|
||||
})
|
||||
|
||||
it('should let live service data override the default cache headers with longer value', async function () {
|
||||
it('should allow serviceData to override the default cache headers with longer value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
@@ -158,17 +158,17 @@ describe('The request handler', function () {
|
||||
queryParams,
|
||||
match,
|
||||
sendBadge,
|
||||
request
|
||||
request,
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers['cache-control']).to.equal('max-age=400, s-maxage=400')
|
||||
})
|
||||
|
||||
it('should not let live service data override the default cache headers with shorter value', async function () {
|
||||
it('should allow serviceData to override the default cache headers with shorter value', async function () {
|
||||
camp.route(
|
||||
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
|
||||
handleRequest(
|
||||
@@ -178,23 +178,23 @@ describe('The request handler', function () {
|
||||
queryParams,
|
||||
match,
|
||||
sendBadge,
|
||||
request
|
||||
request,
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
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=200, s-maxage=200')
|
||||
})
|
||||
|
||||
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`
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=3600`,
|
||||
)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 3600000
|
||||
+new Date(headers.date) + 3600000,
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=3600, s-maxage=3600')
|
||||
@@ -203,10 +203,10 @@ describe('The request handler', 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`
|
||||
`${baseUrl}/testing/123.json?cacheSeconds=300`,
|
||||
)
|
||||
const expectedExpiry = new Date(
|
||||
+new Date(headers.date) + 600000
|
||||
+new Date(headers.date) + 600000,
|
||||
).toGMTString()
|
||||
expect(headers.expires).to.equal(expectedExpiry)
|
||||
expect(headers['cache-control']).to.equal('max-age=600, s-maxage=600')
|
||||
@@ -217,7 +217,7 @@ describe('The request handler', function () {
|
||||
const { headers } = await got(`${baseUrl}/testing/123.json`)
|
||||
expect(headers.expires).to.equal(headers.date)
|
||||
expect(headers['cache-control']).to.equal(
|
||||
'no-cache, no-store, must-revalidate'
|
||||
'no-cache, no-store, must-revalidate',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -234,7 +234,7 @@ describe('The request handler', function () {
|
||||
++handlerCallCount
|
||||
fakeHandler(queryParams, match, sendBadge, request)
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -242,7 +242,7 @@ describe('The request handler', function () {
|
||||
await performTwoRequests(
|
||||
baseUrl,
|
||||
'/testing/123.svg?foo=1',
|
||||
'/testing/123.svg?foo=2'
|
||||
'/testing/123.svg?foo=2',
|
||||
)
|
||||
expect(handlerCallCount).to.equal(2)
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ const serviceDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'..',
|
||||
'services'
|
||||
'services',
|
||||
)
|
||||
|
||||
function toUnixPath(path) {
|
||||
@@ -51,14 +51,14 @@ async function loadServiceClasses(servicePaths) {
|
||||
const serviceClasses = []
|
||||
for await (const servicePath of servicePaths) {
|
||||
const currentServiceClasses = Object.values(
|
||||
await import(`file://${servicePath}`)
|
||||
await import(`file://${servicePath}`),
|
||||
).flatMap(element =>
|
||||
typeof element === 'object' ? Object.values(element) : element
|
||||
typeof element === 'object' ? Object.values(element) : element,
|
||||
)
|
||||
|
||||
if (currentServiceClasses.length === 0) {
|
||||
throw new InvalidService(
|
||||
`Expected ${servicePath} to export a service or a collection of services`
|
||||
`Expected ${servicePath} to export a service or a collection of services`,
|
||||
)
|
||||
}
|
||||
currentServiceClasses.forEach(serviceClass => {
|
||||
@@ -71,7 +71,7 @@ async function loadServiceClasses(servicePaths) {
|
||||
return serviceClasses.push(serviceClass)
|
||||
}
|
||||
throw new InvalidService(
|
||||
`Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}`
|
||||
`Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -80,9 +80,21 @@ async function loadServiceClasses(servicePaths) {
|
||||
serviceClasses.map(({ name }) => name),
|
||||
{
|
||||
message: 'Duplicate service names found',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const routeSummaries = []
|
||||
serviceClasses.forEach(function (serviceClass) {
|
||||
if (serviceClass.openApi) {
|
||||
for (const route of Object.values(serviceClass.openApi)) {
|
||||
routeSummaries.push(route.get.summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
assertNamesUnique(routeSummaries, {
|
||||
message: 'Duplicate route summary found',
|
||||
})
|
||||
|
||||
return serviceClasses
|
||||
}
|
||||
|
||||
@@ -102,8 +114,8 @@ async function collectDefinitions() {
|
||||
async function loadTesters() {
|
||||
return Promise.all(
|
||||
getServicePaths('*.tester.js').map(
|
||||
async path => await import(`file://${path}`)
|
||||
)
|
||||
async path => await import(`file://${path}`),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,50 +12,56 @@ chai.use(chaiAsPromised)
|
||||
const { expect } = chai
|
||||
const fixturesDir = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'loader-test-fixtures'
|
||||
'loader-test-fixtures',
|
||||
)
|
||||
|
||||
describe('loadServiceClasses function', function () {
|
||||
it('throws if module exports empty', async function () {
|
||||
await expect(
|
||||
loadServiceClasses([path.join(fixturesDir, 'empty-undefined.fixture.js')])
|
||||
loadServiceClasses([
|
||||
path.join(fixturesDir, 'empty-undefined.fixture.js'),
|
||||
]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([path.join(fixturesDir, 'empty-array.fixture.js')])
|
||||
loadServiceClasses([path.join(fixturesDir, 'empty-array.fixture.js')]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([path.join(fixturesDir, 'empty-object.fixture.js')])
|
||||
loadServiceClasses([path.join(fixturesDir, 'empty-object.fixture.js')]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([path.join(fixturesDir, 'empty-no-export.fixture.js')])
|
||||
loadServiceClasses([
|
||||
path.join(fixturesDir, 'empty-no-export.fixture.js'),
|
||||
]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([
|
||||
path.join(fixturesDir, 'valid-array.fixture.js'),
|
||||
path.join(fixturesDir, 'valid-class.fixture.js'),
|
||||
path.join(fixturesDir, 'empty-array.fixture.js'),
|
||||
])
|
||||
]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
})
|
||||
|
||||
it('throws if module exports invalid', async function () {
|
||||
await expect(
|
||||
loadServiceClasses([path.join(fixturesDir, 'invalid-no-base.fixture.js')])
|
||||
loadServiceClasses([
|
||||
path.join(fixturesDir, 'invalid-no-base.fixture.js'),
|
||||
]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([
|
||||
path.join(fixturesDir, 'invalid-wrong-base.fixture.js'),
|
||||
])
|
||||
]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([path.join(fixturesDir, 'invalid-mixed.fixture.js')])
|
||||
loadServiceClasses([path.join(fixturesDir, 'invalid-mixed.fixture.js')]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
await expect(
|
||||
loadServiceClasses([
|
||||
path.join(fixturesDir, 'valid-array.fixture.js'),
|
||||
path.join(fixturesDir, 'valid-class.fixture.js'),
|
||||
path.join(fixturesDir, 'invalid-no-base.fixture.js'),
|
||||
])
|
||||
]),
|
||||
).to.be.rejectedWith(InvalidService)
|
||||
})
|
||||
|
||||
@@ -65,7 +71,7 @@ describe('loadServiceClasses function', function () {
|
||||
path.join(fixturesDir, 'valid-array.fixture.js'),
|
||||
path.join(fixturesDir, 'valid-object.fixture.js'),
|
||||
path.join(fixturesDir, 'valid-class.fixture.js'),
|
||||
])
|
||||
]),
|
||||
).to.eventually.have.length(5)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
const baseUrl = process.env.BASE_URL || 'https://img.shields.io'
|
||||
/**
|
||||
* Functions for publishing the shields.io URL schema as an OpenAPI Document
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
const baseUrl = process.env.BASE_URL
|
||||
const globalParamRefs = [
|
||||
{ $ref: '#/components/parameters/style' },
|
||||
{ $ref: '#/components/parameters/logo' },
|
||||
@@ -158,7 +164,7 @@ function examples2openapi(examples) {
|
||||
const parameters = [
|
||||
...pathParams,
|
||||
...Object.entries(queryParams).map(([paramName, exampleValue]) =>
|
||||
param2openapi(pattern, paramName, exampleValue, 'query')
|
||||
param2openapi(pattern, paramName, exampleValue, 'query'),
|
||||
),
|
||||
...globalParamRefs,
|
||||
]
|
||||
@@ -198,7 +204,7 @@ function services2openapi(services) {
|
||||
if (service.openApi) {
|
||||
// if the service declares its own OpenAPI definition, use that...
|
||||
for (const [key, value] of Object.entries(
|
||||
addGlobalProperties(service.openApi)
|
||||
addGlobalProperties(service.openApi),
|
||||
)) {
|
||||
if (key in paths) {
|
||||
throw new Error(`Conflicting route: ${key}`)
|
||||
@@ -208,7 +214,7 @@ function services2openapi(services) {
|
||||
} else {
|
||||
// ...otherwise do our best to build one from examples[]
|
||||
for (const [key, value] of Object.entries(
|
||||
examples2openapi(service.examples)
|
||||
examples2openapi(service.examples),
|
||||
)) {
|
||||
// allow conflicting routes for legacy examples
|
||||
paths[key] = value
|
||||
@@ -228,7 +234,7 @@ function category2openapi(category, services) {
|
||||
name: 'CC0',
|
||||
},
|
||||
},
|
||||
servers: [{ url: baseUrl }],
|
||||
servers: baseUrl ? [{ url: baseUrl }] : undefined,
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
@@ -247,7 +253,7 @@ function category2openapi(category, services) {
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository. <a href="/docs/logos">Further info</a>.',
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
@@ -332,4 +338,132 @@ function category2openapi(category, services) {
|
||||
return spec
|
||||
}
|
||||
|
||||
export { category2openapi }
|
||||
/**
|
||||
* Helper function for assembling an OpenAPI path parameter object
|
||||
*
|
||||
* @param {module:core/base-service/openapi~PathParamInput} param Input param
|
||||
* @returns {module:core/base-service/openapi~OpenApiParam} OpenAPI Parameter Object
|
||||
* @see https://swagger.io/specification/#parameter-object
|
||||
*/
|
||||
function pathParam({
|
||||
name,
|
||||
example,
|
||||
schema = { type: 'string' },
|
||||
description,
|
||||
}) {
|
||||
return { name, in: 'path', required: true, schema, example, description }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for assembling an array of OpenAPI path parameter objects
|
||||
* The code
|
||||
* ```
|
||||
* const params = pathParams(
|
||||
* { name: 'name1', example: 'example1' },
|
||||
* { name: 'name2', example: 'example2' },
|
||||
* )
|
||||
* ```
|
||||
* is equivilent to
|
||||
* ```
|
||||
* const params = [
|
||||
* pathParam({ name: 'name1', example: 'example1' }),
|
||||
* pathParam({ name: 'name2', example: 'example2' }),
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param {...module:core/base-service/openapi~PathParamInput} params Input params
|
||||
* @returns {Array.<module:core/base-service/openapi~OpenApiParam>} Array of OpenAPI Parameter Objects
|
||||
* @see {@link module:core/base-service/openapi~pathParam}
|
||||
*/
|
||||
function pathParams(...params) {
|
||||
return params.map(param => pathParam(param))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for assembling an OpenAPI query parameter object
|
||||
*
|
||||
* @param {module:core/base-service/openapi~QueryParamInput} param Input param
|
||||
* @returns {module:core/base-service/openapi~OpenApiParam} OpenAPI Parameter Object
|
||||
* @see https://swagger.io/specification/#parameter-object
|
||||
*/
|
||||
function queryParam({
|
||||
name,
|
||||
example,
|
||||
schema = { type: 'string' },
|
||||
required = false,
|
||||
description,
|
||||
}) {
|
||||
const param = { name, in: 'query', required, schema, example, description }
|
||||
if (example === null && schema.type === 'boolean') {
|
||||
param.allowEmptyValue = true
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for assembling an array of OpenAPI query parameter objects
|
||||
* The code
|
||||
* ```
|
||||
* const params = queryParams(
|
||||
* { name: 'name1', example: 'example1' },
|
||||
* { name: 'name2', example: 'example2' },
|
||||
* )
|
||||
* ```
|
||||
* is equivilent to
|
||||
* ```
|
||||
* const params = [
|
||||
* queryParam({ name: 'name1', example: 'example1' }),
|
||||
* queryParams({ name: 'name2', example: 'example2' }),
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param {...module:core/base-service/openapi~QueryParamInput} params Input params
|
||||
* @returns {Array.<module:core/base-service/openapi~OpenApiParam>} Array of OpenAPI Parameter Objects
|
||||
* @see {@link module:core/base-service/openapi~queryParam}
|
||||
*/
|
||||
function queryParams(...params) {
|
||||
return params.map(param => queryParam(param))
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} PathParamInput
|
||||
* @property {string} name The name of the parameter. Parameter names are case sensitive
|
||||
* @property {string} example Example of a valid value for this parameter
|
||||
* @property {object} [schema={ type: 'string' }] Parameter schema.
|
||||
* An [OpenAPI Schema object](https://swagger.io/specification/#schema-object)
|
||||
* specifying the parameter type.
|
||||
* Normally this should be omitted as all path parameters are strings.
|
||||
* Use this when we also want to pass an enum of valid parameters
|
||||
* to be presented as a drop-down in the frontend. e.g:
|
||||
* `{'type': 'string', 'enum': ['github', 'bitbucket'}` (Optional)
|
||||
* @property {string} description A brief description of the parameter (Optional)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} QueryParamInput
|
||||
* @property {string} name The name of the parameter. Parameter names are case sensitive
|
||||
* @property {string|null} example Example of a valid value for this parameter
|
||||
* @property {object} [schema={ type: 'string' }] Parameter schema.
|
||||
* An [OpenAPI Schema object](https://swagger.io/specification/#schema-object)
|
||||
* specifying the parameter type. This can normally be omitted.
|
||||
* Query params are usually strings. (Optional)
|
||||
* @property {boolean} [required=false] Determines whether this parameter is mandatory (Optional)
|
||||
* @property {string} description A brief description of the parameter (Optional)
|
||||
*/
|
||||
|
||||
/**
|
||||
* OpenAPI Parameter Object
|
||||
*
|
||||
* @typedef {object} OpenApiParam
|
||||
* @property {string} name The name of the parameter
|
||||
* @property {string|null} example Example of a valid value for this parameter
|
||||
* @property {('path'|'query')} in The location of the parameter
|
||||
* @property {object} schema Parameter schema.
|
||||
* An [OpenAPI Schema object](https://swagger.io/specification/#schema-object)
|
||||
* specifying the parameter type.
|
||||
* @property {boolean} required Determines whether this parameter is mandatory
|
||||
* @property {string} description A brief description of the parameter
|
||||
* @property {boolean} allowEmptyValue If true, allows the ability to pass an empty value to this parameter
|
||||
*/
|
||||
|
||||
export { category2openapi, pathParam, pathParams, queryParam, queryParams }
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import chai from 'chai'
|
||||
import { category2openapi } from './openapi.js'
|
||||
import {
|
||||
category2openapi,
|
||||
pathParam,
|
||||
pathParams,
|
||||
queryParam,
|
||||
queryParams,
|
||||
} from './openapi.js'
|
||||
import BaseJsonService from './base-json.js'
|
||||
const { expect } = chai
|
||||
|
||||
@@ -76,7 +82,6 @@ class LegacyService extends BaseJsonService {
|
||||
const expected = {
|
||||
openapi: '3.0.0',
|
||||
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
|
||||
servers: [{ url: 'https://img.shields.io' }],
|
||||
components: {
|
||||
parameters: {
|
||||
style: {
|
||||
@@ -93,7 +98,7 @@ const expected = {
|
||||
in: 'query',
|
||||
required: false,
|
||||
description:
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository.',
|
||||
'One of the named logos (bitcoin, dependabot, gitlab, npm, paypal, serverfault, stackexchange, superuser, telegram, travis) or simple-icons. All simple-icons are referenced using icon slugs. You can click the icon title on <a href="https://simpleicons.org/" rel="noopener noreferrer" target="_blank">simple-icons</a> to copy the slug or they can be found in the <a href="https://github.com/simple-icons/simple-icons/blob/master/slugs.md">slugs.md file</a> in the simple-icons repository. <a href="/docs/logos">Further info</a>.',
|
||||
schema: { type: 'string' },
|
||||
example: 'appveyor',
|
||||
},
|
||||
@@ -372,8 +377,153 @@ describe('category2openapi', function () {
|
||||
category2openapi({ name: 'build' }, [
|
||||
OpenApiService.getDefinition(),
|
||||
LegacyService.getDefinition(),
|
||||
])
|
||||
)
|
||||
]),
|
||||
),
|
||||
).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pathParam, pathParams', function () {
|
||||
it('generates a pathParam with defaults', function () {
|
||||
const input = { name: 'name', example: 'example' }
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'example',
|
||||
description: undefined,
|
||||
}
|
||||
expect(pathParam(input)).to.deep.equal(expected)
|
||||
expect(pathParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates a pathParam with custom args', function () {
|
||||
const input = {
|
||||
name: 'name',
|
||||
example: true,
|
||||
schema: { type: 'boolean' },
|
||||
description: 'long desc',
|
||||
}
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
example: true,
|
||||
description: 'long desc',
|
||||
}
|
||||
expect(pathParam(input)).to.deep.equal(expected)
|
||||
expect(pathParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates multiple pathParams', function () {
|
||||
expect(
|
||||
pathParams(
|
||||
{ name: 'name1', example: 'example1' },
|
||||
{ name: 'name2', example: 'example2' },
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
name: 'name1',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'example1',
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
example: 'example2',
|
||||
description: undefined,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryParam, queryParams', function () {
|
||||
it('generates a queryParam with defaults', function () {
|
||||
const input = { name: 'name', example: 'example' }
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: 'example',
|
||||
description: undefined,
|
||||
}
|
||||
expect(queryParam(input)).to.deep.equal(expected)
|
||||
expect(queryParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates queryParam with custom args', function () {
|
||||
const input = {
|
||||
name: 'name',
|
||||
example: 'example',
|
||||
required: true,
|
||||
description: 'long desc',
|
||||
}
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'query',
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
example: 'example',
|
||||
description: 'long desc',
|
||||
}
|
||||
expect(queryParam(input)).to.deep.equal(expected)
|
||||
expect(queryParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates a queryParam with boolean/null example', function () {
|
||||
const input = { name: 'name', example: null, schema: { type: 'boolean' } }
|
||||
const expected = {
|
||||
name: 'name',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'boolean' },
|
||||
allowEmptyValue: true,
|
||||
example: null,
|
||||
description: undefined,
|
||||
}
|
||||
expect(queryParam(input)).to.deep.equal(expected)
|
||||
expect(queryParams(input)[0]).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('generates multiple queryParams', function () {
|
||||
expect(
|
||||
queryParams(
|
||||
{ name: 'name1', example: 'example1' },
|
||||
{ name: 'name2', example: 'example2' },
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
name: 'name1',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: 'example1',
|
||||
description: undefined,
|
||||
},
|
||||
{
|
||||
name: 'name2',
|
||||
in: 'query',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
example: 'example2',
|
||||
description: undefined,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ const attrSchema = Joi.object({
|
||||
.required()
|
||||
.error(
|
||||
() =>
|
||||
'"transformPath" must be a function that transforms named params to a new path'
|
||||
'"transformPath" must be a function that transforms named params to a new path',
|
||||
),
|
||||
transformQueryParams: Joi.func().arity(1),
|
||||
dateAdded: Joi.date().required(),
|
||||
@@ -80,7 +80,7 @@ export default function redirector(attrs) {
|
||||
'inbound',
|
||||
emojic.arrowHeadingUp,
|
||||
'Redirector',
|
||||
route.base
|
||||
route.base,
|
||||
)
|
||||
trace.logTrace('inbound', emojic.ticket, 'Named params', namedParams)
|
||||
trace.logTrace('inbound', emojic.crayon, 'Query params', queryParams)
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('Redirector', function () {
|
||||
redirector({
|
||||
...attrs,
|
||||
name: 'ShinyRedirect',
|
||||
}).name
|
||||
}).name,
|
||||
).to.equal('ShinyRedirect')
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('Redirector', function () {
|
||||
|
||||
it('throws the expected error when dateAdded is missing', function () {
|
||||
expect(() =>
|
||||
redirector({ route, category, transformPath }).validateDefinition()
|
||||
redirector({ route, category, transformPath }).validateDefinition(),
|
||||
).to.throw('"dateAdded" is required')
|
||||
})
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('Redirector', function () {
|
||||
})
|
||||
ServiceClass.register(
|
||||
{ camp },
|
||||
{ rasterUrl: 'http://raster.example.test' }
|
||||
{ rasterUrl: 'http://raster.example.test' },
|
||||
)
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/very/old/service/hello-world.svg`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
@@ -114,12 +114,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/very/old/service/hello-world.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/new/service/hello-world.png'
|
||||
'http://raster.example.test/new/service/hello-world.png',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -128,12 +128,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/very/old/service/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'/new/service/hello-world.svg?color=123&style=flat-square'
|
||||
'/new/service/hello-world.svg?color=123&style=flat-square',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -142,12 +142,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/very/old/service/hello%0Dworld.svg?foobar=a%0Db`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'/new/service/hello%0Dworld.svg?foobar=a%0Db'
|
||||
'/new/service/hello%0Dworld.svg?foobar=a%0Db',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -174,12 +174,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'/new/service/hello-world.svg?token=abc123'
|
||||
'/new/service/hello-world.svg?token=abc123',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -188,12 +188,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'/new/service/hello-world.svg?color=123&style=flat-square&token=abc123'
|
||||
'/new/service/hello-world.svg?color=123&style=flat-square&token=abc123',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -202,12 +202,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/another/old/service/token/abc123/hello-world.svg?color=123&style=flat-square&token=def456`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'/new/service/hello-world.svg?color=123&style=flat-square&token=abc123'
|
||||
'/new/service/hello-world.svg?color=123&style=flat-square&token=abc123',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -229,12 +229,12 @@ describe('Redirector', function () {
|
||||
`${baseUrl}/override/service/token/abc123/hello-world.svg?style=flat-square&token=def456`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'/new/service/hello-world.svg?style=flat-square&token=def456'
|
||||
'/new/service/hello-world.svg?style=flat-square&token=def456',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ async function getCachedResource({
|
||||
}
|
||||
|
||||
const { buffer } = await checkErrorResponse({})(
|
||||
await requestFetcher(url, options)
|
||||
await requestFetcher(url, options),
|
||||
)
|
||||
|
||||
let reqData
|
||||
|
||||
@@ -52,7 +52,7 @@ function namedParamsForMatch(captureNames = [], match, ServiceClass) {
|
||||
if (captureNames.length !== captures.length) {
|
||||
throw new Error(
|
||||
`Service ${ServiceClass.name} declares incorrect number of named params ` +
|
||||
`(expected ${captures.length}, got ${captureNames.length})`
|
||||
`(expected ${captures.length}, got ${captureNames.length})`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -86,9 +86,9 @@ describe('Route helpers', function () {
|
||||
expect(() =>
|
||||
namedParamsForMatch(captureNames, regex.exec('/foo/bar/baz.svg'), {
|
||||
name: 'MyService',
|
||||
})
|
||||
}),
|
||||
).to.throw(
|
||||
'Service MyService declares incorrect number of named params (expected 2, got 1)'
|
||||
'Service MyService declares incorrect number of named params (expected 2, got 1)',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -96,14 +96,14 @@ describe('Route helpers', function () {
|
||||
expect(
|
||||
getQueryParamNames({
|
||||
queryParamSchema: Joi.object({ foo: Joi.string() }).required(),
|
||||
})
|
||||
}),
|
||||
).to.deep.equal(['foo'])
|
||||
expect(
|
||||
getQueryParamNames({
|
||||
queryParamSchema: Joi.object({ foo: Joi.string() })
|
||||
.rename('bar', 'foo', { ignoreUndefined: true, override: true })
|
||||
.required(),
|
||||
})
|
||||
}),
|
||||
).to.deep.equal(['foo', 'bar'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import Joi from '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 objectOfKeyValues = Joi.object()
|
||||
@@ -21,7 +18,7 @@ const serviceDefinition = Joi.object({
|
||||
Joi.object({
|
||||
format: Joi.string().required(),
|
||||
queryParams: arrayOfStrings,
|
||||
})
|
||||
}),
|
||||
),
|
||||
examples: Joi.array()
|
||||
.items(
|
||||
@@ -43,7 +40,7 @@ const serviceDefinition = Joi.object({
|
||||
documentation: Joi.object({
|
||||
__html: Joi.string().required(), // Valid HTML.
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
openApi: Joi.object().pattern(
|
||||
@@ -51,7 +48,7 @@ const serviceDefinition = Joi.object({
|
||||
Joi.object({
|
||||
get: Joi.object({
|
||||
summary: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
parameters: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
@@ -59,19 +56,23 @@ const serviceDefinition = Joi.object({
|
||||
description: Joi.string(),
|
||||
in: Joi.string().valid('query', 'path').required(),
|
||||
required: Joi.boolean().required(),
|
||||
schema: Joi.object({ type: Joi.string().required() }).required(),
|
||||
example: Joi.string(),
|
||||
})
|
||||
schema: Joi.object({
|
||||
type: Joi.string().required(),
|
||||
enum: Joi.array(),
|
||||
}).required(),
|
||||
allowEmptyValue: Joi.boolean(),
|
||||
example: Joi.string().allow(null),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
}).required(),
|
||||
),
|
||||
}).required()
|
||||
|
||||
function assertValidServiceDefinition(example, message = undefined) {
|
||||
Joi.assert(example, serviceDefinition, message)
|
||||
function assertValidServiceDefinition(service, message = undefined) {
|
||||
Joi.assert(service, serviceDefinition, message)
|
||||
}
|
||||
|
||||
const serviceDefinitionExport = Joi.object({
|
||||
@@ -82,7 +83,7 @@ const serviceDefinitionExport = Joi.object({
|
||||
id: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
keywords: arrayOfStrings,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.required(),
|
||||
services: Joi.array().items(serviceDefinition).required(),
|
||||
@@ -92,9 +93,4 @@ function assertValidServiceDefinitionExport(examples, message = undefined) {
|
||||
Joi.assert(examples, serviceDefinitionExport, message)
|
||||
}
|
||||
|
||||
export {
|
||||
serviceDefinition,
|
||||
assertValidServiceDefinition,
|
||||
serviceDefinitionExport,
|
||||
assertValidServiceDefinitionExport,
|
||||
}
|
||||
export { assertValidServiceDefinition, assertValidServiceDefinitionExport }
|
||||
|
||||
@@ -12,7 +12,7 @@ function validate(
|
||||
allowAndStripUnknownKeys = true,
|
||||
},
|
||||
data,
|
||||
schema
|
||||
schema,
|
||||
) {
|
||||
if (!schema || !Joi.isSchema(schema)) {
|
||||
throw Error('A Joi schema is required')
|
||||
@@ -28,7 +28,7 @@ function validate(
|
||||
'validate',
|
||||
emojic.womanShrugging,
|
||||
traceErrorMessage,
|
||||
error.message
|
||||
error.message,
|
||||
)
|
||||
|
||||
let prettyMessage = prettyErrorMessage
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('validate', function () {
|
||||
sinon.match.string,
|
||||
traceSuccessMessage,
|
||||
{ requiredString: 'bar' },
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -60,13 +60,13 @@ describe('validate', function () {
|
||||
validate(
|
||||
options,
|
||||
{ requiredString: ['this', "shouldn't", 'work'] },
|
||||
schema
|
||||
schema,
|
||||
)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidParameter)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Parameter: "requiredString" must be a string'
|
||||
'Invalid Parameter: "requiredString" must be a string',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal(prettyErrorMessage)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ describe('validate', function () {
|
||||
'validate',
|
||||
sinon.match.string,
|
||||
traceErrorMessage,
|
||||
'"requiredString" must be a string'
|
||||
'"requiredString" must be a string',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -86,16 +86,16 @@ describe('validate', function () {
|
||||
{
|
||||
requiredString: ['this', "shouldn't", 'work'],
|
||||
},
|
||||
schema
|
||||
schema,
|
||||
)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidParameter)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Parameter: "requiredString" must be a string'
|
||||
'Invalid Parameter: "requiredString" must be a string',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal(
|
||||
`${prettyErrorMessage}: requiredString`
|
||||
`${prettyErrorMessage}: requiredString`,
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -107,13 +107,13 @@ describe('validate', function () {
|
||||
validate(
|
||||
{ ...options, allowAndStripUnknownKeys: false, includeKeys: true },
|
||||
{ requiredString: 'bar', extra: 'nonsense', more: 'bogus' },
|
||||
schema
|
||||
schema,
|
||||
)
|
||||
expect.fail('Expected to throw')
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceof(InvalidParameter)
|
||||
expect(e.message).to.equal(
|
||||
'Invalid Parameter: "extra" is not allowed. "more" is not allowed'
|
||||
'Invalid Parameter: "extra" is not allowed. "more" is not allowed',
|
||||
)
|
||||
expect(e.prettyMessage).to.equal(`${prettyErrorMessage}: extra, more`)
|
||||
}
|
||||
|
||||
@@ -27,14 +27,16 @@ export default class InfluxMetrics {
|
||||
response = await got.post(request)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
new Error(`Cannot push metrics. Cause: ${error.name}: ${error.message}`)
|
||||
new Error(
|
||||
`Cannot push metrics. Cause: ${error.name}: ${error.message}`,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (response && response.statusCode >= 300) {
|
||||
log.error(
|
||||
new Error(
|
||||
`Cannot push metrics. ${request.url} responded with status code ${response.statusCode}`
|
||||
)
|
||||
`Cannot push metrics. ${request.url} responded with status code ${response.statusCode}`,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,7 +44,7 @@ export default class InfluxMetrics {
|
||||
startPushingMetrics() {
|
||||
this._intervalId = setInterval(
|
||||
() => this.sendMetrics(),
|
||||
this._config.intervalSeconds * 1000
|
||||
this._config.intervalSeconds * 1000,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('Influx metrics', function () {
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(await influxMetrics.metrics()).to.be.contain(
|
||||
'instance=test-hostname'
|
||||
'instance=test-hostname',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Influx metrics', function () {
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(await influxMetrics.metrics()).to.be.contain(
|
||||
'instance=test-hostname-alias'
|
||||
'instance=test-hostname-alias',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -96,7 +96,7 @@ describe('Influx metrics', function () {
|
||||
.persist()
|
||||
.post(
|
||||
'/metrics',
|
||||
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11'
|
||||
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11',
|
||||
)
|
||||
.basicAuth({ user: 'metrics-username', pass: 'metrics-password' })
|
||||
.reply(200)
|
||||
@@ -117,7 +117,7 @@ describe('Influx metrics', function () {
|
||||
await clock.tickAsync(10)
|
||||
expect(scope.isDone()).to.be.equal(
|
||||
true,
|
||||
`pending mocks: ${scope.pendingMocks()}`
|
||||
`pending mocks: ${scope.pendingMocks()}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -150,9 +150,9 @@ describe('Influx metrics', function () {
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. Cause: RequestError: Nock: Disallowed net connect for "shields-metrics.io:443/metrics"'
|
||||
)
|
||||
)
|
||||
'Cannot push metrics. Cause: RequestError: Nock: Disallowed net connect for "shields-metrics.io:443/metrics"',
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -167,9 +167,9 @@ describe('Influx metrics', function () {
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. https://shields-metrics.io/metrics responded with status code 400'
|
||||
)
|
||||
)
|
||||
'Cannot push metrics. https://shields-metrics.io/metrics responded with status code 400',
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
|
||||
return metrics
|
||||
.flatMap(metric => {
|
||||
const valuesByLabels = groupBy(metric.values, value =>
|
||||
JSON.stringify(Object.entries(value.labels).sort())
|
||||
JSON.stringify(Object.entries(value.labels).sort()),
|
||||
)
|
||||
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
|
||||
const labels = Object.entries(metricsWithSameLabel[0].labels)
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('Metric format converters', function () {
|
||||
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`)
|
||||
prometheus histogram1_count=3,histogram1_sum=111`),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
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`)
|
||||
prometheus histogram1_count=3,histogram1_sum=111`),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -145,7 +145,7 @@ prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
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`)
|
||||
prometheus summary1_count=3,summary1_sum=111`),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -167,7 +167,7 @@ prometheus summary1_count=3,summary1_sum=111`)
|
||||
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`)
|
||||
prometheus summary1_count=3,summary1_sum=111`),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -204,7 +204,7 @@ prometheus summary1_count=3,summary1_sum=111`)
|
||||
})
|
||||
|
||||
expect(influx).to.be.equal(
|
||||
'prometheus,env=production,instance=instance1 counter1=11'
|
||||
'prometheus,env=production,instance=instance1 counter1=11',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -79,7 +79,7 @@ export default class PrometheusMetrics {
|
||||
return this.counters.serviceResponseSize.labels(
|
||||
category,
|
||||
serviceFamily,
|
||||
service
|
||||
service,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ const publicConfigSchema = Joi.object({
|
||||
bind: {
|
||||
port: Joi.alternatives().try(
|
||||
Joi.number().port(),
|
||||
Joi.string().pattern(/^\\\\\.\\pipe\\.+$/)
|
||||
Joi.string().pattern(/^\\\\\.\\pipe\\.+$/),
|
||||
),
|
||||
address: Joi.alternatives().try(
|
||||
Joi.string().ip().required(),
|
||||
Joi.string().hostname().required()
|
||||
Joi.string().hostname().required(),
|
||||
),
|
||||
},
|
||||
metrics: {
|
||||
@@ -154,8 +154,8 @@ const publicConfigSchema = Joi.object({
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'..',
|
||||
'public'
|
||||
)
|
||||
'public',
|
||||
),
|
||||
),
|
||||
requireCloudflare: Joi.boolean().required(),
|
||||
}).required()
|
||||
@@ -236,7 +236,7 @@ class Server {
|
||||
const publicConfig = Joi.attempt(config.public, publicConfigSchema)
|
||||
const privateConfig = this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateConfigSchema
|
||||
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
|
||||
@@ -245,7 +245,7 @@ class Server {
|
||||
if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
|
||||
this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateMetricsInfluxConfigSchema
|
||||
privateMetricsInfluxConfigSchema,
|
||||
)
|
||||
}
|
||||
this.config = {
|
||||
@@ -270,7 +270,7 @@ class Server {
|
||||
Object.assign({}, publicConfig.metrics.influx, {
|
||||
username: privateConfig.influx_username,
|
||||
password: privateConfig.influx_password,
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -283,8 +283,8 @@ class Server {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
',',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -350,32 +350,35 @@ class Server {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
end,
|
||||
)(
|
||||
makeBadge({
|
||||
label: '410',
|
||||
message: `${format} no longer available`,
|
||||
color: 'lightgray',
|
||||
format: 'svg',
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
if (!rasterUrl) {
|
||||
camp.route(/\.png$/, (query, match, end, request) => {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end
|
||||
)(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'raster badges not available',
|
||||
color: 'lightgray',
|
||||
format: 'svg',
|
||||
})
|
||||
)
|
||||
})
|
||||
camp.route(
|
||||
/^\/((?!img|assets\/)).*\.png$/,
|
||||
(query, match, end, request) => {
|
||||
makeSend(
|
||||
'svg',
|
||||
request.res,
|
||||
end,
|
||||
)(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'raster badges not available',
|
||||
color: 'lightgray',
|
||||
format: 'svg',
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
|
||||
@@ -385,14 +388,14 @@ class Server {
|
||||
makeSend(
|
||||
format,
|
||||
request.res,
|
||||
end
|
||||
end,
|
||||
)(
|
||||
makeBadge({
|
||||
label: '404',
|
||||
message: 'badge not found',
|
||||
color: 'red',
|
||||
format,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -412,18 +415,21 @@ class Server {
|
||||
|
||||
if (rasterUrl) {
|
||||
// Redirect to the raster server for raster versions of modern badges.
|
||||
camp.route(/\.png$/, (queryParams, match, end, ask) => {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
rasterRedirectUrl({ rasterUrl }, ask.req.url)
|
||||
)
|
||||
camp.route(
|
||||
/^\/((?!img|assets\/)).*\.png$/,
|
||||
(queryParams, match, end, ask) => {
|
||||
ask.res.statusCode = 301
|
||||
ask.res.setHeader(
|
||||
'Location',
|
||||
rasterRedirectUrl({ rasterUrl }, ask.req.url),
|
||||
)
|
||||
|
||||
const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
|
||||
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
|
||||
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
|
||||
|
||||
ask.res.end()
|
||||
})
|
||||
ask.res.end()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
@@ -459,8 +465,8 @@ class Server {
|
||||
rasterUrl: config.public.rasterUrl,
|
||||
private: config.private,
|
||||
public: config.public,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('The server', function () {
|
||||
public: {
|
||||
documentRoot: path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'test-public'
|
||||
'test-public',
|
||||
),
|
||||
},
|
||||
})
|
||||
@@ -66,7 +66,7 @@ describe('The server', function () {
|
||||
|
||||
it('should return cors header for the request', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}badge/foo-bar-blue.svg`
|
||||
`${baseUrl}badge/foo-bar-blue.svg`,
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['access-control-allow-origin']).to.equal('*')
|
||||
@@ -77,11 +77,11 @@ describe('The server', function () {
|
||||
`${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'
|
||||
'http://raster.example.test/:fruit-apple-green.png',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -90,17 +90,22 @@ describe('The server', function () {
|
||||
`${baseUrl}badge/foo-bar-blue.png`,
|
||||
{
|
||||
followRedirect: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/badge/foo-bar-blue.png'
|
||||
'http://raster.example.test/badge/foo-bar-blue.png',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not redirect for PNG requests in /img', async function () {
|
||||
const { statusCode } = await got(`${baseUrl}img/frontend-image.png`)
|
||||
expect(statusCode).to.equal(200)
|
||||
})
|
||||
|
||||
it('should produce SVG badges with expected headers', async function () {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg`
|
||||
`${baseUrl}:fruit-apple-green.svg`,
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('image/svg+xml;charset=utf-8')
|
||||
@@ -114,7 +119,7 @@ describe('The server', function () {
|
||||
|
||||
it('should produce JSON badges with expected headers', async function () {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.json`
|
||||
`${baseUrl}:fruit-apple-green.json`,
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
@@ -132,7 +137,7 @@ describe('The server', function () {
|
||||
// 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`
|
||||
`${baseUrl}:fruit-apple-green.svg?logo=1`,
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
@@ -143,7 +148,7 @@ describe('The server', function () {
|
||||
|
||||
it('should not crash with a numeric link', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=1`
|
||||
`${baseUrl}:fruit-apple-green.svg?link=1`,
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
@@ -154,7 +159,7 @@ describe('The server', function () {
|
||||
|
||||
it('should not crash with a boolean link', async function () {
|
||||
const { statusCode, body } = await got(
|
||||
`${baseUrl}:fruit-apple-green.svg?link=true`
|
||||
`${baseUrl}:fruit-apple-green.svg?link=true`,
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(body)
|
||||
@@ -168,7 +173,7 @@ describe('The server', function () {
|
||||
`${baseUrl}this/is/not/a/badge.svg`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
@@ -182,7 +187,7 @@ describe('The server', function () {
|
||||
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
expect(statusCode).to.equal(404)
|
||||
expect(body)
|
||||
@@ -206,7 +211,7 @@ describe('The server', function () {
|
||||
`${baseUrl}badge/foo-bar-blue.jpg`,
|
||||
{
|
||||
throwHttpErrors: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
// TODO It would be nice if this were 404 or 410.
|
||||
expect(statusCode).to.equal(200)
|
||||
@@ -231,7 +236,7 @@ describe('The server', function () {
|
||||
await server.start()
|
||||
|
||||
const { statusCode, body } = await got(
|
||||
`${server.baseUrl}badge/foo-bar-blue.svg`
|
||||
`${server.baseUrl}badge/foo-bar-blue.svg`,
|
||||
)
|
||||
|
||||
expect(statusCode).to.be.equal(200)
|
||||
@@ -360,7 +365,7 @@ describe('The server', function () {
|
||||
it('should require url when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.url
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.url" is required'
|
||||
'"metrics.influx.url" is required',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -373,21 +378,21 @@ describe('The server', function () {
|
||||
it('should require timeoutMilliseconds when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.timeoutMilliseconds
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.timeoutMilliseconds" is required'
|
||||
'"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'
|
||||
'"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'
|
||||
'"metrics.influx.instanceIdFrom" is required',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -395,7 +400,7 @@ describe('The server', 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'
|
||||
'"metrics.influx.instanceIdEnvVarName" is required',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -417,7 +422,7 @@ describe('The server', function () {
|
||||
it('should require envLabel when influx configuration is enabled', function () {
|
||||
delete customConfig.public.metrics.influx.envLabel
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'"metrics.influx.envLabel" is required'
|
||||
'"metrics.influx.envLabel" is required',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -434,14 +439,14 @@ describe('The server', function () {
|
||||
it('should require username when influx configuration is enabled', function () {
|
||||
delete customConfig.private.influx_username
|
||||
expect(() => new Server(customConfig)).to.throw(
|
||||
'Private configuration is invalid. Check these paths: influx_username'
|
||||
'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'
|
||||
'Private configuration is invalid. Check these paths: influx_password',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -500,7 +505,7 @@ describe('The server', function () {
|
||||
})
|
||||
.post(
|
||||
'/metrics',
|
||||
/prometheus,application=shields,category=static,env=localhost-env,family=static-badge,instance=test-instance,service=static_badge service_requests_total=1\n/
|
||||
/prometheus,application=shields,category=static,env=localhost-env,family=static-badge,instance=test-instance,service=static_badge service_requests_total=1\n/,
|
||||
)
|
||||
.basicAuth({ user: 'influx-username', pass: 'influx-password' })
|
||||
.reply(200)
|
||||
@@ -510,7 +515,7 @@ describe('The server', function () {
|
||||
|
||||
expect(scope.isDone()).to.be.equal(
|
||||
true,
|
||||
`pending mocks: ${scope.pendingMocks()}`
|
||||
`pending mocks: ${scope.pendingMocks()}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
BIN
core/server/test-public/img/frontend-image.png
Normal file
BIN
core/server/test-public/img/frontend-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -121,8 +121,8 @@ if (typeof onlyServices === 'undefined' || onlyServices.includes('*****')) {
|
||||
} else {
|
||||
console.info(
|
||||
`Running tests for ${onlyServices.length} services: ${onlyServices.join(
|
||||
', '
|
||||
)}.\n`
|
||||
', ',
|
||||
)}.\n`,
|
||||
)
|
||||
runner.only(onlyServices)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ async function createServiceTester() {
|
||||
const ServiceClass = Object.values(await import(servicePath))[0]
|
||||
if (!(ServiceClass.prototype instanceof BaseService)) {
|
||||
throw Error(
|
||||
`${servicePath} does not export a single service. Invoke new ServiceTester() directly.`
|
||||
`${servicePath} does not export a single service. Invoke new ServiceTester() directly.`,
|
||||
)
|
||||
}
|
||||
return ServiceTester.forServiceClass(ServiceClass)
|
||||
|
||||
@@ -91,11 +91,11 @@ const factory = superclass =>
|
||||
Joi.attempt(
|
||||
json[name],
|
||||
Joi.string().regex(expected),
|
||||
`${name} mismatch:`
|
||||
`${name} mismatch:`,
|
||||
)
|
||||
} else {
|
||||
throw new Error(
|
||||
"'expected' must be a string, a number, a regex, an array or a Joi schema"
|
||||
"'expected' must be a string, a number, a regex, an array or a Joi schema",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class Runner {
|
||||
*/
|
||||
async prepare() {
|
||||
this.testers = (await loadTesters()).flatMap(testerModule =>
|
||||
Object.values(testerModule)
|
||||
Object.values(testerModule),
|
||||
)
|
||||
this.testers.forEach(tester => {
|
||||
tester.beforeEach = () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('Services from PR title', function () {
|
||||
])
|
||||
given('[CRAN CPAN CTAN] Add test coverage').expect(['cran', 'cpan', 'ctan'])
|
||||
given(
|
||||
'[RFC] Add Joi-based request validation to BaseJsonService and rewrite [NPM] badges'
|
||||
'[RFC] Add Joi-based request validation to BaseJsonService and rewrite [NPM] badges',
|
||||
).expect(['npm'])
|
||||
given('make changes to [CRAN] and [CPAN]').expect(['cran', 'cpan'])
|
||||
given('[github appveyor ]').expect(['github', 'appveyor'])
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('SQL token persistence', function () {
|
||||
|
||||
beforeEach('Create temporary table', async function () {
|
||||
await pool.query(
|
||||
`CREATE TEMPORARY TABLE ${tableName} (LIKE github_user_tokens INCLUDING ALL);`
|
||||
`CREATE TEMPORARY TABLE ${tableName} (LIKE github_user_tokens INCLUDING ALL);`,
|
||||
)
|
||||
})
|
||||
afterEach('Drop temporary table', async function () {
|
||||
@@ -57,7 +57,7 @@ describe('SQL token persistence', function () {
|
||||
initialTokens.forEach(async token => {
|
||||
await pool.query(
|
||||
`INSERT INTO pg_temp.${tableName} (token) VALUES ($1::text);`,
|
||||
[token]
|
||||
[token],
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -77,7 +77,7 @@ describe('SQL token persistence', function () {
|
||||
await persistence.noteTokenAdded(newToken)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
`SELECT token FROM pg_temp.${tableName};`,
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
@@ -93,7 +93,7 @@ describe('SQL token persistence', function () {
|
||||
await persistence.noteTokenRemoved(toRemove)
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT token FROM pg_temp.${tableName};`
|
||||
`SELECT token FROM pg_temp.${tableName};`,
|
||||
)
|
||||
const savedTokens = result.rows.map(row => row.token)
|
||||
expect(savedTokens.sort()).to.deep.equal(expected)
|
||||
|
||||
@@ -28,14 +28,14 @@ export default class SqlTokenPersistence {
|
||||
async onTokenAdded(token) {
|
||||
return await this.pool.query(
|
||||
`INSERT INTO ${this.table} (token) VALUES ($1::text) ON CONFLICT (token) DO NOTHING;`,
|
||||
[token]
|
||||
[token],
|
||||
)
|
||||
}
|
||||
|
||||
async onTokenRemoved(token) {
|
||||
return await this.pool.query(
|
||||
`DELETE FROM ${this.table} WHERE token=$1::text;`,
|
||||
[token]
|
||||
[token],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,16 +21,16 @@ describe('The token pool', function () {
|
||||
|
||||
it('should yield the expected tokens', function () {
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id)),
|
||||
)
|
||||
})
|
||||
|
||||
it('should repeat when reaching the end', function () {
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id)),
|
||||
)
|
||||
ids.forEach(id =>
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id))
|
||||
times(batchSize, () => expect(tokenPool.next().id).to.equal(id)),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -2,16 +2,9 @@ import { registerCommand } from 'cypress-wait-for-stable-dom'
|
||||
|
||||
registerCommand()
|
||||
|
||||
describe('Main page', function () {
|
||||
describe('Frontend', function () {
|
||||
const backendUrl = Cypress.env('backend_url')
|
||||
const SEARCH_INPUT = 'input[placeholder="search"]'
|
||||
|
||||
function expectBadgeExample(title, previewUrl, pattern) {
|
||||
cy.contains('tr', `${title}:`).find('code').should('have.text', pattern)
|
||||
cy.contains('tr', `${title}:`)
|
||||
.find('img')
|
||||
.should('have.attr', 'src', previewUrl)
|
||||
}
|
||||
const SEARCH_INPUT = 'input[placeholder="Search"]'
|
||||
|
||||
function visitAndWait(page) {
|
||||
cy.visit(page)
|
||||
@@ -26,36 +19,37 @@ describe('Main page', function () {
|
||||
cy.contains('PyPI - License')
|
||||
})
|
||||
|
||||
it('Shows badge from category', function () {
|
||||
visitAndWait('/category/chat')
|
||||
it('Shows badges from category', function () {
|
||||
visitAndWait('/badges')
|
||||
|
||||
expectBadgeExample(
|
||||
'Discourse status',
|
||||
'http://localhost:8080/badge/discourse-online-brightgreen',
|
||||
'/discourse/status?server=https%3A%2F%2Fmeta.discourse.org'
|
||||
)
|
||||
cy.contains('Build')
|
||||
cy.contains('Chat').click()
|
||||
|
||||
cy.contains('Discourse status')
|
||||
cy.contains('Stack Exchange questions')
|
||||
})
|
||||
|
||||
it('Customizate badges', function () {
|
||||
visitAndWait('/')
|
||||
it('Shows expected code examples', function () {
|
||||
visitAndWait('/badges/static-badge')
|
||||
|
||||
cy.get(SEARCH_INPUT).type('issues')
|
||||
|
||||
cy.contains('/github/issues/:user/:repo').click()
|
||||
|
||||
cy.get('input[name="user"]').type('badges')
|
||||
cy.get('input[name="repo"]').type('shields')
|
||||
cy.get('table input[name="color"]').type('orange')
|
||||
|
||||
cy.get(`img[src='${backendUrl}/github/issues/badges/shields?color=orange']`)
|
||||
cy.contains('button', 'URL').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'Markdown').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'rSt').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'AsciiDoc').should('have.class', 'api-code-tab')
|
||||
cy.contains('button', 'HTML').should('have.class', 'api-code-tab')
|
||||
})
|
||||
|
||||
it('Do not duplicate example parameters', function () {
|
||||
visitAndWait('/category/funding')
|
||||
it('Build a badge', function () {
|
||||
visitAndWait('/badges/git-hub-issues')
|
||||
|
||||
cy.contains('GitHub Sponsors').click()
|
||||
cy.get('[name="style"]').should($style => {
|
||||
expect($style).to.have.length(1)
|
||||
})
|
||||
cy.contains('/github/issues/:user/:repo')
|
||||
|
||||
cy.get('input[placeholder="user"]').type('badges')
|
||||
cy.get('input[placeholder="repo"]').type('shields')
|
||||
|
||||
cy.intercept('GET', `${backendUrl}/github/issues/badges/shields`).as('get')
|
||||
cy.contains('Execute').click()
|
||||
cy.wait('@get').its('response.statusCode').should('eq', 200)
|
||||
cy.get('img[id="badge-preview"]')
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user