[Endpoint] badge (#2473)
This reimplements the idea @bkdotcom came up with in #1519, and took a stab at in #1525. It’s a really powerful way to add all sorts of custom badges, particularly considering [tools like RunKit endpoints and Jupyter Kernel Gateway](https://github.com/badges/shields/issues/2259#issuecomment-444186589), not to mention all the other ways cloud functions can be deployed these days.
This commit is contained in:
@@ -11,6 +11,7 @@ function validate(
|
||||
includeKeys = false,
|
||||
traceErrorMessage = 'Data did not match schema',
|
||||
traceSuccessMessage = 'Data after validation',
|
||||
allowAndStripUnknownKeys = true,
|
||||
},
|
||||
data,
|
||||
schema
|
||||
@@ -18,10 +19,13 @@ function validate(
|
||||
if (!schema || !schema.isJoi) {
|
||||
throw Error('A Joi schema is required')
|
||||
}
|
||||
const { error, value } = Joi.validate(data, schema, {
|
||||
allowUnknown: true,
|
||||
stripUnknown: true,
|
||||
})
|
||||
const options = allowAndStripUnknownKeys
|
||||
? {
|
||||
allowUnknown: true,
|
||||
stripUnknown: true,
|
||||
}
|
||||
: undefined
|
||||
const { error, value } = Joi.validate(data, schema, options)
|
||||
if (error) {
|
||||
trace.logTrace(
|
||||
'validate',
|
||||
|
||||
@@ -105,4 +105,14 @@ describe('validate', function() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('allowAndStripUnknownKeys', function() {
|
||||
expect(() =>
|
||||
validate(
|
||||
{ ...options, allowAndStripUnknownKeys: false },
|
||||
{ requiredString: 'bar', extra: 'nonsense' },
|
||||
schema
|
||||
)
|
||||
).to.throw(InvalidParameter, '"extra" is not allowed')
|
||||
})
|
||||
})
|
||||
|
||||
217
frontend/components/endpoint-page.js
Normal file
217
frontend/components/endpoint-page.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import { staticBadgeUrl } from '../lib/badge-url'
|
||||
import { baseUrl } from '../constants'
|
||||
import Meta from './meta'
|
||||
import Header from './header'
|
||||
import Footer from './footer'
|
||||
import { H3, Badge } from './common'
|
||||
import { Snippet } from './snippet'
|
||||
|
||||
const Explanation = styled.div`
|
||||
max-width: 800px;
|
||||
display: block;
|
||||
`
|
||||
|
||||
const JsonExampleBlock = styled.code`
|
||||
display: inline-block;
|
||||
|
||||
text-align: left;
|
||||
line-height: 1.2em;
|
||||
padding: 16px 18px;
|
||||
|
||||
border-radius: 4px;
|
||||
background: #eef;
|
||||
|
||||
font-family: Lekton;
|
||||
font-size: ${({ fontSize }) => fontSize};
|
||||
|
||||
white-space: pre;
|
||||
`
|
||||
|
||||
const JsonExample = ({ data }) => (
|
||||
<JsonExampleBlock>{JSON.stringify(data, undefined, 2)}</JsonExampleBlock>
|
||||
)
|
||||
JsonExample.propTypes = {
|
||||
data: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const Schema = styled.dl`
|
||||
display: inline-block;
|
||||
max-width: 800px;
|
||||
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
|
||||
background: #efefef;
|
||||
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
|
||||
dt,
|
||||
dd {
|
||||
padding: 0 1%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
dt {
|
||||
width: 100px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 20px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.data_table {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const EndpointPage = () => (
|
||||
<div>
|
||||
<Meta />
|
||||
<Header />
|
||||
<H3 id="static-badge">Endpoint (Beta)</H3>
|
||||
<Snippet snippet={`${baseUrl}/badge/endpoint.svg?url=...&style=...`} />
|
||||
<p>Endpoint response:</p>
|
||||
<JsonExample
|
||||
data={{
|
||||
schemaVersion: 1,
|
||||
label: 'hello',
|
||||
message: 'sweet world',
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<p>Shields response:</p>
|
||||
<Badge
|
||||
src={staticBadgeUrl(baseUrl, 'hello', 'sweet world', 'orange')}
|
||||
alt="hello | sweet world"
|
||||
/>
|
||||
<Explanation>
|
||||
<p>
|
||||
Developers rely on Shields for visual consistency and powerful
|
||||
customization options. As a service provider or data provider, you can
|
||||
use the endpoint badge to provide content while giving users the full
|
||||
power of Shields' badge customization.
|
||||
</p>
|
||||
<p>
|
||||
Using the endpoint badge, you can provide content for a badge through a
|
||||
JSON endpoint. The content can be prerendered, or generated on the fly.
|
||||
To strike a balance between responsiveness and bandwith utilization on
|
||||
one hand, and freshness on the other, cache behavior is configurable,
|
||||
subject to the Shields minimum. The endpoint URL is provided to Shields
|
||||
through the query string. Shields fetches it and formats the badge.
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge is a better alternative than redirecting to the
|
||||
static badge enpoint or generating SVG on your server:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="https://en.wikipedia.org/wiki/Separation_of_content_and_presentation">
|
||||
Content and presentation are separate.
|
||||
</a>{' '}
|
||||
The service provider authors the badge, and Shields takes input from
|
||||
the user to format it. As a service provider you author the badge but
|
||||
don't have to concern yourself with styling. You don't even have to
|
||||
pass the formatting options through to Shields.
|
||||
</li>
|
||||
<li>
|
||||
Badge formatting is always 100% up to date. There's no need to track
|
||||
updates to the npm package, badge templates, or options.
|
||||
</li>
|
||||
<li>
|
||||
A JSON response is easy to implement; easier than an HTTP redirect. It
|
||||
is trivial in almost any framework, and is more compatible with
|
||||
hosting environments such as{' '}
|
||||
<a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
|
||||
</li>
|
||||
<li>
|
||||
As a service provider you can rely on the Shields CDN. There's no need
|
||||
to study the HTTP headers. Adjusting cache behavior is as simple as
|
||||
setting a property in the JSON response.
|
||||
</li>
|
||||
</ol>
|
||||
</Explanation>
|
||||
<h4>Schema</h4>
|
||||
<p>
|
||||
The schema may change during the beta period. Any changes will be posted
|
||||
here. After launch, breaking changes will trigger an increment to the
|
||||
`schemaVersion`.
|
||||
</p>
|
||||
<Schema>
|
||||
<dt>schemaVersion</dt>
|
||||
<dd>
|
||||
Required. Always the number <code>1</code>.
|
||||
</dd>
|
||||
<dt>label</dt>
|
||||
<dd>
|
||||
Required. The left text, or the empty string to omit the left side of
|
||||
the badge. This can be overridden by the query string.
|
||||
</dd>
|
||||
<dt>message</dt>
|
||||
<dd>Required. Can't be empty. The right text.</dd>
|
||||
<dt>color</dt>
|
||||
<dd>
|
||||
Default: <code>lightgrey</code>. The right color. Supports the eight
|
||||
named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
|
||||
colors.
|
||||
</dd>
|
||||
<dt>labelColor</dt>
|
||||
<dd>
|
||||
Default: <code>grey</code>. The left color.
|
||||
</dd>
|
||||
<dt>isError</dt>
|
||||
<dd>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an error
|
||||
badge. This prevents the user from overriding the color. In the future
|
||||
it may affect cache behavior.
|
||||
</dd>
|
||||
<dt>namedLogo</dt>
|
||||
<dd>
|
||||
Default: none. One of the named logos supported by Shields or {}
|
||||
<a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
|
||||
by the query string.
|
||||
</dd>
|
||||
<dt>logoSvg</dt>
|
||||
<dd>Default: none. An SVG string containing a custom logo.</dd>
|
||||
<dt>logoColor</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</dd>
|
||||
<dt>logoWidth</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</dd>
|
||||
<dt>logoPosition</dt>
|
||||
<dd>
|
||||
Default: none. Same meaning as the query string. Can be overridden by
|
||||
the query string.
|
||||
</dd>
|
||||
<dt>style</dt>
|
||||
<dd>
|
||||
Default: <code>flat</code>. The default template to use. Can be
|
||||
overridden by the query string.
|
||||
</dd>
|
||||
<dt>cacheSeconds</dt>
|
||||
<dd>
|
||||
Default: <code>300</code>. Set the HTTP cache lifetime in seconds, which
|
||||
should respected by the Shields' CDN and downstream users. This lets you
|
||||
tune performance and traffic vs. responsiveness. Can be overridden by
|
||||
the user via the query string, but only to a longer value.
|
||||
</dd>
|
||||
</Schema>
|
||||
<Footer baseUrl={baseUrl} />
|
||||
</div>
|
||||
)
|
||||
export default EndpointPage
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
import { staticBadgeUrl } from '../lib/badge-url'
|
||||
@@ -193,6 +194,19 @@ export default class Usage extends React.PureComponent {
|
||||
{this.constructor.renderStaticBadgeEscapingRules()}
|
||||
{this.renderColorExamples()}
|
||||
|
||||
<H3 id="endpoint">Endpoint (Beta)</H3>
|
||||
|
||||
<p>
|
||||
<Snippet
|
||||
snippet={`${baseUrl}/badge/endpoint.svg?url=<URL>&style<STYLE>`}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Create badges from{' '}
|
||||
<Link to={'/endpoint'}>your own JSON endpoint</Link>.
|
||||
</p>
|
||||
|
||||
<H3 id="dynamic-badge">Dynamic</H3>
|
||||
|
||||
<DynamicBadgeMaker baseUrl={baseUrl} />
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"color": {{=JSON.stringify(it.color || null)}},
|
||||
{{?it.labelColor}}
|
||||
"labelColor": {{=JSON.stringify(it.labelColor)}},
|
||||
{{?}}
|
||||
{{?it.logoWidth}}
|
||||
"logoWidth": {{=JSON.stringify(it.logoWidth)}},
|
||||
{{?}}
|
||||
"name": {{=JSON.stringify(it.text[0])}},
|
||||
"value": {{=JSON.stringify(it.text[1])}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import { HashRouter, StaticRouter, Route } from 'react-router-dom'
|
||||
import Main from '../frontend/components/main'
|
||||
import EndpointPage from '../frontend/components/endpoint-page'
|
||||
|
||||
export default class Router extends React.Component {
|
||||
render() {
|
||||
@@ -8,6 +9,7 @@ export default class Router extends React.Component {
|
||||
<div>
|
||||
<Route path="/" exact component={Main} />
|
||||
<Route path="/examples/:category" component={Main} />
|
||||
<Route path="/endpoint" component={EndpointPage} />
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,15 @@ const defaultBadgeDataSchema = Joi.object({
|
||||
namedLogo: Joi.string(),
|
||||
}).required()
|
||||
|
||||
const optionalStringWhenNamedLogoPrsent = Joi.alternatives().when('namedLogo', {
|
||||
is: Joi.string().required(),
|
||||
then: Joi.string(),
|
||||
})
|
||||
|
||||
const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
|
||||
.when('namedLogo', { is: Joi.string().required(), then: Joi.number() })
|
||||
.when('logoSvg', { is: Joi.string().required(), then: Joi.number() })
|
||||
|
||||
const serviceDataSchema = Joi.object({
|
||||
isError: Joi.boolean(),
|
||||
label: Joi.string().allow(''),
|
||||
@@ -45,27 +54,15 @@ const serviceDataSchema = Joi.object({
|
||||
labelColor: Joi.string(),
|
||||
namedLogo: Joi.string(),
|
||||
logoSvg: Joi.string(),
|
||||
logoColor: Joi.forbidden(),
|
||||
logoWidth: Joi.forbidden(),
|
||||
logoPosition: Joi.forbidden(),
|
||||
cacheLengthSeconds: Joi.number()
|
||||
logoColor: optionalStringWhenNamedLogoPrsent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
logoPosition: optionalNumberWhenAnyLogoPresent,
|
||||
cacheSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(0),
|
||||
style: Joi.string(),
|
||||
})
|
||||
.oxor('namedLogo', 'logoSvg')
|
||||
.when(
|
||||
Joi.alternatives().try(
|
||||
Joi.object({ namedLogo: Joi.string().required() }).unknown(),
|
||||
Joi.object({ logoSvg: Joi.string().required() }).unknown()
|
||||
),
|
||||
{
|
||||
then: Joi.object({
|
||||
logoColor: Joi.string(),
|
||||
logoWidth: Joi.number(),
|
||||
logoPosition: Joi.number(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
.required()
|
||||
|
||||
class BaseService {
|
||||
@@ -379,7 +376,7 @@ class BaseService {
|
||||
// string.
|
||||
static _makeBadgeData(overrides, serviceData) {
|
||||
const {
|
||||
style,
|
||||
style: overrideStyle,
|
||||
label: overrideLabel,
|
||||
logoColor: overrideLogoColor,
|
||||
link: overrideLink,
|
||||
@@ -415,7 +412,8 @@ class BaseService {
|
||||
logoWidth: serviceLogoWidth,
|
||||
logoPosition: serviceLogoPosition,
|
||||
link: serviceLink,
|
||||
cacheLengthSeconds: serviceCacheLengthSeconds,
|
||||
cacheSeconds: serviceCacheSeconds,
|
||||
style: serviceStyle,
|
||||
} = serviceData
|
||||
const serviceLogoSvgBase64 = serviceLogoSvg
|
||||
? svg2base64(serviceLogoSvg)
|
||||
@@ -427,7 +425,9 @@ class BaseService {
|
||||
label: defaultLabel,
|
||||
labelColor: defaultLabelColor,
|
||||
} = this.defaultBadgeData
|
||||
const defaultCacheLengthSeconds = this._cacheLength
|
||||
const defaultCacheSeconds = this._cacheLength
|
||||
|
||||
const style = coalesce(overrideStyle, serviceStyle)
|
||||
|
||||
const namedLogoSvgBase64 = prepareNamedLogo({
|
||||
name: coalesce(
|
||||
@@ -480,10 +480,7 @@ class BaseService {
|
||||
overrideNamedLogo ? undefined : serviceLogoPosition
|
||||
),
|
||||
links: toArray(overrideLink || serviceLink),
|
||||
cacheLengthSeconds: coalesce(
|
||||
serviceCacheLengthSeconds,
|
||||
defaultCacheLengthSeconds
|
||||
),
|
||||
cacheLengthSeconds: coalesce(serviceCacheSeconds, defaultCacheSeconds),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,13 +514,14 @@ class BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
static _validate(data, schema) {
|
||||
static _validate(data, schema, { allowAndStripUnknownKeys = true } = {}) {
|
||||
return validate(
|
||||
{
|
||||
ErrorClass: InvalidResponse,
|
||||
prettyErrorMessage: 'invalid response data',
|
||||
traceErrorMessage: 'Response did not match schema',
|
||||
traceSuccessMessage: 'Response after validation',
|
||||
allowAndStripUnknownKeys,
|
||||
},
|
||||
data,
|
||||
schema
|
||||
|
||||
@@ -507,7 +507,7 @@ describe('BaseService', function() {
|
||||
it('overrides the cache length', function() {
|
||||
const badgeData = DummyService._makeBadgeData(
|
||||
{ style: 'pill' },
|
||||
{ cacheLengthSeconds: 123 }
|
||||
{ cacheSeconds: 123 }
|
||||
)
|
||||
expect(badgeData.cacheLengthSeconds).to.equal(123)
|
||||
})
|
||||
|
||||
@@ -12,7 +12,9 @@ const queryParamSchema = Joi.object({
|
||||
maxAge: Joi.number()
|
||||
.integer()
|
||||
.min(0),
|
||||
}).required()
|
||||
})
|
||||
.unknown(true)
|
||||
.required()
|
||||
|
||||
function overrideCacheLengthFromQueryParams(queryParams) {
|
||||
try {
|
||||
|
||||
@@ -35,6 +35,11 @@ describe('Cache header functions', function() {
|
||||
serviceDefaultCacheLengthSeconds: 900,
|
||||
queryParams: { maxAge: 1000 },
|
||||
}).expect(1000)
|
||||
given({
|
||||
cacheHeaderConfig,
|
||||
serviceDefaultCacheLengthSeconds: 900,
|
||||
queryParams: { maxAge: 1000, other: 'here', maybe: 'bogus' },
|
||||
}).expect(1000)
|
||||
given({
|
||||
cacheHeaderConfig,
|
||||
serviceDefaultCacheLengthSeconds: 900,
|
||||
|
||||
132
services/endpoint/endpoint.service.js
Normal file
132
services/endpoint/endpoint.service.js
Normal file
@@ -0,0 +1,132 @@
|
||||
'use strict'
|
||||
|
||||
const { URL } = require('url')
|
||||
const Joi = require('joi')
|
||||
const { errorMessages } = require('../dynamic/dynamic-helpers')
|
||||
const BaseJsonService = require('../base-json')
|
||||
const { InvalidParameter } = require('../errors')
|
||||
const { optionalUrl } = require('../validators')
|
||||
|
||||
const blockedDomains = ['github.com', 'shields.io']
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
url: optionalUrl.required(),
|
||||
}).required()
|
||||
|
||||
const anySchema = Joi.any()
|
||||
|
||||
const optionalStringWhenNamedLogoPresent = Joi.alternatives().when(
|
||||
'namedLogo',
|
||||
{
|
||||
is: Joi.string().required(),
|
||||
then: Joi.string(),
|
||||
}
|
||||
)
|
||||
|
||||
const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
|
||||
.when('namedLogo', { is: Joi.string().required(), then: Joi.number() })
|
||||
.when('logoSvg', { is: Joi.string().required(), then: Joi.number() })
|
||||
|
||||
const endpointSchema = Joi.object({
|
||||
schemaVersion: 1,
|
||||
label: Joi.string()
|
||||
.allow('')
|
||||
.required(),
|
||||
message: Joi.string().required(),
|
||||
color: Joi.string(),
|
||||
labelColor: Joi.string(),
|
||||
isError: Joi.boolean().default(false),
|
||||
namedLogo: Joi.string(),
|
||||
logoSvg: Joi.string(),
|
||||
logoColor: optionalStringWhenNamedLogoPresent,
|
||||
logoWidth: optionalNumberWhenAnyLogoPresent,
|
||||
logoPosition: optionalNumberWhenAnyLogoPresent,
|
||||
style: Joi.string(),
|
||||
cacheSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(0),
|
||||
})
|
||||
// `namedLogo` or `logoSvg`; not both.
|
||||
.oxor('namedLogo', 'logoSvg')
|
||||
.required()
|
||||
|
||||
module.exports = class Endpoint extends BaseJsonService {
|
||||
static get category() {
|
||||
return 'dynamic'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'badge/endpoint',
|
||||
pattern: '',
|
||||
queryParams: ['url'],
|
||||
}
|
||||
}
|
||||
|
||||
static get _cacheLength() {
|
||||
return 300
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return {
|
||||
label: 'custom badge',
|
||||
}
|
||||
}
|
||||
|
||||
static render({
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
namedLogo,
|
||||
logoSvg,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
style,
|
||||
isError,
|
||||
cacheSeconds,
|
||||
}) {
|
||||
return {
|
||||
isError,
|
||||
label,
|
||||
message,
|
||||
color,
|
||||
labelColor,
|
||||
namedLogo,
|
||||
logoSvg,
|
||||
logoColor,
|
||||
logoWidth,
|
||||
logoPosition,
|
||||
style,
|
||||
cacheSeconds,
|
||||
}
|
||||
}
|
||||
|
||||
async handle(namedParams, queryParams) {
|
||||
const { url } = this.constructor._validateQueryParams(
|
||||
queryParams,
|
||||
queryParamSchema
|
||||
)
|
||||
|
||||
const { protocol, hostname } = new URL(url)
|
||||
if (protocol !== 'https:') {
|
||||
throw new InvalidParameter({ prettyMessage: 'please use https' })
|
||||
}
|
||||
if (blockedDomains.some(domain => hostname.endsWith(domain))) {
|
||||
throw new InvalidParameter({ prettyMessage: 'domain is blocked' })
|
||||
}
|
||||
|
||||
const json = await this._requestJson({
|
||||
schema: anySchema,
|
||||
url,
|
||||
errorMessages,
|
||||
})
|
||||
// Override the validation options because we want to reject unknown keys.
|
||||
const validated = this.constructor._validate(json, endpointSchema, {
|
||||
allowAndStripUnknownKeys: false,
|
||||
})
|
||||
|
||||
return this.constructor.render(validated)
|
||||
}
|
||||
}
|
||||
257
services/endpoint/endpoint.tester.js
Normal file
257
services/endpoint/endpoint.tester.js
Normal file
@@ -0,0 +1,257 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { getShieldsIcon } = require('../../lib/logos')
|
||||
|
||||
const t = (module.exports = require('../create-service-tester')())
|
||||
|
||||
t.create('Valid schema (mocked)')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: '',
|
||||
message: 'yo',
|
||||
})
|
||||
)
|
||||
.expectJSON({ name: '', value: 'yo' })
|
||||
|
||||
t.create('color and labelColor')
|
||||
.get('.json?url=https://example.com/badge&style=_shields_test')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
color: '#f0dcc3',
|
||||
labelColor: '#e6e6fa',
|
||||
})
|
||||
)
|
||||
.expectJSON({
|
||||
name: 'hey',
|
||||
value: 'yo',
|
||||
color: '#f0dcc3',
|
||||
labelColor: '#e6e6fa',
|
||||
})
|
||||
|
||||
t.create('style')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
color: '#99c',
|
||||
style: '_shields_test',
|
||||
})
|
||||
)
|
||||
.expectJSON({
|
||||
name: 'hey',
|
||||
value: 'yo',
|
||||
// `color` is only in _shields_test which is being specified by the
|
||||
// service, not the request. If the color key is here we know this has
|
||||
// worked.
|
||||
color: '#99c',
|
||||
})
|
||||
|
||||
t.create('named logo')
|
||||
.get('.svg?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
namedLogo: 'github',
|
||||
})
|
||||
)
|
||||
.after((err, res, body) => {
|
||||
expect(err).not.to.be.ok
|
||||
expect(body).to.include(getShieldsIcon({ name: 'github' }))
|
||||
})
|
||||
|
||||
t.create('named logo with color')
|
||||
.get('.svg?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
namedLogo: 'github',
|
||||
logoColor: 'blue',
|
||||
})
|
||||
)
|
||||
.after((err, res, body) => {
|
||||
expect(err).not.to.be.ok
|
||||
expect(body).to.include(getShieldsIcon({ name: 'github', color: 'blue' }))
|
||||
})
|
||||
|
||||
const logoSvg = Buffer.from(
|
||||
getShieldsIcon({ name: 'github' }).replace('data:image/svg+xml;base64,', ''),
|
||||
'base64'
|
||||
).toString('ascii')
|
||||
|
||||
t.create('custom svg logo')
|
||||
.get('.svg?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
logoSvg,
|
||||
})
|
||||
)
|
||||
.after((err, res, body) => {
|
||||
expect(err).not.to.be.ok
|
||||
expect(body).to.include(getShieldsIcon({ name: 'github' }))
|
||||
})
|
||||
|
||||
t.create('logoWidth')
|
||||
.get('.json?url=https://example.com/badge&style=_shields_test')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
logoSvg,
|
||||
logoWidth: 30,
|
||||
})
|
||||
)
|
||||
.expectJSON({
|
||||
name: 'hey',
|
||||
value: 'yo',
|
||||
color: 'lightgrey',
|
||||
logoWidth: 30,
|
||||
})
|
||||
|
||||
t.create('Invalid schema (mocked)')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: -1,
|
||||
})
|
||||
)
|
||||
.expectJSON({ name: 'custom badge', value: 'invalid response data' })
|
||||
|
||||
t.create('Invalid schema (mocked)')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: 'hey',
|
||||
message: 'yo',
|
||||
extra: 'keys',
|
||||
bogus: true,
|
||||
})
|
||||
)
|
||||
.expectJSON({ name: 'custom badge', value: 'invalid response data' })
|
||||
|
||||
t.create('User color overrides success color')
|
||||
.get('.json?url=https://example.com/badge&colorB=101010&style=_shields_test')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: '',
|
||||
message: 'yo',
|
||||
color: 'blue',
|
||||
})
|
||||
)
|
||||
.expectJSON({ name: '', value: 'yo', color: '#101010' })
|
||||
|
||||
t.create('User color does not override error color')
|
||||
.get('.json?url=https://example.com/badge&colorB=101010&style=_shields_test')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
isError: true,
|
||||
label: 'something is',
|
||||
message: 'not right',
|
||||
color: 'red',
|
||||
})
|
||||
)
|
||||
.expectJSON({ name: 'something is', value: 'not right', color: 'red' })
|
||||
|
||||
t.create('cacheSeconds')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: '',
|
||||
message: 'yo',
|
||||
cacheSeconds: 500,
|
||||
})
|
||||
)
|
||||
.expectHeader('cache-control', 'max-age=500')
|
||||
|
||||
t.create('user can override service cacheSeconds')
|
||||
.get('.json?url=https://example.com/badge&maxAge=1000')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: '',
|
||||
message: 'yo',
|
||||
cacheSeconds: 500,
|
||||
})
|
||||
)
|
||||
.expectHeader('cache-control', 'max-age=1000')
|
||||
|
||||
t.create('user does not override longer service cacheSeconds')
|
||||
.get('.json?url=https://example.com/badge&maxAge=450')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: '',
|
||||
message: 'yo',
|
||||
cacheSeconds: 500,
|
||||
})
|
||||
)
|
||||
.expectHeader('cache-control', 'max-age=500')
|
||||
|
||||
t.create('cacheSeconds does not override longer Shields default')
|
||||
.get('.json?url=https://example.com/badge')
|
||||
.intercept(nock =>
|
||||
nock('https://example.com/')
|
||||
.get('/badge')
|
||||
.reply(200, {
|
||||
schemaVersion: 1,
|
||||
label: '',
|
||||
message: 'yo',
|
||||
cacheSeconds: 10,
|
||||
})
|
||||
)
|
||||
.expectHeader('cache-control', 'max-age=300')
|
||||
|
||||
t.create('Bad scheme')
|
||||
.get('.json?url=http://example.com/badge')
|
||||
.expectJSON({ name: 'custom badge', value: 'please use https' })
|
||||
|
||||
t.create('Blocked domain')
|
||||
.get('.json?url=https://img.shields.io/badge/foo-bar-blue.json')
|
||||
.expectJSON({ name: 'custom badge', value: 'domain is blocked' })
|
||||
Reference in New Issue
Block a user