Compare commits

..

29 Commits

Author SHA1 Message Date
chris48s
b0ef5046d0 use credentials if available 2023-01-15 12:36:30 +00:00
chris48s
f60c2058fa fix PAT check 2023-01-15 11:32:01 +00:00
chris48s
39e4d9bcbc rename GithubGist to Gist 2023-01-15 11:22:41 +00:00
chris48s
2bf863fb09 use a PAT if available 2023-01-12 20:40:49 +00:00
chris48s
ed277d4e79 give token contents:read 2023-01-11 20:33:03 +00:00
chris48s
fdc81c2e3a give token gist:read 2023-01-11 20:24:50 +00:00
chris48s
6ef9dcaba5 use workflow token for github service tests 2023-01-11 20:05:58 +00:00
chris48s
541cb9acf2 update triggers 2023-01-11 19:53:56 +00:00
chris48s
2492ff79f7 Merge branch 'master' into services-gha 2023-01-11 19:42:32 +00:00
chris48s
9c692cd53a fail + report if tests are pending 2022-09-19 18:25:51 +01:00
chris48s
7410bf2e97 do nothing 2022-09-19 18:16:02 +01:00
chris48s
a83cfa4fb6 do nothing 2022-09-19 18:11:29 +01:00
chris48s
4d64969738 do nothing 2022-09-19 18:08:14 +01:00
chris48s
ade213c6d3 pass PR title safely 2022-09-19 17:26:14 +01:00
chris48s
1135fba9f6 remove spurious debug 2022-09-19 17:09:09 +01:00
chris48s
0234fb077f improve report 2022-09-19 17:04:57 +01:00
chris48s
ed86c1de21 debug statement 2022-09-19 17:00:53 +01:00
chris48s
a47f770c82 better summary 2022-09-19 16:30:45 +01:00
chris48s
3176d6f7f3 Revert "break some stuff on purpose"
This reverts commit 0763d8ec66.
2022-09-19 16:20:00 +01:00
chris48s
0763d8ec66 break some stuff on purpose 2022-09-19 16:14:17 +01:00
chris48s
3fcb959ed2 single-quote the PR title 2022-09-19 16:08:42 +01:00
chris48s
f562dfe868 Revert "pass the PR title from the workflow to the action"
This reverts commit 22aee48544.
2022-09-19 16:06:44 +01:00
chris48s
22aee48544 pass the PR title from the workflow to the action 2022-09-19 16:02:56 +01:00
chris48s
098a24cae5 do nothing 2022-09-19 15:56:46 +01:00
chris48s
a2c8ed27b8 only run summary if there were any tests to run 2022-09-19 15:35:51 +01:00
chris48s
2dccd3d040 only run summary if there were any tests to run 2022-09-19 15:27:14 +01:00
chris48s
b8ce38a041 fix titles 2022-09-19 15:25:01 +01:00
chris48s
0784a14153 fix runs-on 2022-09-19 15:21:11 +01:00
chris48s
afc7b283bc migrate service tests to GH actions 2022-09-19 15:16:09 +01:00
808 changed files with 46981 additions and 22157 deletions

3
.circleci/config.yml Normal file
View File

@@ -0,0 +1,3 @@
version: 2
# Do nothing
# TODO: disable Circle

View File

@@ -3,7 +3,6 @@ shields.env
.git/
.gitignore
.vscode/
fly.toml
# Improve layer cacheability.
Dockerfile

View File

@@ -2,6 +2,7 @@ extends:
- standard
- standard-jsx
- standard-react
- plugin:@typescript-eslint/recommended
- prettier
- eslint:recommended
@@ -17,13 +18,12 @@ settings:
react:
version: '16.8'
jsdoc:
mode: typescript
mode: jsdoc
plugins:
- chai-friendly
- jsdoc
- mocha
- icedfrisby
- no-extension-in-require
- sort-class-members
- import
@@ -35,34 +35,40 @@ overrides:
# list of rules, even if they only apply to certain files. That way the
# rules listed here are only ones which conflict.
- files:
- 'badge-maker/**/*.js'
- '**/*.cjs'
env:
node: true
es6: true
- files:
- '**/*.js'
- '!frontend/**/*.js'
- '!badge-maker/**/*.js'
env:
node: true
es6: true
rules:
no-console: 'off'
'@typescript-eslint/explicit-module-boundary-types': 'off'
- files:
- '**/*.@(ts|tsx)'
parserOptions:
sourceType: 'module'
parser: '@typescript-eslint/parser'
rules:
no-console: 'off'
# 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:
- '**/*.ts'
- core/**/*.ts
parserOptions:
sourceType: 'module'
parser: '@typescript-eslint/parser'
- files:
- 'frontend/**/*.js'
- gatsby-browser.js
- 'frontend/**/*.@(js|ts|tsx)'
parserOptions:
sourceType: 'module'
env:
@@ -107,20 +113,21 @@ overrides:
mocha: true
rules:
mocha/no-exclusive-tests: 'error'
mocha/no-skipped-tests: 'error'
mocha/no-mocha-arrows: 'error'
mocha/prefer-arrow-callback: 'error'
- files:
- 'services/**/*.tester.js'
rules:
icedfrisby/no-exclusive-tests: 'error'
icedfrisby/no-skipped-tests: 'error'
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.
@@ -171,7 +178,7 @@ rules:
jsdoc/check-tag-names: 'error'
jsdoc/check-types: 'error'
jsdoc/implements-on-classes: 'error'
jsdoc/tag-lines: ['error', 'any', { 'startLines': 1 }]
jsdoc/newline-after-description: 'error'
jsdoc/require-param: 'error'
jsdoc/require-param-description: 'error'
jsdoc/require-param-name: 'error'
@@ -182,7 +189,11 @@ rules:
jsdoc/require-returns-type: 'error'
jsdoc/valid-types: 'error'
react/prop-types: 'off'
# Disable some from TypeScript.
'@typescript-eslint/camelcase': off
'@typescript-eslint/explicit-function-return-type': 'off'
'@typescript-eslint/no-empty-function': 'off'
react/jsx-sort-props: 'error'
react-hooks/rules-of-hooks: 'error'
react-hooks/exhaustive-deps: 'error'

View File

@@ -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://shields.io/docs/static-badges).
[already possible to make these](https://github.com/badges/shields/blob/master/doc/static-badges.md).
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/badges/static-badge )
(static badge generator can be found at https://shields.io/#your-badge )
validations:
required: true

View File

@@ -28,13 +28,14 @@ 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',
]
@@ -52,15 +53,14 @@ 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)
}

View File

@@ -1,4 +1,4 @@
FROM node:18-bullseye
FROM node:12-buster
RUN apt-get update
RUN apt-get install -y jq

View File

@@ -2,17 +2,14 @@
set -euxo pipefail
# mark workspace dir as 'safe'
git config --system --add safe.directory '/github/workspace'
# Set up a git user
git config user.name "release[bot]"
git config user.email "actions@users.noreply.github.com"
# 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))

View File

@@ -0,0 +1,31 @@
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

View File

@@ -7,19 +7,11 @@ inputs:
runs:
using: 'composite'
steps:
- name: Migrate DB
if: always()
run: npm run migrate up
env:
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
shell: bash
- name: Integration Tests
if: always()
run: npm run test:integration -- --reporter json --reporter-option 'output=reports/integration-tests.json'
env:
GH_TOKEN: '${{ inputs.github-token }}'
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/ci_test
shell: bash
- name: Write Markdown Summary

View File

@@ -19,10 +19,6 @@ 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:

View File

@@ -8,7 +8,6 @@ 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
@@ -28,7 +27,6 @@ updates:
day: friday
time: '12:00'
open-pull-requests-limit: 99
rebase-strategy: disabled
# close-bot package dependencies
- package-ecosystem: npm
@@ -38,12 +36,8 @@ 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

View File

@@ -1,10 +0,0 @@
#!/bin/bash
set -euxo pipefail
apps=$(flyctl apps list --json | jq -r .[].ID | grep -E "pr-[0-9]+-badges-shields") || exit 0
for app in $apps
do
flyctl apps destroy "$app" -y
done

