diff --git a/core/base-service/validate.js b/core/base-service/validate.js index 38adcebfdd9e4b8ad5581cd0f18f4bcccb00ac77..97f7c7a03e75dd7d0ef74a5f18dd0965fcbfb98b 100644 --- a/core/base-service/validate.js +++ b/core/base-service/validate.js @@ -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', diff --git a/core/base-service/validate.spec.js b/core/base-service/validate.spec.js index 3a9afd8162c1da87adde6eb4588e9359300666fc..152a6423dff0f1ff6c5840065949f64eec03e7fe 100644 --- a/core/base-service/validate.spec.js +++ b/core/base-service/validate.spec.js @@ -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') + }) }) diff --git a/frontend/components/endpoint-page.js b/frontend/components/endpoint-page.js new file mode 100644 index 0000000000000000000000000000000000000000..c423676f51aa44d28e7cedc16caaf7ffaeb1e583 --- /dev/null +++ b/frontend/components/endpoint-page.js @@ -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 diff --git a/frontend/components/usage.js b/frontend/components/usage.js index 1485ff74fe89916e3db0478f1bc3f582e45d14db..55cf369285f50a8a2193ab2575e37530e8009611 100644 --- a/frontend/components/usage.js +++ b/frontend/components/usage.js @@ -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} /> diff --git a/gh-badges/templates/_shields_test-template.json b/gh-badges/templates/_shields_test-template.json index e39706a9568a586dbaac7894781c387dcad68e3f..af44570bba65ede7be4b8f8b6b2e94ee591d034a 100644 --- a/gh-badges/templates/_shields_test-template.json +++ b/gh-badges/templates/_shields_test-template.json @@ -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])}} diff --git a/pages/index.js b/pages/index.js index 90cd8635e6771425025d316cd6c425b8b417448a..5ba840123baf3386103af37083e84e0adf64d6bc 100644 --- a/pages/index.js +++ b/pages/index.js @@ -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> ) diff --git a/services/base.js b/services/base.js index c2067ec3d39ba42f0bfd772959649347ee841f1a..d8765290032387ce4a2e14204f0ea7a21455dd7b 100644 --- a/services/base.js +++ b/services/base.js @@ -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 diff --git a/services/base.spec.js b/services/base.spec.js index 506d51442d9e18ece7af07614d0817e36b3ee369..f0b9d2cacd9248ef34c5dc65b0b43283e2b40353 100644 --- a/services/base.spec.js +++ b/services/base.spec.js @@ -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) }) diff --git a/services/cache-headers.js b/services/cache-headers.js index a9370a290fe382ca95f6207ee0db8d0ad2f38fe7..58ecc7e19285f67a196beb8d98b24c334b301b63 100644 --- a/services/cache-headers.js +++ b/services/cache-headers.js @@ -12,7 +12,9 @@ const queryParamSchema = Joi.object({ maxAge: Joi.number() .integer() .min(0), -}).required() +}) + .unknown(true) + .required() function overrideCacheLengthFromQueryParams(queryParams) { try { diff --git a/services/cache-headers.spec.js b/services/cache-headers.spec.js index 71681b76b8b9e87b0a6aae1c8ce06d4bc4d8caef..a2806065a21a73759207e10a20bd29c7875720b4 100644 --- a/services/cache-headers.spec.js +++ b/services/cache-headers.spec.js @@ -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, diff --git a/services/endpoint/endpoint.service.js b/services/endpoint/endpoint.service.js new file mode 100644 index 0000000000000000000000000000000000000000..ed9fa31ce8e77481827da0fbd15218e4e8776888 --- /dev/null +++ b/services/endpoint/endpoint.service.js @@ -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) + } +} diff --git a/services/endpoint/endpoint.tester.js b/services/endpoint/endpoint.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..92a05e0e77d4e2f41cf080305b28b9733c91dcc0 --- /dev/null +++ b/services/endpoint/endpoint.tester.js @@ -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' })