* delete loads of really important stuff that we definitely need
* v basic MVP smoosh docusaurus PoC into repo
* TODO
* delete more really important stuff
* TODO
* tidyup: use run-s
* don't redirect images used in frontend to raster proxy
* fix routing
* preserve the /endpoint link
* delete the blog (for now)
I would quite like to re-add this at some point
but its not really the top priority thing right now
* content edits
* appease the lint gods
* update danger rules
* remove placeholder
* cypress tests
* dockerhub --> ghcr
* Revert "dockerhub --> ghcr"
This reverts commit ef74cbb26b.
* downgrade lockfile format
* implement defs/BASE_URL
* fix e2e build
* actually fix cypress tests
* always run cypress tests on build
* this never worked
* add command for docusaurus:clear
* delete more code we don't need any more
* update ESLint/prettier config
* delete unsused exports
* documentation updates
* delete a fairly large chunk of our dependency tree
* allow base_url as build arg to Dockerfile
* fixup dockerfile
* work out base url at runtime if not set
doing this at image build time is not the right approach
* remove gatsby monorepo from closebot
* rename HomepageFeatures to homepage-features
523 lines
17 KiB
JavaScript
523 lines
17 KiB
JavaScript
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
import { expect } from 'chai'
|
|
import isSvg from 'is-svg'
|
|
import config from 'config'
|
|
import nock from 'nock'
|
|
import sinon from 'sinon'
|
|
import got from '../got-test-client.js'
|
|
import Server from './server.js'
|
|
import { createTestServer } from './in-process-server-test-helpers.js'
|
|
|
|
describe('The server', function () {
|
|
describe('running', function () {
|
|
let server, baseUrl
|
|
before('Start the server', async function () {
|
|
// Fixes https://github.com/badges/shields/issues/2611
|
|
this.timeout(10000)
|
|
server = await createTestServer({
|
|
public: {
|
|
documentRoot: path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'test-public'
|
|
),
|
|
},
|
|
})
|
|
baseUrl = server.baseUrl
|
|
await server.start()
|
|
})
|
|
after('Shut down the server', async function () {
|
|
if (server) {
|
|
await server.stop()
|
|
}
|
|
server = undefined
|
|
})
|
|
|
|
it('should allow strings for port', async function () {
|
|
// fixes #4391 - This allows the app to be run using iisnode, which uses a named pipe for the port.
|
|
const pipeServer = await createTestServer({
|
|
public: {
|
|
bind: {
|
|
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
|
|
},
|
|
},
|
|
})
|
|
expect(pipeServer).to.not.be.undefined
|
|
})
|
|
|
|
it('should produce colorscheme badges', async function () {
|
|
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.svg`)
|
|
expect(statusCode).to.equal(200)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('fruit')
|
|
.and.to.include('apple')
|
|
})
|
|
|
|
it('should serve front-end with default maxAge', async function () {
|
|
const { headers } = await got(`${baseUrl}/`)
|
|
expect(headers['cache-control']).to.equal('max-age=300, s-maxage=300')
|
|
})
|
|
|
|
it('should serve badges with custom maxAge', async function () {
|
|
const { headers } = await got(`${baseUrl}badge/foo-bar-blue`)
|
|
expect(headers['cache-control']).to.equal('max-age=86400, s-maxage=86400')
|
|
})
|
|
|
|
it('should return cors header for the request', async function () {
|
|
const { statusCode, headers } = await got(
|
|
`${baseUrl}badge/foo-bar-blue.svg`
|
|
)
|
|
expect(statusCode).to.equal(200)
|
|
expect(headers['access-control-allow-origin']).to.equal('*')
|
|
})
|
|
|
|
it('should redirect colorscheme PNG badges as configured', async function () {
|
|
const { statusCode, headers } = await got(
|
|
`${baseUrl}:fruit-apple-green.png`,
|
|
{
|
|
followRedirect: false,
|
|
}
|
|
)
|
|
expect(statusCode).to.equal(301)
|
|
expect(headers.location).to.equal(
|
|
'http://raster.example.test/:fruit-apple-green.png'
|
|
)
|
|
})
|
|
|
|
it('should redirect modern PNG badges as configured', async function () {
|
|
const { statusCode, headers } = await got(
|
|
`${baseUrl}badge/foo-bar-blue.png`,
|
|
{
|
|
followRedirect: false,
|
|
}
|
|
)
|
|
expect(statusCode).to.equal(301)
|
|
expect(headers.location).to.equal(
|
|
'http://raster.example.test/badge/foo-bar-blue.png'
|
|
)
|
|
})
|
|
|
|
it('should not redirect for PNG requests in /img', async function () {
|
|
const { statusCode } = await got(`${baseUrl}img/frontend-image.png`)
|
|
expect(statusCode).to.equal(200)
|
|
})
|
|
|
|
it('should produce SVG badges with expected headers', async function () {
|
|
const { statusCode, headers } = await got(
|
|
`${baseUrl}:fruit-apple-green.svg`
|
|
)
|
|
expect(statusCode).to.equal(200)
|
|
expect(headers['content-type']).to.equal('image/svg+xml;charset=utf-8')
|
|
expect(headers['content-length']).to.equal('1130')
|
|
})
|
|
|
|
it('correctly calculates the content-length header for multi-byte unicode characters', async function () {
|
|
const { headers } = await got(`${baseUrl}:fruit-apple🍏-green.json`)
|
|
expect(headers['content-length']).to.equal('100')
|
|
})
|
|
|
|
it('should produce JSON badges with expected headers', async function () {
|
|
const { statusCode, body, headers } = await got(
|
|
`${baseUrl}:fruit-apple-green.json`
|
|
)
|
|
expect(statusCode).to.equal(200)
|
|
expect(headers['content-type']).to.equal('application/json')
|
|
expect(headers['access-control-allow-origin']).to.equal('*')
|
|
expect(headers['content-length']).to.equal('92')
|
|
expect(() => JSON.parse(body)).not.to.throw()
|
|
})
|
|
|
|
it('should preserve label case', async function () {
|
|
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
|
|
expect(statusCode).to.equal(200)
|
|
expect(body).to.satisfy(isSvg).and.to.include('fRuiT')
|
|
})
|
|
|
|
// https://github.com/badges/shields/pull/1319
|
|
it('should not crash with a numeric logo', async function () {
|
|
const { statusCode, body } = await got(
|
|
`${baseUrl}:fruit-apple-green.svg?logo=1`
|
|
)
|
|
expect(statusCode).to.equal(200)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('fruit')
|
|
.and.to.include('apple')
|
|
})
|
|
|
|
it('should not crash with a numeric link', async function () {
|
|
const { statusCode, body } = await got(
|
|
`${baseUrl}:fruit-apple-green.svg?link=1`
|
|
)
|
|
expect(statusCode).to.equal(200)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('fruit')
|
|
.and.to.include('apple')
|
|
})
|
|
|
|
it('should not crash with a boolean link', async function () {
|
|
const { statusCode, body } = await got(
|
|
`${baseUrl}:fruit-apple-green.svg?link=true`
|
|
)
|
|
expect(statusCode).to.equal(200)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('fruit')
|
|
.and.to.include('apple')
|
|
})
|
|
|
|
it('should return the 404 badge for unknown badges', async function () {
|
|
const { statusCode, body } = await got(
|
|
`${baseUrl}this/is/not/a/badge.svg`,
|
|
{
|
|
throwHttpErrors: false,
|
|
}
|
|
)
|
|
expect(statusCode).to.equal(404)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('404')
|
|
.and.to.include('badge not found')
|
|
})
|
|
|
|
it('should return the 404 badge page for rando links', async function () {
|
|
const { statusCode, body } = await got(
|
|
`${baseUrl}this/is/most/definitely/not/a/badge.js`,
|
|
{
|
|
throwHttpErrors: false,
|
|
}
|
|
)
|
|
expect(statusCode).to.equal(404)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('404')
|
|
.and.to.include('badge not found')
|
|
})
|
|
|
|
it('should redirect the root as configured', async function () {
|
|
const { statusCode, headers } = await got(baseUrl, {
|
|
followRedirect: false,
|
|
})
|
|
|
|
expect(statusCode).to.equal(302)
|
|
// This value is set in `config/test.yml`
|
|
expect(headers.location).to.equal('http://frontend.example.test')
|
|
})
|
|
|
|
it('should return the 410 badge for obsolete formats', async function () {
|
|
const { statusCode, body } = await got(
|
|
`${baseUrl}badge/foo-bar-blue.jpg`,
|
|
{
|
|
throwHttpErrors: false,
|
|
}
|
|
)
|
|
// TODO It would be nice if this were 404 or 410.
|
|
expect(statusCode).to.equal(200)
|
|
expect(body)
|
|
.to.satisfy(isSvg)
|
|
.and.to.include('410')
|
|
.and.to.include('jpg no longer available')
|
|
})
|
|
})
|
|
|
|
context('`requireCloudflare` is enabled', function () {
|
|
let server
|
|
afterEach(async function () {
|
|
if (server) {
|
|
server.stop()
|
|
}
|
|
})
|
|
|
|
it('should reject requests from localhost with an empty 200 response', async function () {
|
|
this.timeout(10000)
|
|
server = await createTestServer({ public: { requireCloudflare: true } })
|
|
await server.start()
|
|
|
|
const { statusCode, body } = await got(
|
|
`${server.baseUrl}badge/foo-bar-blue.svg`
|
|
)
|
|
|
|
expect(statusCode).to.be.equal(200)
|
|
expect(body).to.equal('')
|
|
})
|
|
})
|
|
|
|
describe('`requestTimeoutSeconds` setting', function () {
|
|
let server
|
|
|
|
beforeEach(async function () {
|
|
this.timeout(10000)
|
|
|
|
// configure server to time out requests that take >2 seconds
|
|
server = await createTestServer({ public: { requestTimeoutSeconds: 2 } })
|
|
await server.start()
|
|
|
|
// /fast returns a 200 OK after a 1 second delay
|
|
server.camp.route(/^\/fast$/, (data, match, end, ask) => {
|
|
setTimeout(() => {
|
|
ask.res.statusCode = 200
|
|
ask.res.end()
|
|
}, 1000)
|
|
})
|
|
|
|
// /slow returns a 200 OK after a 3 second delay
|
|
server.camp.route(/^\/slow$/, (data, match, end, ask) => {
|
|
setTimeout(() => {
|
|
ask.res.statusCode = 200
|
|
ask.res.end()
|
|
}, 3000)
|
|
})
|
|
})
|
|
|
|
afterEach(async function () {
|
|
if (server) {
|
|
server.stop()
|
|
}
|
|
server = undefined
|
|
})
|
|
|
|
it('should time out slow requests', async function () {
|
|
this.timeout(10000)
|
|
const { statusCode, body } = await got(`${server.baseUrl}slow`, {
|
|
throwHttpErrors: false,
|
|
})
|
|
expect(statusCode).to.be.equal(408)
|
|
expect(body).to.equal('Request Timeout')
|
|
})
|
|
|
|
it('should not time out fast requests', async function () {
|
|
this.timeout(10000)
|
|
const { statusCode, body } = await got(`${server.baseUrl}fast`)
|
|
expect(statusCode).to.be.equal(200)
|
|
expect(body).to.equal('')
|
|
})
|
|
})
|
|
|
|
describe('configuration', function () {
|
|
let server
|
|
afterEach(async function () {
|
|
if (server) {
|
|
server.stop()
|
|
}
|
|
})
|
|
|
|
it('should allow to enable prometheus metrics', async function () {
|
|
// Fixes https://github.com/badges/shields/issues/2611
|
|
this.timeout(10000)
|
|
server = await createTestServer({
|
|
public: {
|
|
metrics: { prometheus: { enabled: true, endpointEnabled: true } },
|
|
},
|
|
})
|
|
await server.start()
|
|
|
|
const { statusCode } = await got(`${server.baseUrl}metrics`)
|
|
|
|
expect(statusCode).to.be.equal(200)
|
|
})
|
|
|
|
it('should allow to disable prometheus metrics', async function () {
|
|
// Fixes https://github.com/badges/shields/issues/2611
|
|
this.timeout(10000)
|
|
server = await createTestServer({
|
|
public: {
|
|
metrics: { prometheus: { enabled: true, endpointEnabled: false } },
|
|
},
|
|
})
|
|
await server.start()
|
|
|
|
const { statusCode } = await got(`${server.baseUrl}metrics`, {
|
|
throwHttpErrors: false,
|
|
})
|
|
|
|
expect(statusCode).to.be.equal(404)
|
|
})
|
|
})
|
|
|
|
describe('configuration validation', function () {
|
|
describe('influx', function () {
|
|
let customConfig
|
|
beforeEach(function () {
|
|
customConfig = config.util.toObject()
|
|
customConfig.public.metrics.influx = {
|
|
enabled: true,
|
|
url: 'http://localhost:8081/telegraf',
|
|
timeoutMilliseconds: 1000,
|
|
intervalSeconds: 2,
|
|
instanceIdFrom: 'random',
|
|
instanceIdEnvVarName: 'INSTANCE_ID',
|
|
hostnameAliases: { 'metrics-hostname': 'metrics-hostname-alias' },
|
|
envLabel: 'test-env',
|
|
}
|
|
customConfig.private = {
|
|
influx_username: 'telegraf',
|
|
influx_password: 'telegrafpass',
|
|
}
|
|
})
|
|
|
|
it('should not require influx configuration', function () {
|
|
delete customConfig.public.metrics.influx
|
|
expect(() => new Server(config.util.toObject())).to.not.throw()
|
|
})
|
|
|
|
it('should require url when influx configuration is enabled', function () {
|
|
delete customConfig.public.metrics.influx.url
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'"metrics.influx.url" is required'
|
|
)
|
|
})
|
|
|
|
it('should not require url when influx configuration is disabled', function () {
|
|
customConfig.public.metrics.influx.enabled = false
|
|
delete customConfig.public.metrics.influx.url
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
|
|
it('should require timeoutMilliseconds when influx configuration is enabled', function () {
|
|
delete customConfig.public.metrics.influx.timeoutMilliseconds
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'"metrics.influx.timeoutMilliseconds" is required'
|
|
)
|
|
})
|
|
|
|
it('should require intervalSeconds when influx configuration is enabled', function () {
|
|
delete customConfig.public.metrics.influx.intervalSeconds
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'"metrics.influx.intervalSeconds" is required'
|
|
)
|
|
})
|
|
|
|
it('should require instanceIdFrom when influx configuration is enabled', function () {
|
|
delete customConfig.public.metrics.influx.instanceIdFrom
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'"metrics.influx.instanceIdFrom" is required'
|
|
)
|
|
})
|
|
|
|
it('should require instanceIdEnvVarName when instanceIdFrom is env-var', function () {
|
|
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
|
|
delete customConfig.public.metrics.influx.instanceIdEnvVarName
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'"metrics.influx.instanceIdEnvVarName" is required'
|
|
)
|
|
})
|
|
|
|
it('should allow instanceIdFrom = hostname', function () {
|
|
customConfig.public.metrics.influx.instanceIdFrom = 'hostname'
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
|
|
it('should allow instanceIdFrom = env-var', function () {
|
|
customConfig.public.metrics.influx.instanceIdFrom = 'env-var'
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
|
|
it('should allow instanceIdFrom = random', function () {
|
|
customConfig.public.metrics.influx.instanceIdFrom = 'random'
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
|
|
it('should require envLabel when influx configuration is enabled', function () {
|
|
delete customConfig.public.metrics.influx.envLabel
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'"metrics.influx.envLabel" is required'
|
|
)
|
|
})
|
|
|
|
it('should not require hostnameAliases', function () {
|
|
delete customConfig.public.metrics.influx.hostnameAliases
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
|
|
it('should allow empty hostnameAliases', function () {
|
|
customConfig.public.metrics.influx.hostnameAliases = {}
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
|
|
it('should require username when influx configuration is enabled', function () {
|
|
delete customConfig.private.influx_username
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'Private configuration is invalid. Check these paths: influx_username'
|
|
)
|
|
})
|
|
|
|
it('should require password when influx configuration is enabled', function () {
|
|
delete customConfig.private.influx_password
|
|
expect(() => new Server(customConfig)).to.throw(
|
|
'Private configuration is invalid. Check these paths: influx_password'
|
|
)
|
|
})
|
|
|
|
it('should allow other private keys', function () {
|
|
customConfig.private.gh_token = 'my-token'
|
|
expect(() => new Server(customConfig)).to.not.throw()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('running with metrics enabled', function () {
|
|
let server, baseUrl, scope, clock
|
|
const metricsPushIntervalSeconds = 1
|
|
before('Start the server', async function () {
|
|
// Fixes https://github.com/badges/shields/issues/2611
|
|
this.timeout(10000)
|
|
process.env.INSTANCE_ID = 'test-instance'
|
|
server = await createTestServer({
|
|
public: {
|
|
metrics: {
|
|
prometheus: { enabled: true },
|
|
influx: {
|
|
enabled: true,
|
|
url: 'http://localhost:1112/metrics',
|
|
instanceIdFrom: 'env-var',
|
|
instanceIdEnvVarName: 'INSTANCE_ID',
|
|
envLabel: 'localhost-env',
|
|
intervalSeconds: metricsPushIntervalSeconds,
|
|
},
|
|
},
|
|
},
|
|
private: {
|
|
influx_username: 'influx-username',
|
|
influx_password: 'influx-password',
|
|
},
|
|
})
|
|
clock = sinon.useFakeTimers()
|
|
baseUrl = server.baseUrl
|
|
await server.start()
|
|
})
|
|
after('Shut down the server', async function () {
|
|
if (server) {
|
|
await server.stop()
|
|
}
|
|
server = undefined
|
|
nock.cleanAll()
|
|
delete process.env.INSTANCE_ID
|
|
clock.restore()
|
|
})
|
|
|
|
it('should push custom metrics', async function () {
|
|
scope = nock('http://localhost:1112', {
|
|
reqheaders: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
})
|
|
.post(
|
|
'/metrics',
|
|
/prometheus,application=shields,category=static,env=localhost-env,family=static-badge,instance=test-instance,service=static_badge service_requests_total=1\n/
|
|
)
|
|
.basicAuth({ user: 'influx-username', pass: 'influx-password' })
|
|
.reply(200)
|
|
await got(`${baseUrl}badge/fruit-apple-green.svg`)
|
|
|
|
await clock.tickAsync(1000 * metricsPushIntervalSeconds + 500)
|
|
|
|
expect(scope.isDone()).to.be.equal(
|
|
true,
|
|
`pending mocks: ${scope.pendingMocks()}`
|
|
)
|
|
})
|
|
})
|
|
})
|