View File

@@ -1,34 +0,0 @@
#!/bin/bash
set -euxo pipefail
app="pr-$PR_NUMBER-badges-shields"
region="ewr"
org="shields-io"
# Get PR JSON from the API
# 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")
# 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 checkout "pr-$PR_NUMBER"
# If the app does not already exist, create it
if ! flyctl status --app "$app"; then
flyctl launch --no-deploy --copy-config --name "$app" --region "$region" --org "$org"
echo $SECRETS | tr " " "\n" | flyctl secrets import --app "$app"
fi
# Deploy
flyctl deploy --app "$app" --region "$region"
# Post a comment on the PR
app_url=$(flyctl status --app "$app" --json | jq -r .Hostname)
comment_url=$(echo "$pr_json" | jq .comments_url -r)
curl "$comment_url" \
-X POST \
-H "Authorization: token $GITHUB_TOKEN" \
--data "{\"body\":\"🚀 Updated review app: https://$app_url\"}"

View File

@@ -1,9 +1,6 @@
name: Build Docker Image
on:
pull_request:
push:
branches:
- 'gh-readonly-queue/**'
jobs:
build-docker-image:
@@ -14,17 +11,15 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: v0.9.1
- name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build
uses: docker/build-push-action@v4
uses: docker/build-push-action@v3
with:
context: .
push: false
tags: ghcr.io/badges/shields:pr-validation
tags: shieldsio/shields:pr-validation
build-args: |
version=${{ env.SHORT_SHA }}

View File

@@ -1,24 +0,0 @@
name: Cleanup Review Apps
on:
schedule:
- cron: '0 7 * * *'
# At 07:00, daily
workflow_dispatch:
jobs:
cleanup-review-apps:
runs-on: ubuntu-latest
environment: 'Review Apps'
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: install jq
run: |
sudo apt-get -qq update
sudo apt-get install -y jq
- run: .github/scripts/cleanup-review-apps.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

View File

@@ -6,7 +6,6 @@ on:
permissions:
contents: write
packages: write
jobs:
create-release:
@@ -36,8 +35,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: v0.9.1
- name: Login to DockerHub
uses: docker/login-action@v2
@@ -46,26 +43,10 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push snapshot release to DockerHub
uses: docker/build-push-action@v4
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: shieldsio/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push snapshot release to GHCR
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/badges/shields:server-${{ steps.date.outputs.date }}
build-args: |
version=server-${{ steps.date.outputs.date }}

View File

@@ -1,7 +1,7 @@
name: Danger
on:
pull_request_target:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
permissions:
checks: write
@@ -11,7 +11,7 @@ permissions:
jobs:
danger:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]' && github.actor != 'repo-ranger[bot]'
if: github.actor != 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@v3

View File

@@ -16,13 +16,10 @@ jobs:
with:
persist-credentials: false
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 16
- name: Build
run: npm run build-docs
run: |
npm ci
npm run build-docs
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4

View File

