Compare commits
49 Commits
server-202
...
4.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8129904f | ||
|
|
677e713310 | ||
|
|
92718ff98b | ||
|
|
0c17d9d44d | ||
|
|
02608e1242 | ||
|
|
250686f147 | ||
|
|
35d5e9d964 | ||
|
|
8eff4f51e4 | ||
|
|
1445c3d21a | ||
|
|
95382dd5ce | ||
|
|
5ae8b29923 | ||
|
|
8303f43f2a | ||
|
|
3509d1faf6 | ||
|
|
283ef0bade | ||
|
|
d2344ed220 | ||
|
|
5aae2fdbd5 | ||
|
|
870b3f4be2 | ||
|
|
d69661600e | ||
|
|
05f0d866da | ||
|
|
2f2df5ba06 | ||
|
|
f905aa4fbe | ||
|
|
497189f007 | ||
|
|
73c4f45eee | ||
|
|
b5c69ce1bb | ||
|
|
ed9f4bf2ff | ||
|
|
18e9b2aa37 | ||
|
|
95df3200f4 | ||
|
|
8698fa1838 | ||
|
|
6be6c1d59f | ||
|
|
0b4f0c837a | ||
|
|
d33d57170f | ||
|
|
0360ba3a1b | ||
|
|
82769bd588 | ||
|
|
ba6bddf4bc | ||
|
|
3bd604bc8e | ||
|
|
d0f17a4f00 | ||
|
|
cfa6bb3d69 | ||
|
|
7a5628587e | ||
|
|
db0ed00d0e | ||
|
|
3fd95638de | ||
|
|
f6f01a9337 | ||
|
|
71d11eea4b | ||
|
|
2b7951f9de | ||
|
|
1e7819dad4 | ||
|
|
437adc67ce | ||
|
|
d23f26b8d6 | ||
|
|
81585acd6b | ||
|
|
f1727a38c1 | ||
|
|
39a17b69d3 |
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -16,7 +16,20 @@ updates:
|
||||
# https://caniuse.com/js-regexp-lookbehind
|
||||
- dependency-name: 'decamelize'
|
||||
- dependency-name: 'humanize-string'
|
||||
|
||||
groups:
|
||||
# All official @docusaurus/* packages should have the exact same version as @docusaurus/core.
|
||||
# From https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups:
|
||||
# "You cannot apply a single grouping set of rules to both version updates and security
|
||||
# updates [...] you must define two, separately named, grouping sets of rules"
|
||||
# See https://github.com/badges/shields/issues/10242 for more information.
|
||||
docusaurus-version-updates:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- '@docusaurus/*'
|
||||
docusaurus-security-updates:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- '@docusaurus/*'
|
||||
# badge-maker package dependencies
|
||||
- package-ecosystem: npm
|
||||
directory: '/badge-maker'
|
||||
|
||||
2
.github/workflows/build-docker-image.yml
vendored
2
.github/workflows/build-docker-image.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
4
.github/workflows/create-release.yml
vendored
4
.github/workflows/create-release.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to DockerHub
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push snapshot release to GHCR
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
4
.github/workflows/publish-docker-next.yml
vendored
4
.github/workflows/publish-docker-next.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- name: Build and push to DockerHub
|
||||
id: docker_build_push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to GHCR
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 4.0.0 [WIP]
|
||||
## 4.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Drop compatibility with Node < 16
|
||||
|
||||
### Features
|
||||
|
||||
- Add `links` and `logoBase64` params
|
||||
|
||||
## 3.3.1
|
||||
|
||||
- Improve font measuring in for-the-badge and social styles
|
||||
|
||||
@@ -67,6 +67,8 @@ The format is the following:
|
||||
message: 'passed', // (Required) Badge message
|
||||
labelColor: '#555', // (Optional) Label color
|
||||
color: '#4c1', // (Optional) Message color
|
||||
logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iI2IxY2U1NiIvPjxwYXRoIGQ9Ik04IDBoMjR2NjRIOGMtNC40MzIgMC04LTMuNTY4LTgtOFY4YzAtNC40MzIgMy41NjgtOCA4LTh6IiBmaWxsPSIjNWQ1ZDVkIi8+PC9zdmc+' // (Optional) Any custom logo can be passed in a URL parameter by base64 encoding
|
||||
links: ['https://example.com', 'https://example.com'], // (Optional) Links array of maximum two links
|
||||
|
||||
// (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social'
|
||||
// Each offers a different visual design.
|
||||
|
||||
2
badge-maker/index.d.ts
vendored
2
badge-maker/index.d.ts
vendored
@@ -4,6 +4,8 @@ interface Format {
|
||||
labelColor?: string
|
||||
color?: string
|
||||
style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social'
|
||||
logoBase64?: string
|
||||
links?: Array<string>
|
||||
}
|
||||
|
||||
export declare class ValidationError extends Error {}
|
||||
|
||||
@@ -16,13 +16,30 @@ function _validate(format) {
|
||||
throw new ValidationError('Field `message` is required')
|
||||
}
|
||||
|
||||
const stringFields = ['labelColor', 'color', 'message', 'label']
|
||||
const stringFields = ['labelColor', 'color', 'message', 'label', 'logoBase64']
|
||||
stringFields.forEach(function (field) {
|
||||
if (field in format && typeof format[field] !== 'string') {
|
||||
throw new ValidationError(`Field \`${field}\` must be of type string`)
|
||||
}
|
||||
})
|
||||
|
||||
if ('links' in format) {
|
||||
if (!Array.isArray(format.links)) {
|
||||
throw new ValidationError('Field `links` must be an array of strings')
|
||||
} else {
|
||||
if (format.links.length > 2) {
|
||||
throw new ValidationError(
|
||||
'Field `links` must not have more than 2 elements',
|
||||
)
|
||||
}
|
||||
format.links.forEach(function (field) {
|
||||
if (typeof field !== 'string') {
|
||||
throw new ValidationError('Field `links` must be an array of strings')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const styleValues = [
|
||||
'plastic',
|
||||
'flat',
|
||||
@@ -38,11 +55,21 @@ function _validate(format) {
|
||||
}
|
||||
|
||||
function _clean(format) {
|
||||
const expectedKeys = ['label', 'message', 'labelColor', 'color', 'style']
|
||||
const expectedKeys = [
|
||||
'label',
|
||||
'message',
|
||||
'labelColor',
|
||||
'color',
|
||||
'style',
|
||||
'logoBase64',
|
||||
'links',
|
||||
]
|
||||
|
||||
const cleaned = {}
|
||||
Object.keys(format).forEach(key => {
|
||||
if (format[key] != null && expectedKeys.includes(key)) {
|
||||
if (format[key] != null && key === 'logoBase64') {
|
||||
cleaned.logo = format[key]
|
||||
} else if (format[key] != null && expectedKeys.includes(key)) {
|
||||
cleaned[key] = format[key]
|
||||
} else {
|
||||
throw new ValidationError(
|
||||
@@ -65,7 +92,9 @@ function _clean(format) {
|
||||
* @param {string} format.message (Required) Badge message (e.g: 'passing')
|
||||
* @param {string} format.labelColor (Optional) Label color
|
||||
* @param {string} format.color (Optional) Message color
|
||||
* @param {string} format.style (Optional) Visual style e.g: 'flat'
|
||||
* @param {string} format.style (Optional) Visual style (e.g: 'flat')
|
||||
* @param {string} format.logoBase64 (Optional) Logo data URL
|
||||
* @param {Array} format.links (Optional) Links array (e.g: ['https://example.com', 'https://example.com'])
|
||||
* @returns {string} Badge in SVG format
|
||||
* @see https://github.com/badges/shields/tree/master/badge-maker/README.md
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,21 @@ describe('makeBadge function', function () {
|
||||
style: 'flat',
|
||||
}),
|
||||
).to.satisfy(isSvg)
|
||||
expect(
|
||||
makeBadge({
|
||||
label: 'build',
|
||||
message: 'passed',
|
||||
color: 'green',
|
||||
style: 'flat',
|
||||
labelColor: 'blue',
|
||||
logoBase64: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
links: ['https://example.com', 'https://example.com'],
|
||||
}),
|
||||
)
|
||||
.to.satisfy(isSvg)
|
||||
// explicitly make an assertion about logoBase64
|
||||
// this param is not a straight passthrough
|
||||
.and.to.include('data:image/svg+xml;base64,PHN2ZyB4bWxu')
|
||||
})
|
||||
|
||||
it('should throw a ValidationError with invalid inputs', function () {
|
||||
@@ -46,6 +61,21 @@ describe('makeBadge function', function () {
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', labelColor: 7 }),
|
||||
).to.throw(ValidationError, 'Field `labelColor` must be of type string')
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', logoBase64: 7 }),
|
||||
).to.throw(ValidationError, 'Field `logoBase64` must be of type string')
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', links: 'test' }),
|
||||
).to.throw(ValidationError, 'Field `links` must be an array of strings')
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', links: [1] }),
|
||||
).to.throw(ValidationError, 'Field `links` must be an array of strings')
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', links: ['1', '2', '3'] }),
|
||||
).to.throw(
|
||||
ValidationError,
|
||||
'Field `links` must not have more than 2 elements',
|
||||
)
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', format: 'png' }),
|
||||
).to.throw(ValidationError, "Unexpected field 'format'")
|
||||
|
||||
@@ -17,7 +17,6 @@ module.exports = function makeBadge({
|
||||
color,
|
||||
labelColor,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoSize,
|
||||
logoWidth,
|
||||
links = ['', ''],
|
||||
@@ -55,7 +54,6 @@ module.exports = function makeBadge({
|
||||
message,
|
||||
links,
|
||||
logo,
|
||||
logoPosition,
|
||||
logoWidth,
|
||||
logoSize,
|
||||
logoPadding: logo && label.length ? 3 : 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "badge-maker",
|
||||
"version": "3.3.1",
|
||||
"version": "4.0.0",
|
||||
"description": "Shields.io badge library",
|
||||
"keywords": [
|
||||
"GitHub",
|
||||
|
||||
@@ -66,7 +66,6 @@ const serviceDataSchema = Joi.object({
|
||||
logoSvg: Joi.string(),
|
||||
logoColor: optionalStringWhenNamedLogoPresent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
logoPosition: optionalNumberWhenAnyLogoPresent,
|
||||
cacheSeconds: Joi.number().integer().min(0),
|
||||
style: Joi.string(),
|
||||
})
|
||||
|
||||
@@ -378,7 +378,6 @@ describe('BaseService', function () {
|
||||
logo: undefined,
|
||||
logoWidth: undefined,
|
||||
logoSize: undefined,
|
||||
logoPosition: undefined,
|
||||
links: [],
|
||||
labelColor: undefined,
|
||||
cacheLengthSeconds: undefined,
|
||||
|
||||
@@ -23,9 +23,8 @@ import toArray from './to-array.js'
|
||||
// base64-encoded logos). Otherwise the default color is used. If the color
|
||||
// is specified for a multicolor Shield logo, the named logo will be used and
|
||||
// colored. The appearance of the logo can be customized using `logoWidth`,
|
||||
// and in the case of the popout badge, `logoPosition`. When `?logo=` is
|
||||
// specified, any logo-related parameters specified dynamically by the
|
||||
// service, or by default in the service, are ignored.
|
||||
// When `?logo=` is specified, any logo-related parameters specified
|
||||
// dynamically by the service, or by default in the service, are ignored.
|
||||
// 2. The second precedence is the dynamic logo returned by a service. This is
|
||||
// used only by the Endpoint badge. The `logoColor` can be overridden by the
|
||||
// query string.
|
||||
@@ -56,7 +55,6 @@ export default function coalesceBadge(
|
||||
} = overrides
|
||||
let {
|
||||
logoWidth: overrideLogoWidth,
|
||||
logoPosition: overrideLogoPosition,
|
||||
logoSize: overrideLogoSize,
|
||||
color: overrideColor,
|
||||
labelColor: overrideLabelColor,
|
||||
@@ -78,7 +76,6 @@ export default function coalesceBadge(
|
||||
overrideLabelColor = `${overrideLabelColor}`
|
||||
}
|
||||
overrideLogoWidth = +overrideLogoWidth || undefined
|
||||
overrideLogoPosition = +overrideLogoPosition || undefined
|
||||
|
||||
const {
|
||||
isError,
|
||||
@@ -91,7 +88,6 @@ export default function coalesceBadge(
|
||||
logoColor: serviceLogoColor,
|
||||
logoSize: serviceLogoSize,
|
||||
logoWidth: serviceLogoWidth,
|
||||
logoPosition: serviceLogoPosition,
|
||||
link: serviceLink,
|
||||
cacheSeconds: serviceCacheSeconds,
|
||||
style: serviceStyle,
|
||||
@@ -122,12 +118,7 @@ export default function coalesceBadge(
|
||||
style = 'flat'
|
||||
}
|
||||
|
||||
let namedLogo,
|
||||
namedLogoColor,
|
||||
logoSize,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
logoSvgBase64
|
||||
let namedLogo, namedLogoColor, logoSize, logoWidth, logoSvgBase64
|
||||
if (overrideLogo) {
|
||||
// `?logo=` could be a named logo or encoded svg.
|
||||
const overrideLogoSvgBase64 = decodeDataUrlFromQueryParam(overrideLogo)
|
||||
@@ -143,7 +134,6 @@ export default function coalesceBadge(
|
||||
// original width or position.
|
||||
logoSize = overrideLogoSize
|
||||
logoWidth = overrideLogoWidth
|
||||
logoPosition = overrideLogoPosition
|
||||
} else {
|
||||
if (serviceLogoSvg) {
|
||||
logoSvgBase64 = svg2base64(serviceLogoSvg)
|
||||
@@ -156,7 +146,6 @@ export default function coalesceBadge(
|
||||
}
|
||||
logoSize = coalesce(overrideLogoSize, serviceLogoSize)
|
||||
logoWidth = coalesce(overrideLogoWidth, serviceLogoWidth)
|
||||
logoPosition = coalesce(overrideLogoPosition, serviceLogoPosition)
|
||||
}
|
||||
if (namedLogo) {
|
||||
const iconSize = getIconSize(String(namedLogo).toLowerCase())
|
||||
@@ -195,7 +184,6 @@ export default function coalesceBadge(
|
||||
namedLogo,
|
||||
logo: logoSvgBase64,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
logoSize,
|
||||
links: toArray(overrideLink || serviceLink),
|
||||
cacheLengthSeconds: coalesce(serviceCacheSeconds, defaultCacheSeconds),
|
||||
|
||||
@@ -195,14 +195,13 @@ describe('coalesceBadge', function () {
|
||||
).to.equal(getSimpleIcon({ name: 'npm', color: 'blue' })).and.not.be.empty
|
||||
})
|
||||
|
||||
it("when the logo is overridden, it ignores the service's logo color, position, and width", function () {
|
||||
it("when the logo is overridden, it ignores the service's logo color and width", function () {
|
||||
expect(
|
||||
coalesceBadge(
|
||||
{ logo: 'npm' },
|
||||
{
|
||||
namedLogo: 'appveyor',
|
||||
logoColor: 'red',
|
||||
logoPosition: -3,
|
||||
logoWidth: 100,
|
||||
},
|
||||
{},
|
||||
@@ -288,20 +287,6 @@ describe('coalesceBadge', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logo position', function () {
|
||||
it('overrides the logoPosition', function () {
|
||||
expect(coalesceBadge({ logoPosition: -10 }, {}, {})).to.include({
|
||||
logoPosition: -10,
|
||||
})
|
||||
})
|
||||
|
||||
it('applies the logo position', function () {
|
||||
expect(
|
||||
coalesceBadge({}, { namedLogo: 'npm', logoPosition: -10 }, {}),
|
||||
).to.include({ logoPosition: -10 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Links', function () {
|
||||
it('overrides the links', function () {
|
||||
expect(
|
||||
|
||||
@@ -11,7 +11,6 @@ const globalQueryParams = new Set([
|
||||
'link',
|
||||
'logo',
|
||||
'logoColor',
|
||||
'logoPosition',
|
||||
'logoSize',
|
||||
'logoWidth',
|
||||
'link',
|
||||
|
||||
@@ -71,7 +71,7 @@ Each service has a directory for its files:
|
||||
`/services/example/example.service.js`.
|
||||
If you add a badge for a new API, create a new directory.
|
||||
|
||||
Example: [wercker](https://github.com/badges/shields/tree/master/services/wercker)
|
||||
Example: [Docs.rs](https://github.com/badges/shields/tree/master/services/docsrs)
|
||||
|
||||
- For service families with multiple badges we usually store the code for each
|
||||
badge in its own file like this:
|
||||
|
||||
@@ -12,7 +12,7 @@ automated tests should be included. They serve three purposes:
|
||||
3. They speed up future contributors when they are debugging or improving a
|
||||
badge.
|
||||
|
||||
Test should cover:
|
||||
Tests should cover:
|
||||
|
||||
1. Valid behavior
|
||||
2. Optional parameters like tags or branches
|
||||
@@ -24,11 +24,11 @@ Test should cover:
|
||||
Before getting started, set up a development environment by following the
|
||||
[setup instructions](https://github.com/badges/shields/blob/master/doc/TUTORIAL.md#2-setup)
|
||||
|
||||
We will write some tests for the [Wercker Build service](https://github.com/badges/shields/blob/master/services/wercker/wercker.service.js)
|
||||
We will write some tests for [Docs.rs](https://github.com/badges/shields/blob/master/services/docsrs/docsrs.service.js), a service that builds documentation of crates, which are packages in the Rust programming language.
|
||||
|
||||
### (1) Boilerplate
|
||||
|
||||
The code for our badge is in `services/wercker/wercker.service.js`. Tests for this badge should be stored in `services/wercker/wercker.tester.js`.
|
||||
The code for our badge is in `services/docsrs/docsrs.service.js`. Tests for this badge should be stored in `services/docsrs/docsrs.tester.js`.
|
||||
|
||||
We'll start by adding some boilerplate to our file:
|
||||
|
||||
@@ -41,8 +41,8 @@ export const t = await createServiceTester()
|
||||
If our `.service.js` module exports a single class, we can
|
||||
`createServiceTester`, which uses convention to create a
|
||||
`ServiceTester` object. Calling this inside
|
||||
`services/wercker/wercker.tester.js` will create a `ServiceTester` object
|
||||
configured for the service exported in `services/wercker/wercker.service.js`.
|
||||
`services/docsrs/docsrs.tester.js` will create a `ServiceTester` object
|
||||
configured for the service exported in `services/docsrs/docsrs.service.js`.
|
||||
We will add our tests to this `ServiceTester` object `t`, which is exported
|
||||
from the module.
|
||||
|
||||
@@ -51,27 +51,30 @@ from the module.
|
||||
First we'll add a test for the typical case:
|
||||
|
||||
```js
|
||||
import { isBuildStatus } from '../test-validators.js'
|
||||
import Joi from 'joi'
|
||||
|
||||
t.create('Build status')
|
||||
.get('/build/wercker/go-wercker-api.json')
|
||||
.expectBadge({ label: 'build', message: isBuildStatus })
|
||||
t.create('Docs with no version specified')
|
||||
.get('/tokio.json')
|
||||
.expectBadge({
|
||||
label: 'docs',
|
||||
message: Joi.allow('passing', 'failing'),
|
||||
})
|
||||
```
|
||||
|
||||
1. The `create()` method adds a new test to the tester object.
|
||||
The chained-on calls come from the API testing framework [IcedFrisby][].
|
||||
Here's a [longer example][] and the complete [API guide][icedfrisby api].
|
||||
2. We use the `get()` method to request a badge. There are several points to consider here:
|
||||
- We need a real project to test against. In this case we have used [wercker/go-wercker-api](https://app.wercker.com/wercker/go-wercker-api/runs) but we could have chosen any stable project.
|
||||
- We need a real crate to test against. In this case we have used [Tokio](https://docs.rs/tokio) but we could have chosen any one.
|
||||
- Note that when we call our badge, we are allowing it to communicate with an external service without mocking the response. We write tests which interact with external services, which is unusual practice in unit testing. We do this because one of the purposes of service tests is to notify us if a badge has broken due to an upstream API change. For this reason it is important for at least one test to call the live API without mocking the interaction.
|
||||
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/wercker/build/wercker/go-wercker-api.svg to generate  we can also call https://img.shields.io/wercker/build/wercker/go-wercker-api.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
|
||||
- We don't need to explicitly call `/wercker/build/wercker/go-wercker-api.json` here, only `/build/wercker/go-wercker-api.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/wercker`) is used as the base URL for any requests made by the tester object.
|
||||
- All badges on shields can be requested in a number of formats. As well as calling https://img.shields.io/docsrs/tokio.svg to generate  we can also call https://img.shields.io/docsrs/tokio.json to request the same content as JSON. When writing service tests, we request the badge in JSON format so it is easier to make assertions about the content.
|
||||
- We don't need to explicitly call `/docsrs/tokio.json` here, only `/tokio.json`. When we create a tester object with `createServiceTester()` the URL base defined in our service class (in this case `/docsrs`) is used as the base URL for any requests made by the tester object.
|
||||
3. `expectBadge()` is a helper function which accepts either a string literal, a [RegExp][] or a [Joi][] schema for the different fields.
|
||||
Joi is a validation library that is built into IcedFrisby which you can use to
|
||||
match based on a set of allowed strings, regexes, or specific values. You can
|
||||
refer to their [API reference][joi api].
|
||||
4. We expect `label` to be a string literal `"build"`.
|
||||
5. Because this test depends on a live service, we don't want our test to depend on our API call returning a particular build status. Instead we should perform a "picture check" to assert that the badge data conforms to an expected pattern. Our test should not depend on the status of the example project's build, but should fail if trying to generate the badge throws an error, or if there is a breaking change to the upstream API. In this case we will use a pre-defined regular expression to check that the badge value looks like a build status. [services/test-validators.js](https://github.com/badges/shields/blob/master/services/test-validators.js) defines a number of useful validators we can use. Many of the common badge types (version, downloads, rank, etc.) already have validators defined here.
|
||||
4. We expect `label` to be a string literal `"docs"`.
|
||||
5. Because this test depends on a live service, we don't want our test to depend on our API call returning a particular build status. Instead we should perform a "picture check" to assert that the badge data conforms to an expected pattern. Our test should not depend on the status of the example crates's documentation build, but should fail if trying to generate the badge throws an error, or if there is a breaking change to the upstream API. In this case, we specify a list with all possible response values, `Joi.allow('passing', 'failing')`. For more complex cases, [services/test-validators.js](https://github.com/badges/shields/blob/master/services/test-validators.js) defines a number of useful validators we can use with regular expressions. Many of the common badge types (version, downloads, rank, etc.) already have validators defined there.
|
||||
|
||||
When defining an IcedFrisby test, typically you would invoke the `toss()`
|
||||
method, to register the test. This is not necessary, because the Shields test
|
||||
@@ -89,7 +92,7 @@ harness will call it for you.
|
||||
Lets run the test we have written:
|
||||
|
||||
```
|
||||
npm run test:services -- --only=wercker
|
||||
npm run test:services -- --only=docsrs
|
||||
```
|
||||
|
||||
The `--only=` option indicates which service or services you want to test. You
|
||||
@@ -101,11 +104,12 @@ runner.
|
||||
Here's the output:
|
||||
|
||||
```
|
||||
Server is starting up: http://lib/service-test-runner/cli.js:80/
|
||||
Wercker
|
||||
Build status
|
||||
✓
|
||||
[ GET /build/wercker/go-wercker-api.json ] (572ms)
|
||||
Server is starting up: http://localhost:1111/
|
||||
DocsRs
|
||||
[live] Docs with no version specified
|
||||
√
|
||||
[ GET /tokio.json ] (441ms)
|
||||
|
||||
|
||||
1 passing (1s)
|
||||
```
|
||||
@@ -115,80 +119,98 @@ That's looking good!
|
||||
Sometimes if we have a failing test, it is useful to be able to see some logging output to help work out why the test is failing. We can do that by calling `npm run test:services:trace`. Try running
|
||||
|
||||
```
|
||||
npm run test:services:trace -- --only=wercker
|
||||
npm run test:services:trace -- --only=docsrs
|
||||
```
|
||||
|
||||
to run the test with some additional debug output.
|
||||
|
||||
### (4) Writing More Tests
|
||||
|
||||
We should write tests cases for valid paths through our code. The Wercker badge supports an optional branch parameter so we'll add a second test for a branch build.
|
||||
We should write tests cases for valid paths through our code. The Docs.rs badge supports an optional version parameter so we'll add a second test for a branch build. In this case, we know for sure that the documentation for this older version was successfully built, we specify a string literal instead of a Joi schema for `message`. This narrows down the expectation and gives us a more helpful error message if the test fails.
|
||||
|
||||
```js
|
||||
t.create('Build status (with branch)')
|
||||
.get('/build/wercker/go-wercker-api/master.json')
|
||||
.expectBadge({ label: 'build', message: isBuildStatus })
|
||||
t.create('Passing docs for version').get('/tokio/1.37.0.json').expectBadge({
|
||||
label: 'docs@1.37.0',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
```
|
||||
|
||||
```
|
||||
Server is starting up: http://lib/service-test-runner/cli.js:80/
|
||||
Wercker
|
||||
Build status
|
||||
✓
|
||||
[ GET /build/wercker/go-wercker-api.json ] (572ms)
|
||||
Build status (with branch)
|
||||
✓
|
||||
[ GET /build/wercker/go-wercker-api/master.json ] (368ms)
|
||||
Server is starting up: http://localhost:1111/
|
||||
DocsRs
|
||||
[live] Docs with no version specified
|
||||
√
|
||||
[ GET /tokio.json ] (408ms)
|
||||
[live] Passing docs for version
|
||||
√
|
||||
[ GET /tokio/1.37.0.json ] (171ms)
|
||||
|
||||
2 passing (1s)
|
||||
|
||||
2 passing (2s)
|
||||
```
|
||||
|
||||
Once we have multiple tests, sometimes it is useful to run only one test. We can do this using the `--fgrep` argument. For example:
|
||||
|
||||
```
|
||||
npm run test:services -- --only="wercker" --fgrep="Build status (with branch)"
|
||||
npm run test:services -- --only="docsrs" --fgrep="Passing docs for version"
|
||||
```
|
||||
|
||||
Having covered the typical and custom cases, we'll move on to errors. We should include a test for the 'not found' response and also tests for any other custom error handling. The Wercker integration defines a custom error condition for 401 as well as a custom 404 message:
|
||||
Documentation for tokio version 1.32.1 failed to build, we can also add a corresponding test:
|
||||
|
||||
```js
|
||||
httpErrors: {
|
||||
401: 'private application not supported',
|
||||
404: 'application not found',
|
||||
}
|
||||
t.create('Failing docs for version').get('/tokio/1.32.1.json').expectBadge({
|
||||
label: 'docs@1.32.1',
|
||||
message: 'failing',
|
||||
color: 'red',
|
||||
})
|
||||
```
|
||||
|
||||
First we'll add a test for a project which will return a 404 error:
|
||||
Note that in these tests, we have specified a `color` parameter in `expectBadge`. This is helpful in a case like this when we want to test custom color logic, but it is only necessary to explicitly test color values if our badge implements custom logic for setting the badge colors.
|
||||
|
||||
Having covered the typical and custom cases, we'll move on to errors. We should include a test for the 'not found' response and also tests for any other custom error handling. When a version is specified, the Docs.rs integration defines a custom error condition for 400 status codes:
|
||||
|
||||
```js
|
||||
t.create('Build status (application not found)')
|
||||
.get('/build/some-project/that-doesnt-exist.json')
|
||||
.expectBadge({ label: 'build', message: 'application not found' })
|
||||
httpErrors: version ? { 400: 'malformed version' } : {},
|
||||
```
|
||||
|
||||
In this case we are expecting a string literal instead of a pattern for `message`. This narrows down the expectation and gives us a more helpful error message if the test fails.
|
||||
|
||||
We also want to include a test for the 'private application not supported' case. One way to do this would be to find another example of a private project which is unlikely to change. For example:
|
||||
First we'll add a test for a crate and a test for a version which will return 404 errors:
|
||||
|
||||
```js
|
||||
t.create('Build status (private application)')
|
||||
.get('/build/wercker/blueprint.json')
|
||||
.expectBadge({ label: 'build', message: 'private application not supported' })
|
||||
t.create('Crate not found')
|
||||
.get('/not-a-crate/latest.json')
|
||||
.expectBadge({ label: 'docs', message: 'not found' })
|
||||
|
||||
t.create('Version not found')
|
||||
.get('/tokio/0.8.json')
|
||||
.expectBadge({ label: 'docs', message: 'not found' })
|
||||
```
|
||||
|
||||
We also want to include a test for a case where a malformed version was specified. For example:
|
||||
|
||||
```js
|
||||
t.create('Malformed version')
|
||||
.get('/tokio/not-a-version.json')
|
||||
.expectBadge({ label: 'docs', message: 'malformed version' })
|
||||
```
|
||||
|
||||
## (5) Mocking Responses
|
||||
|
||||
If we didn't have a stable example of a private project, another approach would be to mock the response. An alternative test for the 'private application' case might look like:
|
||||
If we didn't have a stable example of crate version with a failing documentation build, another approach would be to mock the response. An alternative test for the 'Failing docs for version' case might look like:
|
||||
|
||||
```js
|
||||
t.create('Build status (private application)')
|
||||
.get('/build/wercker/go-wercker-api.json')
|
||||
t.create('Failing docs for version')
|
||||
.get('/tokio/1.32.1.json')
|
||||
.intercept(nock =>
|
||||
nock('https://app.wercker.com/api/v3/applications/')
|
||||
.get('/wercker/go-wercker-api/builds?limit=1')
|
||||
.reply(401),
|
||||
nock('https://docs.rs/crate')
|
||||
.get('/tokio/1.32.1/status.json')
|
||||
.reply(200, { doc_status: false }),
|
||||
)
|
||||
.expectBadge({ label: 'build', message: 'private application not supported' })
|
||||
.expectBadge({
|
||||
label: 'docs@1.32.1',
|
||||
message: 'failing',
|
||||
color: 'red',
|
||||
})
|
||||
```
|
||||
|
||||
This will intercept the request and provide our own mock response.
|
||||
@@ -204,49 +226,6 @@ and path.
|
||||
[icedfrisby-nock]: https://github.com/paulmelnikow/icedfrisby-nock#usage
|
||||
[nock]: https://github.com/node-nock/nock
|
||||
|
||||
Our test suite should also include service tests which receive a known value from the API. For example, in the `render()` method of our service, there is some logic which sets the badge color based on the build status:
|
||||
|
||||
```js
|
||||
static render({ status, result }) {
|
||||
if (status === 'finished') {
|
||||
if (result === 'passed') {
|
||||
return { message: 'passing', color: 'brightgreen' }
|
||||
} else {
|
||||
return { message: result, color: 'red' }
|
||||
}
|
||||
}
|
||||
return { message: status }
|
||||
}
|
||||
```
|
||||
|
||||
We can also use nock to intercept API calls to return a known response body.
|
||||
|
||||
```js
|
||||
t.create('Build passed')
|
||||
.get('/build/wercker/go-wercker-api.json')
|
||||
.intercept(nock =>
|
||||
nock('https://app.wercker.com/api/v3/applications/')
|
||||
.get('/wercker/go-wercker-api/builds?limit=1')
|
||||
.reply(200, [{ status: 'finished', result: 'passed' }]),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('Build failed')
|
||||
.get('/build/wercker/go-wercker-api.json')
|
||||
.intercept(nock =>
|
||||
nock('https://app.wercker.com/api/v3/applications/')
|
||||
.get('/wercker/go-wercker-api/builds?limit=1')
|
||||
.reply(200, [{ status: 'finished', result: 'failed' }]),
|
||||
)
|
||||
.expectBadge({ label: 'build', message: 'failed', color: 'red' })
|
||||
```
|
||||
|
||||
Note that in these tests, we have specified a `color` parameter in `expectBadge`. This is helpful in a case like this when we want to test custom color logic, but it is only necessary to explicitly test color values if our badge implements custom logic for setting the badge colors.
|
||||
|
||||
## Code coverage
|
||||
|
||||
By checking code coverage, we can make sure we've covered all our bases.
|
||||
@@ -254,7 +233,7 @@ By checking code coverage, we can make sure we've covered all our bases.
|
||||
We can generate a coverage report and open it:
|
||||
|
||||
```
|
||||
npm run coverage:test:services -- -- --only=wercker
|
||||
npm run coverage:test:services -- -- --only=docsrs
|
||||
npm run coverage:report:open
|
||||
```
|
||||
|
||||
|
||||
33
frontend/blog/2024-06-01-simpleicons12.md
Normal file
33
frontend/blog/2024-06-01-simpleicons12.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
slug: simple-icons-12
|
||||
title: Simple Icons 12
|
||||
authors:
|
||||
name: chris48s
|
||||
title: Shields.io Core Team
|
||||
url: https://github.com/chris48s
|
||||
image_url: https://avatars.githubusercontent.com/u/6025893
|
||||
tags: []
|
||||
---
|
||||
|
||||
Logos on Shields.io are provided by SimpleIcons. We've recently upgraded to SimpleIcons 12. This release removes the following 10 icons:
|
||||
|
||||
- FITE
|
||||
- Flattr
|
||||
- Google Bard
|
||||
- Integromat
|
||||
- Niantic
|
||||
- Nintendo Network
|
||||
- Rome
|
||||
- Shotcut
|
||||
- Skynet
|
||||
- Twitter
|
||||
|
||||
And renames the following 3:
|
||||
|
||||
- Airbrake.io to Airbrake
|
||||
- Amazon AWS to Amazon Web Services
|
||||
- RStudio to RStudio IDE
|
||||
|
||||
More details can be found in the [release notes](https://github.com/simple-icons/simple-icons/releases/tag/12.0.0).
|
||||
|
||||
Please remember that we are just consumers of SimpleIcons. Decisions about changes and removals are made by the [SimpleIcons](https://github.com/simple-icons/simple-icons) project.
|
||||
1083
package-lock.json
generated
1083
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -23,7 +23,7 @@
|
||||
"dependencies": {
|
||||
"@renovatebot/pep440": "^3.0.20",
|
||||
"@renovatebot/ruby-semver": "^3.0.23",
|
||||
"@sentry/node": "^8.7.0",
|
||||
"@sentry/node": "^8.11.0",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"@xmldom/xmldom": "0.8.10",
|
||||
"badge-maker": "file:badge-maker",
|
||||
@@ -39,12 +39,12 @@
|
||||
"emojic": "^1.1.17",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"fast-xml-parser": "^4.4.0",
|
||||
"glob": "^10.4.1",
|
||||
"glob": "^10.4.2",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.3.0",
|
||||
"graphql": "16.8.1",
|
||||
"got": "^14.4.1",
|
||||
"graphql": "16.8.2",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"joi": "17.13.1",
|
||||
"joi": "17.13.3",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath": "~1.1.1",
|
||||
@@ -53,18 +53,18 @@
|
||||
"lodash.times": "^4.3.2",
|
||||
"matcher": "^5.0.0",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"node-pg-migrate": "^7.4.0",
|
||||
"node-pg-migrate": "^7.5.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.2.2",
|
||||
"pg": "^8.11.5",
|
||||
"pg": "^8.12.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^15.1.2",
|
||||
"qs": "^6.12.1",
|
||||
"query-string": "^9.0.0",
|
||||
"semver": "~7.6.2",
|
||||
"simple-icons": "12.0.0",
|
||||
"smol-toml": "1.2.0",
|
||||
"simple-icons": "12.3.0",
|
||||
"smol-toml": "1.2.1",
|
||||
"svg-path-commander": "^2.0.9",
|
||||
"webextension-store-meta": "^1.2.3",
|
||||
"xpath": "~0.0.34"
|
||||
@@ -145,22 +145,23 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/core": "^3.3.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.41.0",
|
||||
"@docusaurus/core": "^3.4.0",
|
||||
"@docusaurus/preset-classic": "^3.4.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.44.2",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"c8": "^9.1.0",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"c8": "^10.1.2",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.4.1",
|
||||
"chai-as-promised": "^7.1.2",
|
||||
"chai-as-promised": "^8.0.0",
|
||||
"chai-datetime": "^1.8.0",
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"cypress": "^13.10.0",
|
||||
"cypress": "^13.12.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^12.3.0",
|
||||
"danger": "^12.3.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"docusaurus-preset-openapi": "0.7.5",
|
||||
"eslint": "8.57.0",
|
||||
@@ -168,16 +169,16 @@
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-config-standard-jsx": "11.0.0",
|
||||
"eslint-config-standard-react": "13.0.0",
|
||||
"eslint-plugin-chai-friendly": "^0.8.0",
|
||||
"eslint-plugin-chai-friendly": "^1.0.0",
|
||||
"eslint-plugin-cypress": "^3.3.0",
|
||||
"eslint-plugin-icedfrisby": "^0.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.2.7",
|
||||
"eslint-plugin-jsdoc": "^48.2.12",
|
||||
"eslint-plugin-mocha": "^10.4.3",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "6.2.0",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-sort-class-members": "^1.20.0",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -185,7 +186,7 @@
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^5.0.1",
|
||||
"jsdoc": "^4.0.3",
|
||||
"lint-staged": "^15.2.5",
|
||||
"lint-staged": "^15.2.7",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^10.4.0",
|
||||
@@ -194,11 +195,11 @@
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.5.4",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"nodemon": "^3.1.2",
|
||||
"nodemon": "^3.1.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^8.0.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "3.2.5",
|
||||
"prettier": "3.3.2",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -209,8 +210,8 @@
|
||||
"sinon": "^18.0.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"snap-shot-it": "^7.9.10",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"tsd": "^0.31.0",
|
||||
"start-server-and-test": "2.0.4",
|
||||
"tsd": "^0.31.1",
|
||||
"url": "^0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -35,12 +35,12 @@ t.create('maintainability letter')
|
||||
t.create('issues when outer user repos query returns multiple items')
|
||||
.get('/issues/tensorflow/models.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.codeclimate.com', { allowUnmocked: true })
|
||||
nock('https://api.codeclimate.com')
|
||||
.get('/v1/repos?github_slug=tensorflow%2Fmodels')
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: 'xxxxxxxxxxxx', // Fake repo id, which is expected to be ignored in favour of the one that does contain snapshot data.
|
||||
id: 'xxxxxxxxxxxx', // Expected to be ignored in favour of the one that does contain snapshot data.
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: null,
|
||||
@@ -51,7 +51,7 @@ t.create('issues when outer user repos query returns multiple items')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '57e2efacc718d40058000c9b', // Real repo id for tensorflow/models. The test retrieves live data using the real snapshot id below.
|
||||
id: '57e2efacc718d40058000c9b',
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: {
|
||||
@@ -65,12 +65,31 @@ t.create('issues when outer user repos query returns multiple items')
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/v1/repos/57e2efacc718d40058000c9b/snapshots/65ae115f34117d0001055101',
|
||||
)
|
||||
.reply(200, {
|
||||
data: {
|
||||
attributes: {
|
||||
ratings: [
|
||||
{
|
||||
letter: 'D',
|
||||
measure: {
|
||||
value: 24.73668395092537,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
issues_count: 11538,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.networkOn() // Combined with allowUnmocked: true, this allows the inner snapshots query to go through.
|
||||
.expectBadge({
|
||||
label: 'issues',
|
||||
message: Joi.number().integer().positive(),
|
||||
message: '11538',
|
||||
})
|
||||
|
||||
t.create('maintainability letter for non-existent repo')
|
||||
|
||||
@@ -23,12 +23,12 @@ t.create('test coverage letter')
|
||||
t.create('test coverage when outer user repos query returns multiple items')
|
||||
.get('/coverage/codeclimate/codeclimate.json')
|
||||
.intercept(nock =>
|
||||
nock('https://api.codeclimate.com', { allowUnmocked: true })
|
||||
nock('https://api.codeclimate.com')
|
||||
.get('/v1/repos?github_slug=codeclimate%2Fcodeclimate')
|
||||
.reply(200, {
|
||||
data: [
|
||||
{
|
||||
id: 'xxxxxxxxxxxx', // Fake repo id, which is expected to be ignored in favour of the one that does contain snapshot data.
|
||||
id: 'xxxxxxxxxxxx', // Expected to be ignored in favour of the one that does contain snapshot data.
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: null,
|
||||
@@ -39,7 +39,7 @@ t.create('test coverage when outer user repos query returns multiple items')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '558479d6e30ba034120008a9', // Real repo id for codeclimate/codeclimate. The test retrieves live data using the real test report id below.
|
||||
id: '558479d6e30ba034120008a9',
|
||||
relationships: {
|
||||
latest_default_branch_snapshot: {
|
||||
data: null,
|
||||
@@ -53,12 +53,24 @@ t.create('test coverage when outer user repos query returns multiple items')
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/v1/repos/558479d6e30ba034120008a9/test_reports/65a1662cb0077b00013cb4de',
|
||||
)
|
||||
.reply(200, {
|
||||
data: {
|
||||
attributes: {
|
||||
covered_percent: 24,
|
||||
rating: {
|
||||
letter: 'B',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.networkOn() // Combined with allowUnmocked: true, this allows the inner test reports query to go through.
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: isIntegerPercentage,
|
||||
message: '24%',
|
||||
})
|
||||
|
||||
t.create('test coverage percentage for non-existent repo')
|
||||
|
||||
@@ -17,6 +17,7 @@ const archEnum = [
|
||||
'mips64',
|
||||
'mips64le',
|
||||
'riscv64',
|
||||
'loong64',
|
||||
]
|
||||
|
||||
// Valid architecture values: https://golang.org/doc/install/source#environment (GOARCH)
|
||||
|
||||
@@ -2,10 +2,12 @@ import { isSemver } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('docker version (valid, library)').get('/_/alpine.json').expectBadge({
|
||||
label: 'version',
|
||||
message: isSemver,
|
||||
})
|
||||
t.create('docker version (valid, library)')
|
||||
.get('/_/memcached.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
message: isSemver,
|
||||
})
|
||||
|
||||
t.create('docker version (valid, library with tag)')
|
||||
.get('/_/alpine/latest.json')
|
||||
|
||||
@@ -61,6 +61,7 @@ export default class DocsRs extends BaseJsonService {
|
||||
return await this._requestJson({
|
||||
schema,
|
||||
url: `https://docs.rs/crate/${crate}/${version}/status.json`,
|
||||
httpErrors: version ? { 400: 'malformed version' } : {},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,32 @@ import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Passing docs')
|
||||
.get('/tokio/0.3.0.json')
|
||||
.expectBadge({ label: 'docs@0.3.0', message: 'passing' })
|
||||
t.create('Docs with no version specified')
|
||||
.get('/tokio.json')
|
||||
.expectBadge({
|
||||
label: 'docs',
|
||||
message: Joi.allow('passing', 'failing'),
|
||||
})
|
||||
|
||||
t.create('Failing docs')
|
||||
.get('/tensorflow/0.16.1.json')
|
||||
.expectBadge({ label: 'docs@0.16.1', message: 'failing' })
|
||||
t.create('Passing docs for version').get('/tokio/1.37.0.json').expectBadge({
|
||||
label: 'docs@1.37.0',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('Failing docs for version').get('/tokio/1.32.1.json').expectBadge({
|
||||
label: 'docs@1.32.1',
|
||||
message: 'failing',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('Multiple builds, latest passing')
|
||||
.get('/bevy_tweening/0.3.1.json')
|
||||
.expectBadge({ label: 'docs@0.3.1', message: 'passing' })
|
||||
.expectBadge({
|
||||
label: 'docs@0.3.1',
|
||||
message: 'passing',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('Getting latest version works')
|
||||
.get('/rand/latest.json')
|
||||
@@ -26,5 +41,9 @@ t.create('Crate not found')
|
||||
.expectBadge({ label: 'docs', message: 'not found' })
|
||||
|
||||
t.create('Version not found')
|
||||
.get('/tokio/not-a-version.json')
|
||||
.get('/tokio/0.8.json')
|
||||
.expectBadge({ label: 'docs', message: 'not found' })
|
||||
|
||||
t.create('Malformed version')
|
||||
.get('/tokio/not-a-version.json')
|
||||
.expectBadge({ label: 'docs', message: 'malformed version' })
|
||||
|
||||
@@ -36,9 +36,14 @@ const endpointSchema = Joi.object({
|
||||
logoSvg: Joi.string(),
|
||||
logoColor: optionalStringWhenNamedLogoPresent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
logoPosition: optionalNumberWhenAnyLogoPresent,
|
||||
style: Joi.string(),
|
||||
cacheSeconds: Joi.number().integer().min(0),
|
||||
/*
|
||||
Retained for legacy compatibility
|
||||
Although this does nothing,
|
||||
passing it should not throw an error
|
||||
*/
|
||||
logoPosition: optionalNumberWhenAnyLogoPresent,
|
||||
})
|
||||
// `namedLogo` or `logoSvg`; not both.
|
||||
.oxor('namedLogo', 'logoSvg')
|
||||
|
||||
@@ -102,13 +102,6 @@ The endpoint badge takes a single required query param: <code>url</code>, which
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>logoPosition</code></td>
|
||||
<td>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>style</code></td>
|
||||
<td>
|
||||
@@ -157,7 +150,6 @@ export default class Endpoint extends BaseJsonService {
|
||||
logoSvg,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
style,
|
||||
cacheSeconds,
|
||||
}) {
|
||||
@@ -171,7 +163,6 @@ export default class Endpoint extends BaseJsonService {
|
||||
logoSvg,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
style,
|
||||
// don't allow the user to set cacheSeconds any shorter than this._cacheLength
|
||||
cacheSeconds: Math.max(
|
||||
|
||||
@@ -119,6 +119,24 @@ t.create('logoWidth')
|
||||
logoWidth: 30,
|
||||
})
|
||||
|
||||
// The logoPosition param was removed, but passing it should not
|
||||
// throw a validation error. It should just do nothing.
|
||||
t.create('logoPosition')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/').get('/badge').reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
logoSvg,
|
||||
logoPosition: 30,
|
||||
}),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
})
|
||||
|
||||
t.create('Invalid schema')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
|
||||
@@ -17,7 +17,7 @@ t.create(
|
||||
nock =>
|
||||
nock('https://api.github.com', { allowUnmocked: true })
|
||||
.get('/repos/badges/shields/pulls/1110')
|
||||
.reply(200, JSON.stringify({ head: { sha: 'abc123' } })), // Looks like a real ref, but isn't.
|
||||
.reply(200, JSON.stringify({ head: { sha: 'abcde12356' } })), // Looks like a real ref, but isn't.
|
||||
)
|
||||
.networkOn()
|
||||
.expectBadge({
|
||||
|
||||
@@ -120,7 +120,7 @@ export default class JenkinsCoverage extends JenkinsBase {
|
||||
}),
|
||||
queryParam({
|
||||
name: 'jobUrl',
|
||||
example: 'https://jenkins.sqlalchemy.org/job/alembic_coverage',
|
||||
example: 'https://jenkins.sqlalchemy.org/job/dogpile_coverage',
|
||||
required: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -27,7 +27,7 @@ t.create('cobertura: job not found')
|
||||
|
||||
t.create('cobertura: job found')
|
||||
.get(
|
||||
'/cobertura.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
|
||||
'/cobertura.json?jobUrl=https://jenkins.sqlalchemy.org/job/dogpile_coverage',
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: isIntegerPercentage })
|
||||
|
||||
|
||||
70
services/mbin/mbin.service.js
Normal file
70
services/mbin/mbin.service.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Joi from 'joi'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { BaseJsonService, InvalidParameter, pathParams } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
subscriptionsCount: Joi.number().required(),
|
||||
}).required()
|
||||
|
||||
export default class Mbin extends BaseJsonService {
|
||||
static category = 'social'
|
||||
|
||||
static route = {
|
||||
base: 'mbin',
|
||||
pattern: ':magazine',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/mbin/{magazine}': {
|
||||
get: {
|
||||
summary: 'Mbin',
|
||||
description:
|
||||
'Mbin is a fork of Kbin, a content aggregator for the Fediverse.',
|
||||
parameters: pathParams({
|
||||
name: 'magazine',
|
||||
description: 'The magazine to query. This is CASE SENSITIVE.',
|
||||
example: 'teletext@fedia.io',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'magazine', namedLogo: 'activitypub' }
|
||||
|
||||
static render({ magazine, members }) {
|
||||
return {
|
||||
label: `subscribe to ${magazine}`,
|
||||
message: metric(members),
|
||||
style: 'social',
|
||||
color: 'brightgreen',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ magazine }) {
|
||||
const splitAlias = magazine.split('@')
|
||||
// The magazine will be in the format of 'magazine@server'
|
||||
if (splitAlias.length !== 2) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'invalid magazine',
|
||||
})
|
||||
}
|
||||
|
||||
const mag = splitAlias[0]
|
||||
const host = splitAlias[1]
|
||||
|
||||
const data = await this._requestJson({
|
||||
url: `https://${host}/api/magazine/name/${mag}`,
|
||||
schema,
|
||||
httpErrors: {
|
||||
404: 'magazine not found',
|
||||
},
|
||||
})
|
||||
|
||||
return data.subscriptionsCount
|
||||
}
|
||||
|
||||
async handle({ magazine }) {
|
||||
const members = await this.fetch({ magazine })
|
||||
return this.constructor.render({ magazine, members })
|
||||
}
|
||||
}
|
||||
43
services/mbin/mbin.tester.js
Normal file
43
services/mbin/mbin.tester.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('get magazine subscribers')
|
||||
.get('/magazine@instance.tld.json')
|
||||
.intercept(nock =>
|
||||
nock('https://instance.tld/')
|
||||
.get('/api/magazine/name/magazine')
|
||||
.reply(
|
||||
200,
|
||||
JSON.stringify({
|
||||
subscriptionsCount: 42,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'subscribe to magazine@instance.tld',
|
||||
message: '42',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
|
||||
t.create('unknown community')
|
||||
.get('/01J12N2ETYG3W5B6G8Y11F5EXG@fedia.io.json')
|
||||
.expectBadge({
|
||||
label: 'magazine',
|
||||
message: 'magazine not found',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('invalid magazine').get('/magazine.invalid.json').expectBadge({
|
||||
label: 'magazine',
|
||||
message: 'invalid magazine',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
t.create('test on real mbin magazine for API compliance')
|
||||
.get('/teletext@fedia.io.json')
|
||||
.expectBadge({
|
||||
label: 'subscribe to teletext@fedia.io',
|
||||
message: isMetric,
|
||||
color: 'brightgreen',
|
||||
})
|
||||
@@ -24,6 +24,27 @@ const modulesSchema = Joi.object({
|
||||
),
|
||||
}).required()
|
||||
|
||||
const modulesValidationSchema = Joi.array()
|
||||
.items(
|
||||
Joi.alternatives().try(
|
||||
Joi.object({
|
||||
name: Joi.string().valid('total').required(),
|
||||
score: nonNegativeInteger,
|
||||
}).required(),
|
||||
Joi.object({}).required(),
|
||||
),
|
||||
)
|
||||
.custom((value, helpers) => {
|
||||
// Custom validation to check for exactly one type1 object
|
||||
const totalCount = value.filter(item => item.name === 'total').length
|
||||
if (totalCount !== 1) {
|
||||
return helpers.message(
|
||||
'Array must contain exactly one object of type "total"',
|
||||
)
|
||||
}
|
||||
return value
|
||||
})
|
||||
|
||||
class BasePuppetForgeUsersService extends BaseJsonService {
|
||||
async fetch({ user }) {
|
||||
return this._requestJson({
|
||||
@@ -42,4 +63,17 @@ class BasePuppetForgeModulesService extends BaseJsonService {
|
||||
}
|
||||
}
|
||||
|
||||
export { BasePuppetForgeModulesService, BasePuppetForgeUsersService }
|
||||
class BasePuppetForgeModulesValidationService extends BaseJsonService {
|
||||
async fetch({ user, moduleName }) {
|
||||
return this._requestJson({
|
||||
schema: modulesValidationSchema,
|
||||
url: `https://forgeapi.puppetlabs.com/private/validations/${user}-${moduleName}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
BasePuppetForgeModulesService,
|
||||
BasePuppetForgeUsersService,
|
||||
BasePuppetForgeModulesValidationService,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { coveragePercentage as coveragePercentageColor } from '../color-formatters.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { BasePuppetForgeModulesValidationService } from './puppetforge-base.js'
|
||||
|
||||
export default class PuppetforgeModuleQualityScoreService extends BasePuppetForgeModulesValidationService {
|
||||
static category = 'rating'
|
||||
|
||||
static route = {
|
||||
base: 'puppetforge/qualityscore',
|
||||
pattern: ':user/:moduleName',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/puppetforge/qualityscore/{user}/{moduleName}': {
|
||||
get: {
|
||||
summary: 'Puppet Forge quality score',
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'user',
|
||||
example: 'camptocamp',
|
||||
},
|
||||
{
|
||||
name: 'moduleName',
|
||||
example: 'openssl',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'quality score' }
|
||||
|
||||
static render({ score }) {
|
||||
return {
|
||||
message: `${score}%`,
|
||||
color: coveragePercentageColor(score),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ user, moduleName }) {
|
||||
const data = await this.fetch({ user, moduleName })
|
||||
const qualityScore = data.find(el => el.name === 'total').score
|
||||
return this.constructor.render({ score: qualityScore })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { isPercentage } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('module quality-score').get('/camptocamp/openssl.json').expectBadge({
|
||||
label: 'quality score',
|
||||
message: isPercentage,
|
||||
})
|
||||
|
||||
t.create('module quality score (no ratings)')
|
||||
.get('/camptocamp/openssl.json')
|
||||
.intercept(nock =>
|
||||
nock('https://forgeapi.puppetlabs.com/private/validations')
|
||||
.get('/camptocamp-openssl')
|
||||
.reply(200, []),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'quality score',
|
||||
message: 'invalid response data',
|
||||
})
|
||||
|
||||
t.create('module quality score (not found)')
|
||||
.get('/notarealuser/notarealpackage.json')
|
||||
.expectBadge({
|
||||
label: 'quality score',
|
||||
message: 'not found',
|
||||
})
|
||||
Reference in New Issue
Block a user