@@ -1,43 +0,0 @@
name: Create/Update Review App
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR Number to deploy e.g: 1234'
required: true
permissions:
pull-requests: write
jobs:
deploy-review-app:
runs-on: ubuntu-latest
environment: 'Review Apps'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: install jq
run: |
sudo apt-get -qq update
sudo apt-get install -y jq
- run: .github/scripts/deploy-review-app.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
# credentials to set when we create the review app
SECRETS: |
GH_TOKEN=${{ secrets.GH_PAT }}
LIBRARIESIO_TOKENS=${{ secrets.SERVICETESTS_LIBRARIESIO_TOKENS }}
OBS_USER=${{ secrets.SERVICETESTS_OBS_USER }}
OBS_PASS=${{ secrets.SERVICETESTS_OBS_PASS }}
SL_INSIGHT_API_TOKEN=${{ secrets.SERVICETESTS_SL_INSIGHT_USER_UUID }}
SL_INSIGHT_USER_UUID=${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}
TWITCH_CLIENT_ID=${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET=${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}
WHEELMAP_TOKEN=${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}
YOUTUBE_API_KEY=${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}

View File

@@ -1,7 +1,7 @@
name: 'Dependency Review'
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
jobs:
enforce-dependency-review:

View File

@@ -4,9 +4,6 @@ on:
branches:
- master
permissions:
packages: write
jobs:
publish-docker-next:
runs-on: ubuntu-latest
@@ -16,8 +13,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
version: v0.9.1
- name: Login to DockerHub
uses: docker/login-action@v2
@@ -28,27 +23,11 @@ jobs:
- name: Set Git Short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Build and push to DockerHub
uses: docker/build-push-action@v4
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: shieldsio/shields:next
build-args: |
version=${{ env.SHORT_SHA }}
- name: Login to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to GHCR
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/badges/shields:next
build-args: |
version=${{ env.SHORT_SHA }}

View File

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

View File

@@ -1,7 +1,7 @@
name: E2E
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
@@ -29,10 +29,13 @@ 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
run: npm run e2e-on-build
- name: Archive videos
if: always()

26
.github/workflows/test-frontend.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Frontend
on:
pull_request:
types: [opened, edited, 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

View File

@@ -1,32 +1,28 @@
name: Integration@node 18
name: Integration@node 17
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-integration-18:
test-integration-17:
runs-on: ubuntu-latest
env:
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ci_test
redis:
image: redis
options: >-
--health-cmd pg_isready
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
- 6379:6379
steps:
- name: Checkout
@@ -35,7 +31,7 @@ jobs:
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 18
node-version: 17
env:
NPM_CONFIG_ENGINE_STRICT: 'false'

View File

@@ -1,7 +1,7 @@
name: Integration
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
@@ -14,19 +14,15 @@ jobs:
PAT_EXISTS: ${{ secrets.GH_PAT != '' }}
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: ci_test
redis:
image: redis
options: >-
--health-cmd pg_isready
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
- 6379:6379
steps:
- name: Checkout

View File

@@ -1,7 +1,7 @@
name: Lint
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'

View File

@@ -1,14 +1,14 @@
name: Main@node 18
name: Main@node 17
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
- 'dependabot/**'
jobs:
test-main-18:
test-main-17:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -17,7 +17,7 @@ jobs:
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 18
node-version: 17
env:
NPM_CONFIG_ENGINE_STRICT: 'false'

View File

@@ -1,7 +1,7 @@
name: Main
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'

View File

@@ -1,7 +1,7 @@
name: Package CLI
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
@@ -16,9 +16,12 @@ jobs:
strategy:
matrix:
include:
- node: '14'
engine-strict: 'false'
- node: '16'
engine-strict: 'false'
- node: '18'
- node: '20'
engine-strict: 'true'
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -29,6 +32,9 @@ 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

View File

@@ -1,7 +1,7 @@
name: Package Library
on:
pull_request:
types: [opened, reopened, synchronize]
types: [opened, edited, reopened, synchronize]
push:
branches-ignore:
- 'gh-pages'
@@ -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

View File

@@ -1,13 +1,10 @@
name: Services@node 18
name: Services@node 17
on:
pull_request:
types: [opened, edited, reopened, synchronize]
push:
branches:
- 'gh-readonly-queue/**'
jobs:
test-services-18:
test-services-17:
runs-on: ubuntu-latest
steps:
@@ -17,7 +14,7 @@ jobs:
- name: Setup
uses: ./.github/actions/setup
with:
node-version: 18
node-version: 17
env:
NPM_CONFIG_ENGINE_STRICT: 'false'

View File

@@ -2,9 +2,6 @@ name: Services
on:
pull_request:
types: [opened, edited, reopened, synchronize]
push:
branches:
- 'gh-readonly-queue/**'
jobs:
test-services:

View File

@@ -25,7 +25,7 @@ jobs:
run: node scripts/update-github-api.js
- name: Create Pull Request if config has changed
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v4
with:
token: '${{ secrets.GITHUB_TOKEN }}'
commit-message: Update GitHub API Version

18
.gitignore vendored
View File

@@ -92,7 +92,10 @@ typings/
# Temporary build artifacts.
/build
frontend/categories/*.yaml
.next
badge-examples.json
supported-features.json
service-definitions.yml
# Local runtime configuration.
/config/local*.yml
@@ -100,6 +103,11 @@ frontend/categories/*.yaml
# Template for the local runtime configuration.
!/config/local*.template.yml
# Gatsby
/frontend/.cache
/frontend/public
/public
# Cypress
/cypress/videos/
/cypress/screenshots/
@@ -109,11 +117,3 @@ frontend/categories/*.yaml
# Flamebearer
flamegraph.html
# config file for node-pg-migrate
migrations-config.json
# Frontend/Docusaurus
frontend/.docusaurus
frontend/.cache-loader
/public

5
.mocharc-frontend.yml Normal file
View File

@@ -0,0 +1,5 @@
reporter: mocha-env-reporter
require:
- '@babel/polyfill'
- '@babel/register'
- mocha-yaml-loader

10
.nycrc-frontend.json Normal file
View File

@@ -0,0 +1,10 @@
{
"reporter": ["lcov"],
"all": false,
"silent": true,
"clean": false,
"sourceMap": false,
"instrument": false,
"include": ["frontend/**/*.js"],
"exclude": ["**/*.spec.js", "**/mocha-*.js"]
}

View File

@@ -10,5 +10,5 @@ public
private/*.json
/.nyc_output
analytics.json
frontend/.docusaurus
frontend/categories
supported-features.json
service-definitions.yml

View File

@@ -4,100 +4,6 @@ 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)
- set a custom error on 429 [#9159](https://github.com/badges/shields/issues/9159)
- deprecate [travis].org badges [#9171](https://github.com/badges/shields/issues/9171)
- count private sponsors on [GithubSponsors] badge [#9170](https://github.com/badges/shields/issues/9170)
- Dependency updates
## server-2023-05-01
** Removal:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. This feature was deprecated in `server-2023-03-01`. As of this release, the `RedisTokenPersistence` backend is now removed. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
- fail to start server if there are duplicate service names [#9099](https://github.com/badges/shields/issues/9099)
- [SourceForge] Added badges for SourceForge [#9078](https://github.com/badges/shields/issues/9078) [#9102](https://github.com/badges/shields/issues/9102)
- crates: Use `?include=` to reduce crates.io backend load [#9081](https://github.com/badges/shields/issues/9081)
- Dependency updates
## server-2023-04-02
- [JenkinsCoverage] Update Jenkins Code Coverage API for new plugin version [#9010](https://github.com/badges/shields/issues/9010)
- [CTAN] fallback to date if version is empty [#9036](https://github.com/badges/shields/issues/9036)
- Update to [CTAN] API version 2.0 [#9016](https://github.com/badges/shields/issues/9016)
- handle missing statistics array in [VisualStudioMarketplace] badges [#8985](https://github.com/badges/shields/issues/8985)
- [Netlify] upgrade colors for SVG parsing [#8971](https://github.com/badges/shields/issues/8971)
- Fix [Vcpkg] version service for different version fields [#8945](https://github.com/badges/shields/issues/8945)
- only try to close pool if one exists [#8947](https://github.com/badges/shields/issues/8947)
- misc minor fixes to [githubsize node pypi] [#8946](https://github.com/badges/shields/issues/8946)
- Dependency updates
## server-2023-03-01
**Deprecation:** For users who need to maintain a Github Token pool, storage has been provided via the `RedisTokenPersistence` and `REDIS_URL` settings. As of this release, the `RedisTokenPersistence` backend is now deprecated and will be removed in a future release. If you are using this feature, you will need to migrate to using the `SQLTokenPersistence` backend for storage and provide a postgres connection string via the `POSTGRES_URL` setting. [#8922](https://github.com/badges/shields/issues/8922)
- fix: for crates.io versions, use max_stable_version if it exists [#8687](https://github.com/badges/shields/issues/8687)
- don't autofocus search [#8927](https://github.com/badges/shields/issues/8927)
- Add [Vcpkg] version service [#8923](https://github.com/badges/shields/issues/8923)
- fix: Set uid/gid in docker image to 0 [#8908](https://github.com/badges/shields/issues/8908)
- expose port 443 in Dockerfile [#8889](https://github.com/badges/shields/issues/8889)
- Dependency updates
## server-2023-02-01
- replace [twitter] badge with static fallback [#8842](https://github.com/badges/shields/issues/8842)
- Add various [Polymart] badges [#8811](https://github.com/badges/shields/issues/8811)
- update [githubpipenv] tests/examples [#8797](https://github.com/badges/shields/issues/8797)
- deprecate [apm] service [#8773](https://github.com/badges/shields/issues/8773)
- deprecate lgtm [#8771](https://github.com/badges/shields/issues/8771)
- Dependency updates
## server-2023-01-01
- Breaking change: Routes for GitHub workflows badge have changed. See https://github.com/badges/shields/issues/8671 for more details

View File

@@ -134,19 +134,9 @@ Prettier before a commit by default.
When adding or changing a service [please write tests][service-tests], and ensure the [title of your Pull Requests follows the required conventions](#running-service-tests-in-pull-requests) to ensure your tests are executed.
When changing other code, please add unit tests.
The integration tests are not run by default. For most contributions it is OK to skip these unless you're working directly on the code for storing the GitHub token pool in postgres.
To run the integration tests:
- You must have PostgreSQL installed. Use `brew install postgresql`, `apt-get install postgresql`, etc.
- Set a connection string either with an env var `POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/db_name` or by using
```yaml
private:
postgres_url: 'postgresql://user:pass@127.0.0.1:5432/db_name'
```
in a yaml config file.
- Run `npm run migrate up` to apply DB migrations
- Run `npm run test:integration` to run the tests
To run the integration tests, you must have Redis installed and in your PATH.
Use `brew install redis`, `yum install redis`, etc. The test runner will
start the server automatically.
[service-tests]: https://github.com/badges/shields/blob/master/doc/service-tests.md

View File

@@ -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@^9.0.0"
RUN npm install -g "npm@>=8"
# 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
@@ -30,8 +30,8 @@ LABEL fly.version=$version
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --from=Builder --chown=0:0 /usr/src/app /usr/src/app
COPY --from=Builder /usr/src/app /usr/src/app
CMD node server
EXPOSE 80 443
EXPOSE 80

View File

@@ -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]: http://localhost:3000/badges/static-badge
[custom badges]: https://shields.io/#your-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 (`docusaurus start`) should also automatically reload. However the badge
server (`gatsby dev`) 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.

View File

@@ -2,7 +2,7 @@
## 4.0.0 [WIP]
- Drop compatibility with Node < 16
- Drop compatibility with Node < 14
## 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
},
}
)
})
```

View File

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

View File

@@ -1,6 +1,7 @@
'use strict'
const path = require('path')
const isSvg = require('is-svg')
const { spawn } = require('child-process-promise')
const { expect, use } = require('chai')
use(require('chai-string'))
@@ -19,7 +20,6 @@ describe('The CLI', function () {
})
it('should produce default badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown'])
expect(stdout)
.to.satisfy(isSvg)
@@ -28,13 +28,11 @@ describe('The CLI', function () {
})
it('should produce colorschemed badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown', ':green'])
expect(stdout).to.satisfy(isSvg)
})
it('should produce right-color badges', async function () {
const { default: isSvg } = await import('is-svg')
const { stdout } = await runCli(['cactus', 'grown', '#abcdef'])
expect(stdout).to.satisfy(isSvg).and.to.include('#abcdef')
})

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
'use strict'
const { expect } = require('chai')
const isSvg = require('is-svg')
const { makeBadge, ValidationError } = require('.')
describe('makeBadge function', function () {
it('should produce badge with valid input', async function () {
const { default: isSvg } = await import('is-svg')
it('should produce badge with valid input', function () {
expect(
makeBadge({
label: 'build',
message: 'passed',
}),
})
).to.satisfy(isSvg)
expect(
makeBadge({
message: 'passed',
}),
})
).to.satisfy(isSvg)
expect(
makeBadge({
@@ -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)'
)
})
})

View File

@@ -58,6 +58,6 @@ module.exports = function makeBadge({
logoPadding: logo && label.length ? 3 : 0,
color: toSvgColor(color),
labelColor: toSvgColor(labelColor),
}),
})
)
}

View File

@@ -3,11 +3,12 @@
const { test, given, forCases } = require('sazerac')
const { expect } = require('chai')
const snapshot = require('snap-shot-it')
const isSvg = require('is-svg')
const prettier = require('prettier')
const makeBadge = require('./make-badge')
async function expectBadgeToMatchSnapshot(format) {
snapshot(await prettier.format(makeBadge(format), { parser: 'html' }))
function expectBadgeToMatchSnapshot(format) {
snapshot(prettier.format(makeBadge(format), { parser: 'html' }))
}
function testColor(color = '', colorAttr = 'color') {
@@ -17,7 +18,7 @@ function testColor(color = '', colorAttr = 'color') {
message: 'Bob',
[colorAttr]: color,
format: 'json',
}),
})
).color
}
@@ -67,7 +68,7 @@ describe('The badge generator', function () {
given('bluish'),
given('almostred'),
given('brightmaroon'),
given('cactus'),
given('cactus')
).expect(undefined)
})
})
@@ -79,16 +80,15 @@ describe('The badge generator', function () {
})
describe('SVG', function () {
it('should produce SVG', async function () {
const { default: isSvg } = await import('is-svg')
it('should produce SVG', function () {
expect(makeBadge({ label: 'cactus', message: 'grown', format: 'svg' }))
.to.satisfy(isSvg)
.and.to.include('cactus')
.and.to.include('grown')
})
it('should match snapshot', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshot', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -113,8 +113,7 @@ describe('The badge generator', function () {
})
})
it('should replace undefined svg badge style with "flat"', async function () {
const { default: isSvg } = await import('is-svg')
it('should replace undefined svg badge style with "flat"', function () {
const jsonBadgeWithUnknownStyle = makeBadge({
label: 'name',
message: 'Bob',
@@ -138,14 +137,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', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -155,8 +154,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -167,8 +166,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -177,8 +176,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -188,8 +187,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo and labelColor', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -200,8 +199,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with links', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -212,8 +211,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the label color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -223,8 +222,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the message color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -236,8 +235,8 @@ describe('The badge generator', function () {
})
describe('"flat-square" template badge generation', function () {
it('should match snapshots: message/label, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -247,8 +246,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -259,8 +258,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -269,8 +268,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -280,8 +279,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo and labelColor', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -292,8 +291,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with links', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -304,8 +303,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the label color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -315,8 +314,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the message color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -328,8 +327,8 @@ describe('The badge generator', function () {
})
describe('"plastic" template badge generation', function () {
it('should match snapshots: message/label, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -339,8 +338,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -351,8 +350,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -361,8 +360,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -372,8 +371,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo and labelColor', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -384,8 +383,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with links', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -396,8 +395,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the label color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -407,8 +406,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the message color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -428,7 +427,7 @@ describe('The badge generator', function () {
message: 1999,
format: 'svg',
style: 'for-the-badge',
}),
})
)
.to.include('1998')
.and.to.include('1999')
@@ -441,14 +440,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', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -458,8 +457,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -470,8 +469,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -480,8 +479,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -491,8 +490,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo and labelColor', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -503,8 +502,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with links', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -515,8 +514,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the label color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the label color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -526,8 +525,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: black text when the message color is light', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: black text when the message color is light', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -546,7 +545,7 @@ describe('The badge generator', function () {
message: 'some-value',
format: 'svg',
style: 'social',
}),
})
)
.to.include('Some-key')
.and.to.include('some-value')
@@ -560,14 +559,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', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, no logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -577,8 +576,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with logo', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -589,8 +588,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, no logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, no logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -599,8 +598,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -610,8 +609,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message only, with logo and labelColor', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message only, with logo and labelColor', function () {
expectBadgeToMatchSnapshot({
label: '',
message: 'grown',
format: 'svg',
@@ -622,8 +621,8 @@ describe('The badge generator', function () {
})
})
it('should match snapshots: message/label, with links', async function () {
await expectBadgeToMatchSnapshot({
it('should match snapshots: message/label, with links', function () {
expectBadgeToMatchSnapshot({
label: 'cactus',
message: 'grown',
format: 'svg',
@@ -636,8 +635,8 @@ describe('The badge generator', function () {
})
describe('badges with logos should always produce the same badge', function () {
it('badge with logo', async function () {
await expectBadgeToMatchSnapshot({
it('badge with logo', function () {
expectBadgeToMatchSnapshot({
label: 'label',
message: 'message',
format: 'svg',

View File

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

View File

@@ -26,7 +26,8 @@
"badge": "lib/badge-cli.js"
},
"engines": {
"node": ">=16"
"node": ">= 14",
"npm": ">= 6"
},
"collective": {
"type": "opencollective",

View File

@@ -94,7 +94,6 @@ private:
obs_user: 'OBS_USER'
obs_pass: 'OBS_PASS'
redis_url: 'REDIS_URL'
postgres_url: 'POSTGRES_URL'
sentry_dsn: 'SENTRY_DSN'
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'

View File

@@ -4,6 +4,7 @@ private:
gh_client_id: ...
gh_client_secret: ...
gitlab_token: ...
redis_url: ...
sentry_dsn: ...
shields_secret: ...
sl_insight_userUuid: ...

94
core/badge-urls/make-badge-url.d.ts vendored Normal file
View File

@@ -0,0 +1,94 @@
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

View File

@@ -1,5 +1,119 @@
// 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
@@ -10,4 +124,11 @@ function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
return result
}
export { rasterRedirectUrl }
export {
badgeUrlFromPath,
encodeField,
staticBadgeUrl,
queryStringStaticBadgeUrl,
dynamicBadgeUrl,
rasterRedirectUrl,
}

View File

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

View File

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

View File

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

View File

@@ -8,13 +8,12 @@ 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 () {
@@ -26,8 +25,8 @@ describe('AuthHelper', function () {
passKey: 'myci_pass',
authorizedOrigins: true,
},
{ private: {} },
),
{ private: {} }
)
).to.throw(Error, 'Expected authorizedOrigins to be an array of origins')
})
})
@@ -36,7 +35,7 @@ describe('AuthHelper', function () {
function validate(config, privateConfig) {
return new AuthHelper(
{ authorizedOrigins: ['https://example.test'], ...config },
{ private: privateConfig },
{ private: privateConfig }
).isValid
}
test(validate, () => {
@@ -44,20 +43,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' }),
@@ -71,16 +70,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 }, {}),
@@ -92,18 +91,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({
@@ -115,11 +114,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',
@@ -169,7 +168,7 @@ describe('AuthHelper', function () {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
}),
})
).to.throw(InvalidParameter)
})
})
@@ -193,7 +192,7 @@ describe('AuthHelper', function () {
expect(() =>
authHelper.enforceStrictSsl({
options: { https: { rejectUnauthorized: false } },
}),
})
).not.to.throw()
})
})
@@ -319,7 +318,7 @@ describe('AuthHelper', function () {
},
},
private: { myci_user: 'admin', myci_pass: 'abc123' },
},
}
)
const withBasicAuth = requestOptions =>
authHelper.withBasicAuth(requestOptions)
@@ -377,7 +376,7 @@ describe('AuthHelper', function () {
withBasicAuth({
url: 'https://myci.test/api',
options: { https: { rejectUnauthorized: false } },
}),
})
).to.throw(InvalidParameter)
})
})

View File

@@ -44,12 +44,6 @@ class BaseGraphqlService extends BaseService {
* and custom error messages e.g: `{ 404: 'package not found' }`.
* This can be used to extend or override the
* [default](https://github.com/badges/shields/blob/master/core/base-service/check-error-response.js#L5)
* @param {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.
@@ -68,7 +62,6 @@ class BaseGraphqlService extends BaseService {
variables = {},
options = {},
httpErrorMessages = {},
systemErrors = {},
transformJson = data => data,
transformErrors = defaultTransformErrors,
}) {
@@ -81,8 +74,7 @@ class BaseGraphqlService extends BaseService {
const { buffer } = await this._request({
url,
options: mergedOptions,
httpErrors: httpErrorMessages,
systemErrors,
errorMessages: httpErrorMessages,
})
const json = transformJson(this._parseJson(buffer))
if (json.errors) {
@@ -91,7 +83,7 @@ class BaseGraphqlService extends BaseService {
throw exception
} else {
throw Error(
`transformErrors() must return a ShieldsRuntimeError; got ${exception}`,
`transformErrors() must return a ShieldsRuntimeError; got ${exception}`
)
}
}

View File

@@ -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}","variables":{}}',
body: '{"query":"{\\n requiredString\\n}\\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}","variables":{}}',
body: '{"query":"{\\n requiredString\\n}\\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',

View File

@@ -30,26 +30,14 @@ 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.httpErrors={}] Key-value map of status codes
* @param {object} [attrs.errorMessages={}] 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 = {},
httpErrors = {},
systemErrors = {},
}) {
async _requestJson({ schema, url, options = {}, errorMessages = {} }) {
const mergedOptions = {
...{ headers: { Accept: 'application/json' } },
...options,
@@ -57,8 +45,7 @@ class BaseJsonService extends BaseService {
const { buffer } = await this._request({
url,
options: mergedOptions,
httpErrors,
systemErrors,
errorMessages,
})
const json = this._parseJson(buffer)
return this.constructor._validate(json, schema)

View File

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

View File

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

View File

@@ -53,16 +53,10 @@ 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.httpErrors={}] Key-value map of status codes
* @param {object} [attrs.errorMessages={}] 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
*/
@@ -71,8 +65,7 @@ class BaseSvgScrapingService extends BaseService {
valueMatcher,
url,
options = {},
httpErrors = {},
systemErrors = {},
errorMessages = {},
}) {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
const mergedOptions = {
@@ -82,8 +75,7 @@ class BaseSvgScrapingService extends BaseService {
const { buffer } = await this._request({
url,
options: mergedOptions,
httpErrors,
systemErrors,
errorMessages,
})
logTrace(emojic.dart, 'Response SVG', buffer)
const data = {

View File

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

View File

@@ -24,16 +24,10 @@ 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.httpErrors={}] Key-value map of status codes
* @param {object} [attrs.errorMessages={}] 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
@@ -44,8 +38,7 @@ class BaseXmlService extends BaseService {
schema,
url,
options = {},
httpErrors = {},
systemErrors = {},
errorMessages = {},
parserOptions = {},
}) {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
@@ -56,8 +49,7 @@ class BaseXmlService extends BaseService {
const { buffer } = await this._request({
url,
options: mergedOptions,
httpErrors,
systemErrors,
errorMessages,
})
const validateResult = XMLValidator.validate(buffer)
if (validateResult !== true) {

View File

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

View File

@@ -23,16 +23,10 @@ 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.httpErrors={}] Key-value map of status codes
* @param {object} [attrs.errorMessages={}] 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
@@ -41,8 +35,7 @@ class BaseYamlService extends BaseService {
schema,
url,
options = {},
httpErrors = {},
systemErrors = {},
errorMessages = {},
encoding = 'utf8',
}) {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
@@ -58,8 +51,7 @@ class BaseYamlService extends BaseService {
const { buffer } = await this._request({
url,
options: mergedOptions,
httpErrors,
systemErrors,
errorMessages,
})
let parsed
try {

View File

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

View File

@@ -44,7 +44,7 @@ const optionalStringWhenNamedLogoPresent = Joi.alternatives().conditional(
{
is: Joi.string().required(),
then: Joi.string(),
},
}
)
const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
@@ -140,15 +140,6 @@ class BaseService {
*/
static examples = []
/**
* Optional: an OpenAPI Paths Object describing this service's
* route or routes in OpenAPI format.
*
* @see https://swagger.io/specification/#paths-object
* @abstract
*/
static openApi = undefined
static get _cacheLength() {
const cacheLengths = {
build: 30,
@@ -183,37 +174,21 @@ 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() {
const { category, name, isDeprecated, openApi } = this
const { category, name, isDeprecated } = this
const { base, format, pattern } = this.route
const queryParams = getQueryParamNames(this.route)
const examples = this.examples.map((example, index) =>
transformExample(example, index, this),
transformExample(example, index, this)
)
let route
@@ -225,7 +200,7 @@ class BaseService {
route = undefined
}
const result = { category, name, isDeprecated, route, examples, openApi }
const result = { category, name, isDeprecated, route, examples }
assertValidServiceDefinition(result, `getDefinition() for ${this.name}`)
@@ -234,7 +209,7 @@ class BaseService {
constructor(
{ requestFetcher, authHelper, metricHelper },
{ handleInternalErrors },
{ handleInternalErrors }
) {
this._requestFetcher = requestFetcher
this.authHelper = authHelper
@@ -242,7 +217,7 @@ class BaseService {
this._metricHelper = metricHelper
}
async _request({ url, options = {}, httpErrors = {}, systemErrors = {} }) {
async _request({ url, options = {}, errorMessages = {} }) {
const logTrace = (...args) => trace.logTrace('fetch', ...args)
let logUrl = url
const logOptions = Object.assign({}, options)
@@ -250,9 +225,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
@@ -260,16 +235,12 @@ class BaseService {
logTrace(
emojic.bowAndArrow,
'Request',
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}`,
)
const { res, buffer } = await this._requestFetcher(
url,
options,
systemErrors,
`${logUrl}\n${JSON.stringify(logOptions, null, 2)}`
)
const { res, buffer } = await this._requestFetcher(url, options)
await this._meterResponse(res, buffer)
logTrace(emojic.dart, 'Response status code', res.statusCode)
return checkErrorResponse(httpErrors)({ buffer, res })
return checkErrorResponse(errorMessages)({ buffer, res })
}
static enabledMetrics = []
@@ -295,7 +266,7 @@ class BaseService {
prettyErrorMessage = 'invalid response data',
includeKeys = false,
allowAndStripUnknownKeys = true,
} = {},
} = {}
) {
return validate(
{
@@ -307,7 +278,7 @@ class BaseService {
allowAndStripUnknownKeys,
},
data,
schema,
schema
)
}
@@ -348,22 +319,18 @@ class BaseService {
error instanceof Deprecated
) {
trace.logTrace('outbound', emojic.noGoodWoman, 'Handled error', error)
const serviceData = {
return {
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
@@ -381,7 +348,7 @@ class BaseService {
'unhandledError',
emojic.boom,
'Unhandled internal error',
error,
error
)
throw error
}
@@ -391,7 +358,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)
@@ -425,13 +392,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
@@ -445,7 +412,7 @@ class BaseService {
try {
serviceData = await serviceInstance.handle(
namedParams,
transformedQueryParams,
transformedQueryParams
)
serviceInstance._validateServiceData(serviceData)
} catch (error) {
@@ -470,7 +437,7 @@ class BaseService {
librariesIoApiProvider,
metricInstance,
},
serviceConfig,
serviceConfig
) {
const { cacheHeaders: cacheHeaderConfig } = serviceConfig
const { regex, captureNames } = prepareRoute(this.route)
@@ -498,14 +465,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(/^\./, '')
@@ -514,7 +481,7 @@ class BaseService {
metricHandle.noteResponseSent()
},
cacheLength: this._cacheLength,
}),
})
)
}
}

View File

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

View File

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

View File

@@ -74,12 +74,12 @@ describe('Cache header functions', function () {
serviceDefaultCacheLengthSeconds: 900,
serviceOverrideCacheLengthSeconds: 400,
queryParams: {},
}).expect(400)
}).expect(900)
given({
cacheHeaderConfig,
serviceOverrideCacheLengthSeconds: 400,
queryParams: {},
}).expect(400)
}).expect(777)
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)
})
},
}
)
})
})

View File

@@ -2,22 +2,21 @@ import { NotFound, InvalidResponse, Inaccessible } from './errors.js'
const defaultErrorMessages = {
404: 'not found',
429: 'rate limited by upstream service',
}
export default function checkErrorResponse(httpErrors = {}) {
export default function checkErrorResponse(errorMessages = {}) {
return async function ({ buffer, res }) {
let error
httpErrors = { ...defaultErrorMessages, ...httpErrors }
errorMessages = { ...defaultErrorMessages, ...errorMessages }
if (res.statusCode === 404) {
error = new NotFound({ prettyMessage: httpErrors[404] })
error = new NotFound({ prettyMessage: errorMessages[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 (httpErrors[res.statusCode] !== undefined) {
props.prettyMessage = httpErrors[res.statusCode]
if (errorMessages[res.statusCode] !== undefined) {
props.prettyMessage = errorMessages[res.statusCode]
}
if (res.statusCode >= 500) {
error = new Inaccessible(props)

View File

@@ -45,42 +45,6 @@ describe('async error handler', function () {
})
})
context('when status is 429', function () {
const buffer = Buffer.from('some stuff')
const res = { statusCode: 429 }
it('throws InvalidResponse', async function () {
try {
await checkErrorResponse()({ res, buffer })
expect.fail('Expected to throw')
} catch (e) {
expect(e).to.be.an.instanceof(InvalidResponse)
expect(e.message).to.equal(
'Invalid Response: Got status code 429 (expected 200)',
)
expect(e.prettyMessage).to.equal('rate limited by upstream service')
expect(e.response).to.equal(res)
expect(e.buffer).to.equal(buffer)
}
})
it('displays the custom too many requests', async function () {
const notFoundMessage = "terribly sorry but that's one too many requests"
try {
await checkErrorResponse({ 429: notFoundMessage })({ res, buffer })
expect.fail('Expected to throw')
} catch (e) {
expect(e).to.be.an.instanceof(InvalidResponse)
expect(e.message).to.equal(
'Invalid Response: Got status code 429 (expected 200)',
)
expect(e.prettyMessage).to.equal(
"terribly sorry but that's one too many requests",
)
}
})
})
context('when status is 4xx', function () {
it('throws InvalidResponse', async function () {
const res = { statusCode: 499 }
@@ -90,7 +54,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 +70,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 +86,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 +102,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')
}

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,6 @@ class ShieldsRuntimeError extends Error {
if (props.underlyingError) {
this.stack = props.underlyingError.stack
}
this.cacheSeconds = props.cacheSeconds
}
}
@@ -207,9 +206,6 @@ 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 {

View File

@@ -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,16 +126,15 @@ function transformExample(inExample, index, ServiceClass) {
{},
staticPreview,
ServiceClass.defaultBadgeData,
ServiceClass,
ServiceClass
)
const category = categories.find(c => c.id === ServiceClass.category)
return {
title,
example: {
pattern: makeFullUrl(
ServiceClass.route.base,
pattern || ServiceClass.route.pattern,
pattern || ServiceClass.route.pattern
),
namedParams,
queryParams,
@@ -147,7 +146,9 @@ function transformExample(inExample, index, ServiceClass) {
style: style === 'flat' ? undefined : style,
namedLogo,
},
keywords: category ? keywords.concat(category.keywords) : keywords,
keywords: keywords.concat(
categories.find(c => c.id === ServiceClass.category).keywords
),
documentation: documentation ? { __html: documentation } : undefined,
}
}

View File

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

View File

@@ -7,7 +7,7 @@ import {
const userAgent = getUserAgent()
async function sendRequest(gotWrapper, url, options = {}, systemErrors = {}) {
async function sendRequest(gotWrapper, url, options) {
const gotOptions = Object.assign({}, options)
gotOptions.throwHttpErrors = false
gotOptions.retry = { limit: 0 }
@@ -22,12 +22,6 @@ async function sendRequest(gotWrapper, url, options = {}, systemErrors = {}) {
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 })
}
}

View File

@@ -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,40 +38,10 @@ 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"',
)
})
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)
})
'Nock: Disallowed net connect for "www.google.com:443/foo/bar"'
)
})

View File

@@ -9,16 +9,18 @@ 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(
@@ -35,11 +37,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(
@@ -59,9 +61,9 @@ describe('mergeQueries function', function () {
query {
baz
}
`,
),
),
`
)
)
).to.equalIgnoreSpaces('{ foo bar baz }')
expect(
@@ -76,9 +78,9 @@ describe('mergeQueries function', function () {
{
bar
}
`,
),
),
`
)
)
).to.equalIgnoreSpaces('{ foo bar }')
})

View File

@@ -15,7 +15,6 @@ import {
Deprecated,
ImproperlyConfigured,
} from './errors.js'
import { pathParam, pathParams, queryParam, queryParams } from './openapi.js'
export {
BaseService,
@@ -33,8 +32,4 @@ export {
InvalidParameter,
ImproperlyConfigured,
Deprecated,
pathParam,
pathParams,
queryParam,
queryParams,
}

View File

@@ -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', '/badges/endpoint-badge')
ask.res.setHeader('Location', '/endpoint/')
ask.res.end()
return
}
@@ -73,9 +73,11 @@ 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`.
// Then the `cacheSeconds` query param can also override both of those
// but only if `cacheSeconds` is longer.
// `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.
//
// When the legacy services have been rewritten, all the code in here
// will go away, which should achieve this goal in a simpler way.
@@ -104,7 +106,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(/^\./, '')
@@ -126,7 +128,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) {

View File

@@ -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 allow serviceData to override the default cache headers with longer value', async function () {
it('should let live service data override the default cache headers with longer value', async function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(
@@ -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 allow serviceData to override the default cache headers with shorter value', async function () {
it('should not let live service data override the default cache headers with shorter value', async function () {
camp.route(
/^\/testing\/([^/]+)\.(svg|png|gif|jpg|json)$/,
handleRequest(
@@ -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=200, s-maxage=200')
expect(headers['cache-control']).to.equal('max-age=300, s-maxage=300')
})
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)
})

View File

@@ -1,10 +1,10 @@
import BaseJsonService from '../base-json.js'
class BadBaseService {}
class GoodMixedService extends BaseJsonService {
class GoodService extends BaseJsonService {
static category = 'build'
static route = { base: 'it/is', pattern: 'good' }
}
class BadMixedService extends BadBaseService {}
class BadService extends BadBaseService {}
export default [GoodMixedService, BadMixedService]
export default [GoodService, BadService]

View File

@@ -1,3 +1,3 @@
class BadNoBaseService {}
class BadService {}
export default BadNoBaseService
export default BadService

View File

@@ -1,4 +1,4 @@
class BadBaseService {}
class BadChildService extends BadBaseService {}
class BadService extends BadBaseService {}
export default BadChildService
export default BadService

View File

@@ -1,12 +1,12 @@
import BaseJsonService from '../base-json.js'
class GoodServiceArrayOne extends BaseJsonService {
class GoodServiceOne extends BaseJsonService {
static category = 'build'
static route = { base: 'good', pattern: 'one' }
}
class GoodServiceArrayTwo extends BaseJsonService {
class GoodServiceTwo extends BaseJsonService {
static category = 'build'
static route = { base: 'good', pattern: 'two' }
}
export default [GoodServiceArrayOne, GoodServiceArrayTwo]
export default [GoodServiceOne, GoodServiceTwo]

View File

@@ -1,12 +1,12 @@
import BaseJsonService from '../base-json.js'
class GoodServiceObjectOne extends BaseJsonService {
class GoodServiceOne extends BaseJsonService {
static category = 'build'
static route = { base: 'good', pattern: 'one' }
}
class GoodServiceObjectTwo extends BaseJsonService {
class GoodServiceTwo extends BaseJsonService {
static category = 'build'
static route = { base: 'good', pattern: 'two' }
}
export { GoodServiceObjectOne, GoodServiceObjectTwo }
export { GoodServiceOne, GoodServiceTwo }

View File

@@ -1,6 +1,6 @@
import path from 'path'
import { fileURLToPath } from 'url'
import { globSync } from 'glob'
import glob from 'glob'
import countBy from 'lodash.countby'
import categories from '../../services/categories.js'
import BaseService from './base.js'
@@ -10,7 +10,7 @@ const serviceDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'services',
'services'
)
function toUnixPath(path) {
@@ -27,8 +27,42 @@ class InvalidService extends Error {
}
}
function getServicePaths(pattern) {
return globSync(toUnixPath(path.join(serviceDir, '**', pattern))).sort()
async function loadServiceClasses(servicePaths) {
if (!servicePaths) {
servicePaths = glob.sync(
toUnixPath(path.join(serviceDir, '**', '*.service.js'))
)
}
const serviceClasses = []
for await (const servicePath of servicePaths) {
const currentServiceClasses = Object.values(
await import(`file://${servicePath}`)
).flatMap(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`
)
}
currentServiceClasses.forEach(serviceClass => {
if (serviceClass && serviceClass.prototype instanceof BaseService) {
// Decorate each service class with the directory that contains it.
serviceClass.serviceFamily = servicePath
.replace(toUnixPath(serviceDir), '')
.split('/')[1]
serviceClass.validateDefinition()
return serviceClasses.push(serviceClass)
}
throw new InvalidService(
`Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}`
)
})
}
return serviceClasses
}
function assertNamesUnique(names, { message }) {
@@ -43,59 +77,14 @@ function assertNamesUnique(names, { message }) {
}
}
async function loadServiceClasses(servicePaths) {
if (!servicePaths) {
servicePaths = getServicePaths('*.service.js')
}
const serviceClasses = []
for await (const servicePath of servicePaths) {
const currentServiceClasses = Object.values(
await import(`file://${servicePath}`),
).flatMap(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`,
)
}
currentServiceClasses.forEach(serviceClass => {
if (serviceClass && serviceClass.prototype instanceof BaseService) {
// Decorate each service class with the directory that contains it.
serviceClass.serviceFamily = servicePath
.replace(serviceDir, '')
.split(path.sep)[1]
serviceClass.validateDefinition()
return serviceClasses.push(serviceClass)
}
throw new InvalidService(
`Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}`,
)
})
}
async function checkNames() {
const services = await loadServiceClasses()
assertNamesUnique(
serviceClasses.map(({ name }) => name),
services.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
)
}
async function collectDefinitions() {
@@ -113,16 +102,16 @@ async function collectDefinitions() {
async function loadTesters() {
return Promise.all(
getServicePaths('*.tester.js').map(
async path => await import(`file://${path}`),
),
glob
.sync(path.join(serviceDir, '**', '*.tester.js'))
.map(async path => await import(`file://${path}`))
)
}
export {
InvalidService,
loadServiceClasses,
getServicePaths,
checkNames,
collectDefinitions,
loadTesters,
}

View File

@@ -2,66 +2,56 @@ import path from 'path'
import { fileURLToPath } from 'url'
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import {
loadServiceClasses,
getServicePaths,
InvalidService,
} from './loader.js'
import { loadServiceClasses, InvalidService } from './loader.js'
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)
})
@@ -71,19 +61,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)
})
})
describe('getServicePaths', function () {
// these tests just make sure we discover a
// plausibly large number of .service and .tester files
it('finds a non-zero number of services in the project', function () {
expect(getServicePaths('*.service.js')).to.have.length.above(400)
})
it('finds a non-zero number of testers in the project', function () {
expect(getServicePaths('*.tester.js')).to.have.length.above(400)
})
})

View File

@@ -1,469 +0,0 @@
/**
* 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' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
]
function getCodeSamples(altText) {
return [
{
lang: 'URL',
label: 'URL',
source: '$url',
},
{
lang: 'Markdown',
label: 'Markdown',
source: `![${altText}]($url)`,
},
{
lang: 'reStructuredText',
label: 'rSt',
source: `.. image:: $url\n: alt: ${altText}`,
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: `image:$url[${altText}]`,
},
{
lang: 'HTML',
label: 'HTML',
source: `<img alt="${altText}" src="$url">`,
},
]
}
function pattern2openapi(pattern) {
return pattern
.replace(/:([A-Za-z0-9_\-.]+)(?=[/]?)/g, (matches, grp1) => `{${grp1}}`)
.replace(/\([^)]*\)/g, '')
.replace(/\+$/, '')
}
function getEnum(pattern, paramName) {
const re = new RegExp(`${paramName}\\(([A-Za-z0-9_\\-|]+)\\)`)
const match = pattern.match(re)
if (match === null) {
return undefined
}
if (!match[1].includes('|')) {
return undefined
}
return match[1].split('|')
}
function param2openapi(pattern, paramName, exampleValue, paramType) {
const outParam = {}
outParam.name = paramName
// We don't have description if we are building the OpenAPI spec from examples[]
outParam.in = paramType
if (paramType === 'path') {
outParam.required = true
} else {
/* Occasionally we do have required query params, but we can't
detect this if we are building the OpenAPI spec from examples[]
so just assume all query params are optional */
outParam.required = false
}
if (exampleValue === null && paramType === 'query') {
outParam.schema = { type: 'boolean' }
outParam.allowEmptyValue = true
} else {
outParam.schema = { type: 'string' }
}
if (paramType === 'path') {
outParam.schema.enum = getEnum(pattern, paramName)
}
outParam.example = exampleValue
return outParam
}
function getVariants(pattern) {
/*
given a URL pattern (which may include '/one/or/:more?/:optional/:parameters*')
return an array of all possible permutations:
[
'/one/or/:more/:optional/:parameters',
'/one/or/:optional/:parameters',
'/one/or/:more/:optional',
'/one/or/:optional',
]
*/
const patterns = [pattern.split('/')]
while (patterns.flat().find(p => p.endsWith('?') || p.endsWith('*'))) {
for (let i = 0; i < patterns.length; i++) {
const pattern = patterns[i]
for (let j = 0; j < pattern.length; j++) {
const path = pattern[j]
if (path.endsWith('?') || path.endsWith('*')) {
pattern[j] = path.slice(0, -1)
patterns.push(patterns[i].filter(p => p !== pattern[j]))
}
}
}
}
for (let i = 0; i < patterns.length; i++) {
patterns[i] = patterns[i].join('/')
}
return patterns
}
function examples2openapi(examples) {
const paths = {}
for (const example of examples) {
const patterns = getVariants(example.example.pattern)
for (const pattern of patterns) {
const openApiPattern = pattern2openapi(pattern)
if (
openApiPattern.includes('*') ||
openApiPattern.includes('?') ||
openApiPattern.includes('+') ||
openApiPattern.includes('(')
) {
throw new Error(`unexpected characters in pattern '${openApiPattern}'`)
}
/*
There's several things going on in this block:
1. Filter out any examples for params that don't appear
in this variant of the route
2. Make sure we add params to the array
in the same order they appear in the route
3. If there are any params we don't have an example value for,
make sure they still appear in the pathParams array with
exampleValue == undefined anyway
*/
const pathParams = []
for (const param of openApiPattern
.split('/')
.filter(p => p.startsWith('{') && p.endsWith('}'))) {
const paramName = param.slice(1, -1)
const exampleValue = example.example.namedParams[paramName]
pathParams.push(param2openapi(pattern, paramName, exampleValue, 'path'))
}
const queryParams = example.example.queryParams || {}
const parameters = [
...pathParams,
...Object.entries(queryParams).map(([paramName, exampleValue]) =>
param2openapi(pattern, paramName, exampleValue, 'query'),
),
...globalParamRefs,
]
paths[openApiPattern] = {
get: {
summary: example.title,
description: example?.documentation?.__html
.replace(/<br>/g, '<br />') // react does not like <br>
.replace(/{/g, '&#123;')
.replace(/}/g, '&#125;')
.replace(/<style>(.|\n)*?<\/style>/, ''), // workaround for w3c-validation TODO: remove later
parameters,
'x-code-samples': getCodeSamples(example.title),
},
}
}
}
return paths
}
function addGlobalProperties(endpoints) {
const paths = {}
for (const key of Object.keys(endpoints)) {
paths[key] = endpoints[key]
paths[key].get.parameters = [
...paths[key].get.parameters,
...globalParamRefs,
]
paths[key].get['x-code-samples'] = getCodeSamples(paths[key].get.summary)
}
return paths
}
function services2openapi(services) {
const paths = {}
for (const service of services) {
if (service.openApi) {
// if the service declares its own OpenAPI definition, use that...
for (const [key, value] of Object.entries(
addGlobalProperties(service.openApi),
)) {
if (key in paths) {
throw new Error(`Conflicting route: ${key}`)
}
paths[key] = value
}
} else {
// ...otherwise do our best to build one from examples[]
for (const [key, value] of Object.entries(
examples2openapi(service.examples),
)) {
// allow conflicting routes for legacy examples
paths[key] = value
}
}
}
return paths
}
function category2openapi(category, services) {
const spec = {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: category.name,
license: {
name: 'CC0',
},
},
servers: baseUrl ? [{ url: baseUrl }] : undefined,
components: {
parameters: {
style: {
name: 'style',
in: 'query',
required: false,
description:
'One of: flat (default), flat-square, plastic, for-the-badge, social',
schema: {
type: 'string',
},
example: 'flat',
},
logo: {
name: 'logo',
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. <a href="/docs/logos">Further info</a>.',
schema: {
type: 'string',
},
example: 'appveyor',
},
logoColor: {
name: 'logoColor',
in: 'query',
required: false,
description:
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
schema: {
type: 'string',
},
example: 'violet',
},
label: {
name: 'label',
in: 'query',
required: false,
description:
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
schema: {
type: 'string',
},
example: 'healthiness',
},
labelColor: {
name: 'labelColor',
in: 'query',
required: false,
description:
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: {
type: 'string',
},
example: 'abcdef',
},
color: {
name: 'color',
in: 'query',
required: false,
description:
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: {
type: 'string',
},
example: 'fedcba',
},
cacheSeconds: {
name: 'cacheSeconds',
in: 'query',
required: false,
description:
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
schema: {
type: 'string',
},
example: '3600',
},
link: {
name: 'link',
in: 'query',
required: false,
description:
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
style: 'form',
explode: true,
schema: {
type: 'array',
maxItems: 2,
items: {
type: 'string',
},
},
},
},
},
paths: services2openapi(services),
}
return spec
}
/**
* 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 }

View File

@@ -1,529 +0,0 @@
import chai from 'chai'
import {
category2openapi,
pathParam,
pathParams,
queryParam,
queryParams,
} from './openapi.js'
import BaseJsonService from './base-json.js'
const { expect } = chai
class OpenApiService extends BaseJsonService {
static category = 'build'
static route = { base: 'openapi/service', pattern: ':packageName/:distTag*' }
// this service defines its own API Paths Object
static openApi = {
'/openapi/service/{packageName}': {
get: {
summary: 'OpenApiService Summary',
description: 'OpenApiService Description',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
],
},
},
'/openapi/service/{packageName}/{distTag}': {
get: {
summary: 'OpenApiService Summary (with Tag)',
description: 'OpenApiService Description (with Tag)',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{
name: 'distTag',
description: 'distTag description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'latest',
},
],
},
},
}
}
class LegacyService extends BaseJsonService {
static category = 'build'
static route = { base: 'legacy/service', pattern: ':packageName/:distTag*' }
// this service defines an Examples Array
static examples = [
{
title: 'LegacyService Title',
namedParams: { packageName: 'badge-maker' },
staticPreview: { label: 'build', message: 'passing' },
documentation: 'LegacyService Description',
},
{
title: 'LegacyService Title (with Tag)',
namedParams: { packageName: 'badge-maker', distTag: 'latest' },
staticPreview: { label: 'build', message: 'passing' },
documentation: 'LegacyService Description (with Tag)',
},
]
}
const expected = {
openapi: '3.0.0',
info: { version: '1.0.0', title: 'build', license: { name: 'CC0' } },
components: {
parameters: {
style: {
name: 'style',
in: 'query',
required: false,
description:
'One of: flat (default), flat-square, plastic, for-the-badge, social',
schema: { type: 'string' },
example: 'flat',
},
logo: {
name: 'logo',
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. <a href="/docs/logos">Further info</a>.',
schema: { type: 'string' },
example: 'appveyor',
},
logoColor: {
name: 'logoColor',
in: 'query',
required: false,
description:
'The color of the logo (hex, rgb, rgba, hsl, hsla and css named colors supported). Supported for named logos and Shields logos but not for custom logos. For multicolor Shields logos, the corresponding named logo will be used and colored.',
schema: { type: 'string' },
example: 'violet',
},
label: {
name: 'label',
in: 'query',
required: false,
description:
'Override the default left-hand-side text (<a href="https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding">URL-Encoding</a> needed for spaces or special characters!)',
schema: { type: 'string' },
example: 'healthiness',
},
labelColor: {
name: 'labelColor',
in: 'query',
required: false,
description:
'Background color of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: { type: 'string' },
example: 'abcdef',
},
color: {
name: 'color',
in: 'query',
required: false,
description:
'Background color of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported).',
schema: { type: 'string' },
example: 'fedcba',
},
cacheSeconds: {
name: 'cacheSeconds',
in: 'query',
required: false,
description:
'HTTP cache lifetime (rules are applied to infer a default value on a per-badge basis, any values specified below the default will be ignored).',
schema: { type: 'string' },
example: '3600',
},
link: {
name: 'link',
in: 'query',
required: false,
description:
'Specify what clicking on the left/right of a badge should do. Note that this only works when integrating your badge in an `<object>` HTML tag, but not an `<img>` tag or a markup language.',
style: 'form',
explode: true,
schema: { type: 'array', maxItems: 2, items: { type: 'string' } },
},
},
},
paths: {
'/openapi/service/{packageName}': {
get: {
summary: 'OpenApiService Summary',
description: 'OpenApiService Description',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![OpenApiService Summary]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source: '.. image:: $url\n: alt: OpenApiService Summary',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[OpenApiService Summary]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="OpenApiService Summary" src="$url">',
},
],
},
},
'/openapi/service/{packageName}/{distTag}': {
get: {
summary: 'OpenApiService Summary (with Tag)',
description: 'OpenApiService Description (with Tag)',
parameters: [
{
name: 'packageName',
description: 'packageName description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{
name: 'distTag',
description: 'distTag description',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'latest',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![OpenApiService Summary (with Tag)]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source:
'.. image:: $url\n: alt: OpenApiService Summary (with Tag)',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[OpenApiService Summary (with Tag)]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="OpenApiService Summary (with Tag)" src="$url">',
},
],
},
},
'/legacy/service/{packageName}/{distTag}': {
get: {
summary: 'LegacyService Title (with Tag)',
description: 'LegacyService Description (with Tag)',
parameters: [
{
name: 'packageName',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{
name: 'distTag',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'latest',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![LegacyService Title (with Tag)]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[LegacyService Title (with Tag)]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
},
],
},
},
'/legacy/service/{packageName}': {
get: {
summary: 'LegacyService Title (with Tag)',
description: 'LegacyService Description (with Tag)',
parameters: [
{
name: 'packageName',
in: 'path',
required: true,
schema: { type: 'string' },
example: 'badge-maker',
},
{ $ref: '#/components/parameters/style' },
{ $ref: '#/components/parameters/logo' },
{ $ref: '#/components/parameters/logoColor' },
{ $ref: '#/components/parameters/label' },
{ $ref: '#/components/parameters/labelColor' },
{ $ref: '#/components/parameters/color' },
{ $ref: '#/components/parameters/cacheSeconds' },
{ $ref: '#/components/parameters/link' },
],
'x-code-samples': [
{ lang: 'URL', label: 'URL', source: '$url' },
{
lang: 'Markdown',
label: 'Markdown',
source: '![LegacyService Title (with Tag)]($url)',
},
{
lang: 'reStructuredText',
label: 'rSt',
source: '.. image:: $url\n: alt: LegacyService Title (with Tag)',
},
{
lang: 'AsciiDoc',
label: 'AsciiDoc',
source: 'image:$url[LegacyService Title (with Tag)]',
},
{
lang: 'HTML',
label: 'HTML',
source: '<img alt="LegacyService Title (with Tag)" src="$url">',
},
],
},
},
},
}
function clean(obj) {
// remove any undefined values in the object
return JSON.parse(JSON.stringify(obj))
}
describe('category2openapi', function () {
it('generates an Open API spec', function () {
expect(
clean(
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,
},
])
})
})

View File

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

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