diff --git a/.prettierignore b/.prettierignore index 65d17f6404dfab4257b0e5f16fb47898a32912df..dac86b5475f8fdbc4d58cbc37bc158c766996217 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,3 @@ -server.js -dangerfile.js -next.config.js -server.spec.js package.json package-lock.json /__snapshots__ diff --git a/dangerfile.js b/dangerfile.js index 4722810c502e3014d6b75c62bd9a4a2fd21da0a5..8001a6c36bbb3bba482cbdaea1b52c590072840d 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -7,133 +7,147 @@ // `./node_modules/.bin/danger pr pr-url` // Note that the line numbers in the runtime errors are incorrecr. -const { danger, fail, message, warn } = require('danger'); -const chainsmoker = require('chainsmoker'); -const { default: noTestShortcuts } = require('danger-plugin-no-test-shortcuts'); +const { danger, fail, message, warn } = require('danger') +const chainsmoker = require('chainsmoker') +const { default: noTestShortcuts } = require('danger-plugin-no-test-shortcuts') const fileMatch = chainsmoker({ created: danger.git.created_files, modified: danger.git.modified_files, createdOrModified: danger.git.modified_files.concat(danger.git.created_files), deleted: danger.git.deleted_files, -}); +}) const documentation = fileMatch( '**/*.md', 'lib/all-badge-examples.js', 'frontend/components/usage.js' -); -const server = fileMatch('server.js'); -const serviceTests = fileMatch('services/**/*.tester.js'); +) +const server = fileMatch('server.js') +const serviceTests = fileMatch('services/**/*.tester.js') const helpers = fileMatch( 'lib/**/*.js', '!**/*.spec.js', '!lib/all-badge-examples.js' -); -const logos = fileMatch( - 'logo/*.svg' -); -const helperTests = fileMatch('lib/**/*.spec.js'); -const packageJson = fileMatch('package.json'); -const packageLock = fileMatch('package-lock.json'); -const capitals = fileMatch('**/*[A-Z]*.js'); -const underscores = fileMatch('**/*_*.js'); -const targetBranch = danger.github.pr.base.ref; - -message([ - ':sparkles: Thanks for your contribution to Shields, ', - `@${danger.github.pr.user.login}!`, -].join('')); +) +const logos = fileMatch('logo/*.svg') +const helperTests = fileMatch('lib/**/*.spec.js') +const packageJson = fileMatch('package.json') +const packageLock = fileMatch('package-lock.json') +const capitals = fileMatch('**/*[A-Z]*.js') +const underscores = fileMatch('**/*_*.js') +const targetBranch = danger.github.pr.base.ref + +message( + [ + ':sparkles: Thanks for your contribution to Shields, ', + `@${danger.github.pr.user.login}!`, + ].join('') +) if (targetBranch !== 'master') { - const message = `This PR targets \`${targetBranch}\``; - const idea = 'It is likely that the target branch should be `master`'; - warn(`${message} - <i>${idea}</i>`); + const message = `This PR targets \`${targetBranch}\`` + const idea = 'It is likely that the target branch should be `master`' + warn(`${message} - <i>${idea}</i>`) } if (documentation.createdOrModified) { - message([ - 'Thanks for contributing to our documentation. ', - 'We :heart: our [documentarians](http://www.writethedocs.org/)!', - ].join('')); + message( + [ + 'Thanks for contributing to our documentation. ', + 'We :heart: our [documentarians](http://www.writethedocs.org/)!', + ].join('') + ) } if (packageJson.modified && !packageLock.modified) { - const message = 'This PR modified `package.json`, but not `package-lock.json`'; - const idea = 'Perhaps you need to run `npm install`?'; - warn(`${message} - <i>${idea}</i>`); + const message = 'This PR modified `package.json`, but not `package-lock.json`' + const idea = 'Perhaps you need to run `npm install`?' + warn(`${message} - <i>${idea}</i>`) } if (server.modified && !serviceTests.createdOrModified) { - warn([ - 'This PR modified the server but none of the service tests. <br>', - "That's okay so long as it's refactoring existing code. ", - "Otherwise, please consider adding tests to the service: ", - "[How-to](https://github.com/badges/shields/blob/master/doc/service-tests.md#readme)", - ].join('')); + warn( + [ + 'This PR modified the server but none of the service tests. <br>', + "That's okay so long as it's refactoring existing code. ", + 'Otherwise, please consider adding tests to the service: ', + '[How-to](https://github.com/badges/shields/blob/master/doc/service-tests.md#readme)', + ].join('') + ) } if (helpers.created && !helperTests.created) { - warn([ - 'This PR added helper modules in `lib/` but not accompanying tests. <br>', - 'Generally helper modules should have their own tests.', - ].join('')); + warn( + [ + 'This PR added helper modules in `lib/` but not accompanying tests. <br>', + 'Generally helper modules should have their own tests.', + ].join('') + ) } else if (helpers.createdOrModified && !helperTests.createdOrModified) { - warn([ - 'This PR modified helper functions in `lib/` but not accompanying tests. <br>', - "That's okay so long as it's refactoring existing code.", - ].join('')); + warn( + [ + 'This PR modified helper functions in `lib/` but not accompanying tests. <br>', + "That's okay so long as it's refactoring existing code.", + ].join('') + ) } if (logos.created) { - message([ - ':art: Thanks for submitting a logo. <br>', - 'Please ensure your contribution follows our ', - '[guidance](https://github.com/badges/shields/blob/master/CONTRIBUTING.md#logos) ', - 'for logo submissions.', - ].join('')); + message( + [ + ':art: Thanks for submitting a logo. <br>', + 'Please ensure your contribution follows our ', + '[guidance](https://github.com/badges/shields/blob/master/CONTRIBUTING.md#logos) ', + 'for logo submissions.', + ].join('') + ) } if (capitals.created || underscores.created) { - fail([ - 'JavaScript source files should be named with `kebab-case` ', - '(dash-separated lowercase).', - ].join('')); + fail( + [ + 'JavaScript source files should be named with `kebab-case` ', + '(dash-separated lowercase).', + ].join('') + ) } -const allFiles = danger.git.created_files.concat(danger.git.modified_files); +const allFiles = danger.git.created_files.concat(danger.git.modified_files) allFiles.forEach(file => { danger.git.diffForFile(file).then(diff => { if (/\+.*assert[(.]/.test(diff.diff)) { - warn([ - `Found 'assert' statement added in \`${file}\`. <br>`, - 'Please ensure tests are written using Chai ', - '[expect syntax](http://chaijs.com/guide/styles/#expect)', - ].join('')); + warn( + [ + `Found 'assert' statement added in \`${file}\`. <br>`, + 'Please ensure tests are written using Chai ', + '[expect syntax](http://chaijs.com/guide/styles/#expect)', + ].join('') + ) } - }); -}); + }) +}) function onlyUnique(value, index, self) { - return self.indexOf(value) === index; + return self.indexOf(value) === index } const affectedServices = allFiles .map(file => { - const match = file.match(/^services\/(.+)\/.+\.service.js$/); - return match ? match[1] : undefined; + const match = file.match(/^services\/(.+)\/.+\.service.js$/) + return match ? match[1] : undefined }) .filter(Boolean) - .filter(onlyUnique); + .filter(onlyUnique) const testedServices = allFiles .map(file => { - const match = file.match(/^services\/(.+)\/.+\.tester.js$/); - return match ? match[1] : undefined; + const match = file.match(/^services\/(.+)\/.+\.tester.js$/) + return match ? match[1] : undefined }) .filter(Boolean) - .filter(onlyUnique); + .filter(onlyUnique) affectedServices.forEach(service => { if (testedServices.indexOf(service) === -1) { @@ -142,9 +156,9 @@ affectedServices.forEach(service => { `This PR modified service code for <kbd>${service}</kbd> but not its test code. <br>`, "That's okay so long as it's refactoring existing code.", ].join('') - ); + ) } -}); +}) // Prevent merging exclusive services tests. noTestShortcuts({ @@ -152,4 +166,4 @@ noTestShortcuts({ patterns: { only: ['only()'], }, -}); +}) diff --git a/next.config.js b/next.config.js index 0254924c5eddb910d92b8914b6e9e49205e3c97f..e20cf711fe59bde888dd5503b76375a26a44f69b 100644 --- a/next.config.js +++ b/next.config.js @@ -1,43 +1,47 @@ 'use strict' -const envFlag = require('node-env-flag'); -const webpack = require('webpack'); -const shouldAnalyze = envFlag(process.env.ANALYZE); -const assetPrefix = process.env.NEXT_ASSET_PREFIX; +const envFlag = require('node-env-flag') +const webpack = require('webpack') +const shouldAnalyze = envFlag(process.env.ANALYZE) +const assetPrefix = process.env.NEXT_ASSET_PREFIX module.exports = { webpack: config => { - config.plugins.push(new webpack.EnvironmentPlugin({ BASE_URL: null, LONG_CACHE: null })); + config.plugins.push( + new webpack.EnvironmentPlugin({ BASE_URL: null, LONG_CACHE: null }) + ) if (shouldAnalyze) { // We don't include webpack-bundle-analyzer in devDependencies, so load // lazily. - const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); - config.plugins.push(new BundleAnalyzerPlugin({ - analyzerMode: 'server', - analyzerPort: 8888, - openAnalyzer: true, - })); + const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'server', + analyzerPort: 8888, + openAnalyzer: true, + }) + ) } config.module.loaders = (config.module.loaders || []).concat({ test: /\.json$/, loader: 'json-loader', - }); + }) if (assetPrefix) { - config.output.publicPath = `${assetPrefix}/${config.output.publicPath}`; + config.output.publicPath = `${assetPrefix}/${config.output.publicPath}` } - return config; + return config }, exportPathMap: () => ({ '/': { page: '/' }, }), -}; +} // Avoid setting an `undefined` value. This causes // `TypeError: Cannot read property 'replace' of undefined` at build time. if (assetPrefix) { - module.exports.assetPrefix = assetPrefix; + module.exports.assetPrefix = assetPrefix } diff --git a/server.js b/server.js index 5fc2c48d3b431b97403d64fd49cd765ff2e87874..f15189f70bdc2bf3113730cbae5987430512e7f0 100644 --- a/server.js +++ b/server.js @@ -1,41 +1,41 @@ -'use strict'; - -const { DOMParser } = require('xmldom'); -const jp = require('jsonpath'); -const path = require('path'); -const xpath = require('xpath'); -const yaml = require('js-yaml'); -const Raven = require('raven'); - -const serverSecrets = require('./lib/server-secrets'); -Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install(); -Raven.disableConsoleAlerts(); - -const { loadServiceClasses } = require('./services'); -const { checkErrorResponse } = require('./lib/error-helper'); -const analytics = require('./lib/analytics'); -const config = require('./lib/server-config'); -const GithubConstellation = require('./services/github/github-constellation'); -const sysMonitor = require('./lib/sys/monitor'); -const log = require('./lib/log'); -const { makeMakeBadgeFn } = require('./lib/make-badge'); -const { QuickTextMeasurer } = require('./lib/text-measurer'); -const suggest = require('./lib/suggest'); +'use strict' + +const { DOMParser } = require('xmldom') +const jp = require('jsonpath') +const path = require('path') +const xpath = require('xpath') +const yaml = require('js-yaml') +const Raven = require('raven') + +const serverSecrets = require('./lib/server-secrets') +Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install() +Raven.disableConsoleAlerts() + +const { loadServiceClasses } = require('./services') +const { checkErrorResponse } = require('./lib/error-helper') +const analytics = require('./lib/analytics') +const config = require('./lib/server-config') +const GithubConstellation = require('./services/github/github-constellation') +const sysMonitor = require('./lib/sys/monitor') +const log = require('./lib/log') +const { makeMakeBadgeFn } = require('./lib/make-badge') +const { QuickTextMeasurer } = require('./lib/text-measurer') +const suggest = require('./lib/suggest') const { makeColorB, makeLabel: getLabel, makeBadgeData: getBadgeData, setBadgeColor, -} = require('./lib/badge-data'); +} = require('./lib/badge-data') const { makeHandleRequestFn, clearRequestCache, -} = require('./lib/request-handler'); -const { clearRegularUpdateCache } = require('./lib/regular-update'); -const { makeSend } = require('./lib/result-sender'); -const { escapeFormat } = require('./lib/path-helpers'); +} = require('./lib/request-handler') +const { clearRegularUpdateCache } = require('./lib/regular-update') +const { makeSend } = require('./lib/result-sender') +const { escapeFormat } = require('./lib/path-helpers') -const serverStartTime = new Date((new Date()).toGMTString()); +const serverStartTime = new Date(new Date().toGMTString()) const camp = require('camp').start({ documentRoot: path.join(__dirname, 'public'), @@ -44,274 +44,292 @@ const camp = require('camp').start({ secure: config.ssl.isSecure, cert: config.ssl.cert, key: config.ssl.key, -}); +}) const githubConstellation = new GithubConstellation({ persistence: config.persistence, service: config.services.github, -}); -const { apiProvider: githubApiProvider } = githubConstellation; +}) +const { apiProvider: githubApiProvider } = githubConstellation function reset() { - clearRequestCache(); - clearRegularUpdateCache(); + clearRequestCache() + clearRegularUpdateCache() } async function stop() { - await githubConstellation.stop(); - analytics.cancelAutosaving(); + await githubConstellation.stop() + analytics.cancelAutosaving() return new Promise(resolve => { - camp.close(resolve); - }); + camp.close(resolve) + }) } module.exports = { camp, reset, stop, -}; +} -log(`Server is starting up: ${config.baseUri}`); +log(`Server is starting up: ${config.baseUri}`) -let measurer; +let measurer try { - measurer = new QuickTextMeasurer(config.font.path, config.font.fallbackPath); + measurer = new QuickTextMeasurer(config.font.path, config.font.fallbackPath) } catch (e) { - console.log(`Unable to load fallback font. Using Helvetica-Bold instead.`); - measurer = new QuickTextMeasurer('Helvetica'); + console.log(`Unable to load fallback font. Using Helvetica-Bold instead.`) + measurer = new QuickTextMeasurer('Helvetica') } -const makeBadge = makeMakeBadgeFn(measurer); -const cache = makeHandleRequestFn(makeBadge); +const makeBadge = makeMakeBadgeFn(measurer) +const cache = makeHandleRequestFn(makeBadge) -analytics.load(); -analytics.scheduleAutosaving(); -analytics.setRoutes(camp); +analytics.load() +analytics.scheduleAutosaving() +analytics.setRoutes(camp) if (serverSecrets && serverSecrets.shieldsSecret) { - sysMonitor.setRoutes(camp); + sysMonitor.setRoutes(camp) } -githubConstellation.initialize(camp); +githubConstellation.initialize(camp) -suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp); +suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp) camp.notfound(/\.(svg|png|gif|jpg|json)/, (query, match, end, request) => { - const format = match[1]; - const badgeData = getBadgeData("404", query); - badgeData.text[1] = 'badge not found'; - badgeData.colorscheme = 'red'; - // Add format to badge data. - badgeData.format = format; - const svg = makeBadge(badgeData); - makeSend(format, request.res, end)(svg); -}); + const format = match[1] + const badgeData = getBadgeData('404', query) + badgeData.text[1] = 'badge not found' + badgeData.colorscheme = 'red' + // Add format to badge data. + badgeData.format = format + const svg = makeBadge(badgeData) + makeSend(format, request.res, end)(svg) +}) camp.notfound(/.*/, (query, match, end, request) => { - end(null, { template: '404.html' }); -}); + end(null, { template: '404.html' }) +}) // Vendors. -loadServiceClasses().forEach( - serviceClass => serviceClass.register( +loadServiceClasses().forEach(serviceClass => + serviceClass.register( { camp, handleRequest: cache, githubApiProvider }, - { handleInternalErrors: config.handleInternalErrors })); + { handleInternalErrors: config.handleInternalErrors } + ) +) // User defined sources - JSON response -camp.route(/^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/, -cache({ - queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'], - handler: function(query, match, sendBadge, request) { - const type = match[1]; - const format = match[2]; - const prefix = query.prefix || ''; - const suffix = query.suffix || ''; - const pathExpression = query.query; - let requestOptions = {}; - - const badgeData = getBadgeData('custom badge', query); - - if (!query.uri && !query.url || !query.query){ - setBadgeColor(badgeData, 'red'); - badgeData.text[1] = !query.query ? 'no query specified' : 'no url specified'; - sendBadge(format, badgeData); - return; - } - - let url - try { - url = encodeURI(decodeURIComponent(query.url || query.uri)); - } catch(e){ - setBadgeColor(badgeData, 'red'); - badgeData.text[1] = 'malformed url'; - sendBadge(format, badgeData); - return; - } - - switch (type) { - case 'json': - requestOptions = { - headers: { - Accept: 'application/json', - }, - json: true, - }; - break; - case 'xml': - requestOptions = { - headers: { - Accept: 'application/xml, text/xml', - }, - }; - break; - case 'yaml': - requestOptions = { - headers: { - Accept: 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', - }, - }; - break; - } +camp.route( + /^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/, + cache({ + queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'], + handler: function(query, match, sendBadge, request) { + const type = match[1] + const format = match[2] + const prefix = query.prefix || '' + const suffix = query.suffix || '' + const pathExpression = query.query + let requestOptions = {} + + const badgeData = getBadgeData('custom badge', query) + + if ((!query.uri && !query.url) || !query.query) { + setBadgeColor(badgeData, 'red') + badgeData.text[1] = !query.query + ? 'no query specified' + : 'no url specified' + sendBadge(format, badgeData) + return + } - request(url, requestOptions, (err, res, data) => { + let url try { - if (checkErrorResponse(badgeData, err, res, { 404: 'resource not found' })) { - return; - } - - badgeData.colorscheme = 'brightgreen'; - - let innerText = []; - switch (type){ - case 'json': - data = (typeof data === 'object' ? data : JSON.parse(data)); - data = jp.query(data, pathExpression); - if (!data.length) { - throw Error('no result'); - } - innerText = data; - break; - case 'xml': - data = new DOMParser().parseFromString(data); - data = xpath.select(pathExpression, data); - if (!data.length) { - throw Error('no result'); - } - data.forEach((i,v)=>{ - innerText.push(pathExpression.indexOf('@') + 1 ? i.value : i.firstChild.data); - }); - break; - case 'yaml': - data = yaml.safeLoad(data); - data = jp.query(data, pathExpression); - if (!data.length) { - throw Error('no result'); - } - innerText = data; - break; - } - badgeData.text[1] = (prefix || '') + innerText.join(', ') + (suffix || ''); + url = encodeURI(decodeURIComponent(query.url || query.uri)) } catch (e) { - setBadgeColor(badgeData, 'lightgrey'); - badgeData.text[1] = e.message; - } finally { - sendBadge(format, badgeData); + setBadgeColor(badgeData, 'red') + badgeData.text[1] = 'malformed url' + sendBadge(format, badgeData) + return } - }); - }, -})); - -// Any badge. -camp.route(/^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/, -(data, match, end, ask) => { - const subject = escapeFormat(match[2]); - const status = escapeFormat(match[4]); - const color = escapeFormat(match[6]); - const format = match[8]; - analytics.noteRequest(data, match); + switch (type) { + case 'json': + requestOptions = { + headers: { + Accept: 'application/json', + }, + json: true, + } + break + case 'xml': + requestOptions = { + headers: { + Accept: 'application/xml, text/xml', + }, + } + break + case 'yaml': + requestOptions = { + headers: { + Accept: + 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', + }, + } + break + } - // Cache management - the badge is constant. - const cacheDuration = (3600*24*1)|0; // 1 day. - ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration); - if (+(new Date(ask.req.headers['if-modified-since'])) >= +serverStartTime) { - ask.res.statusCode = 304; - ask.res.end(); // not modified. - return; - } - ask.res.setHeader('Last-Modified', serverStartTime.toGMTString()); + request(url, requestOptions, (err, res, data) => { + try { + if ( + checkErrorResponse(badgeData, err, res, { + 404: 'resource not found', + }) + ) { + return + } + + badgeData.colorscheme = 'brightgreen' + + let innerText = [] + switch (type) { + case 'json': + data = typeof data === 'object' ? data : JSON.parse(data) + data = jp.query(data, pathExpression) + if (!data.length) { + throw Error('no result') + } + innerText = data + break + case 'xml': + data = new DOMParser().parseFromString(data) + data = xpath.select(pathExpression, data) + if (!data.length) { + throw Error('no result') + } + data.forEach((i, v) => { + innerText.push( + pathExpression.indexOf('@') + 1 ? i.value : i.firstChild.data + ) + }) + break + case 'yaml': + data = yaml.safeLoad(data) + data = jp.query(data, pathExpression) + if (!data.length) { + throw Error('no result') + } + innerText = data + break + } + badgeData.text[1] = + (prefix || '') + innerText.join(', ') + (suffix || '') + } catch (e) { + setBadgeColor(badgeData, 'lightgrey') + badgeData.text[1] = e.message + } finally { + sendBadge(format, badgeData) + } + }) + }, + }) +) - // Badge creation. - try { - const badgeData = getBadgeData(subject, data); - badgeData.text[0] = getLabel(subject, data); - badgeData.text[1] = status; - badgeData.colorB = makeColorB(color, data); - badgeData.template = data.style; - if (config.profiling.makeBadge) { - console.time('makeBadge total'); +// Any badge. +camp.route( + /^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/, + (data, match, end, ask) => { + const subject = escapeFormat(match[2]) + const status = escapeFormat(match[4]) + const color = escapeFormat(match[6]) + const format = match[8] + + analytics.noteRequest(data, match) + + // Cache management - the badge is constant. + const cacheDuration = (3600 * 24 * 1) | 0 // 1 day. + ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration) + if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) { + ask.res.statusCode = 304 + ask.res.end() // not modified. + return } - const svg = makeBadge(badgeData); - if (config.profiling.makeBadge) { - console.timeEnd('makeBadge total'); + ask.res.setHeader('Last-Modified', serverStartTime.toGMTString()) + + // Badge creation. + try { + const badgeData = getBadgeData(subject, data) + badgeData.text[0] = getLabel(subject, data) + badgeData.text[1] = status + badgeData.colorB = makeColorB(color, data) + badgeData.template = data.style + if (config.profiling.makeBadge) { + console.time('makeBadge total') + } + const svg = makeBadge(badgeData) + if (config.profiling.makeBadge) { + console.timeEnd('makeBadge total') + } + makeSend(format, ask.res, end)(svg) + } catch (e) { + log.error(e.stack) + const svg = makeBadge({ + text: ['error', 'bad badge'], + colorscheme: 'red', + }) + makeSend(format, ask.res, end)(svg) } - makeSend(format, ask.res, end)(svg); - } catch(e) { - log.error(e.stack); - const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' }); - makeSend(format, ask.res, end)(svg); } -}); +) // Production cache debugging. -let bitFlip = false; +let bitFlip = false camp.route(/^\/flip\.svg$/, (data, match, end, ask) => { - const cacheSecs = 60; - ask.res.setHeader('Cache-Control', 'max-age=' + cacheSecs); - const reqTime = new Date(); - const date = (new Date(+reqTime + cacheSecs * 1000)).toGMTString(); - ask.res.setHeader('Expires', date); - const badgeData = getBadgeData('flip', data); - bitFlip = !bitFlip; - badgeData.text[1] = bitFlip? 'on': 'off'; - badgeData.colorscheme = bitFlip? 'brightgreen': 'red'; - const svg = makeBadge(badgeData); - makeSend('svg', ask.res, end)(svg); -}); + const cacheSecs = 60 + ask.res.setHeader('Cache-Control', 'max-age=' + cacheSecs) + const reqTime = new Date() + const date = new Date(+reqTime + cacheSecs * 1000).toGMTString() + ask.res.setHeader('Expires', date) + const badgeData = getBadgeData('flip', data) + bitFlip = !bitFlip + badgeData.text[1] = bitFlip ? 'on' : 'off' + badgeData.colorscheme = bitFlip ? 'brightgreen' : 'red' + const svg = makeBadge(badgeData) + makeSend('svg', ask.res, end)(svg) +}) // Any badge, old version. -camp.route(/^\/([^/]+)\/(.+).png$/, -(data, match, end, ask) => { - const subject = match[1]; - const status = match[2]; - const color = data.color; +camp.route(/^\/([^/]+)\/(.+).png$/, (data, match, end, ask) => { + const subject = match[1] + const status = match[2] + const color = data.color // Cache management - the badge is constant. - const cacheDuration = (3600*24*1)|0; // 1 day. - ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration); - if (+(new Date(ask.req.headers['if-modified-since'])) >= +serverStartTime) { - ask.res.statusCode = 304; - ask.res.end(); // not modified. - return; + const cacheDuration = (3600 * 24 * 1) | 0 // 1 day. + ask.res.setHeader('Cache-Control', 'max-age=' + cacheDuration) + if (+new Date(ask.req.headers['if-modified-since']) >= +serverStartTime) { + ask.res.statusCode = 304 + ask.res.end() // not modified. + return } - ask.res.setHeader('Last-Modified', serverStartTime.toGMTString()); + ask.res.setHeader('Last-Modified', serverStartTime.toGMTString()) // Badge creation. try { - const badgeData = { text: [subject, status] }; - badgeData.colorscheme = color; - const svg = makeBadge(badgeData); - makeSend('png', ask.res, end)(svg); - } catch(e) { - const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' }); - makeSend('png', ask.res, end)(svg); + const badgeData = { text: [subject, status] } + badgeData.colorscheme = color + const svg = makeBadge(badgeData) + makeSend('png', ask.res, end)(svg) + } catch (e) { + const svg = makeBadge({ text: ['error', 'bad badge'], colorscheme: 'red' }) + makeSend('png', ask.res, end)(svg) } -}); +}) if (config.redirectUri) { camp.route(/^\/$/, (data, match, end, ask) => { - ask.res.statusCode = 302; - ask.res.setHeader('Location', config.redirectUri); - ask.res.end(); - }); + ask.res.statusCode = 302 + ask.res.setHeader('Location', config.redirectUri) + ask.res.end() + }) } diff --git a/server.spec.js b/server.spec.js index 23fd41a34613bc97718c485d4210a2e07a45f11e..c5d0913a638bb671012b9f0cd30a33f0b1d0a66d 100644 --- a/server.spec.js +++ b/server.spec.js @@ -1,95 +1,102 @@ -'use strict'; +'use strict' -const { expect } = require('chai'); -const config = require('./lib/test-config'); -const fetch = require('node-fetch'); -const fs = require('fs'); -const isPng = require('is-png'); -const isSvg = require('is-svg'); -const path = require('path'); -const serverHelpers = require('./lib/in-process-server-test-helpers'); -const sinon = require('sinon'); -const svg2img = require('./lib/svg-to-img'); +const { expect } = require('chai') +const config = require('./lib/test-config') +const fetch = require('node-fetch') +const fs = require('fs') +const isPng = require('is-png') +const isSvg = require('is-svg') +const path = require('path') +const serverHelpers = require('./lib/in-process-server-test-helpers') +const sinon = require('sinon') +const svg2img = require('./lib/svg-to-img') -describe('The server', function () { - const baseUri = `http://127.0.0.1:${config.port}`; +describe('The server', function() { + const baseUri = `http://127.0.0.1:${config.port}` - let server; - before('Start running the server', function () { - this.timeout(5000); - server = serverHelpers.start(); - }); - after('Shut down the server', function () { serverHelpers.stop(server); }); + let server + before('Start running the server', function() { + this.timeout(5000) + server = serverHelpers.start() + }) + after('Shut down the server', function() { + serverHelpers.stop(server) + }) - it('should produce colorscheme badges', async function () { + it('should produce colorscheme badges', async function() { // This is the first server test to run, and often times out. - this.timeout(5000); - const res = await fetch(`${baseUri}/:fruit-apple-green.svg`); - expect(res.ok).to.be.true; + this.timeout(5000) + const res = await fetch(`${baseUri}/:fruit-apple-green.svg`) + expect(res.ok).to.be.true expect(await res.text()) .to.satisfy(isSvg) .and.to.include('fruit') - .and.to.include('apple'); - }); + .and.to.include('apple') + }) - it('should produce colorscheme PNG badges', async function () { - this.timeout(5000); - const res = await fetch(`${baseUri}/:fruit-apple-green.png`); - expect(res.ok).to.be.true; - expect(await res.buffer()).to.satisfy(isPng); - }); + it('should produce colorscheme PNG badges', async function() { + this.timeout(5000) + const res = await fetch(`${baseUri}/:fruit-apple-green.png`) + expect(res.ok).to.be.true + expect(await res.buffer()).to.satisfy(isPng) + }) // https://github.com/badges/shields/pull/1319 - it('should not crash with a numeric logo', async function () { - const res = await fetch(`${baseUri}/:fruit-apple-green.svg?logo=1`); - expect(res.ok).to.be.true; + it('should not crash with a numeric logo', async function() { + const res = await fetch(`${baseUri}/:fruit-apple-green.svg?logo=1`) + expect(res.ok).to.be.true expect(await res.text()) .to.satisfy(isSvg) .and.to.include('fruit') - .and.to.include('apple'); - }); + .and.to.include('apple') + }) - it('should not crash with a numeric link', async function () { - const res = await fetch(`${baseUri}/:fruit-apple-green.svg?link=1`); - expect(res.ok).to.be.true; + it('should not crash with a numeric link', async function() { + const res = await fetch(`${baseUri}/:fruit-apple-green.svg?link=1`) + expect(res.ok).to.be.true expect(await res.text()) .to.satisfy(isSvg) .and.to.include('fruit') - .and.to.include('apple'); - }); + .and.to.include('apple') + }) - it('should not crash with a boolean link', async function () { - const res = await fetch(`${baseUri}/:fruit-apple-green.svg?link=true`); - expect(res.ok).to.be.true; + it('should not crash with a boolean link', async function() { + const res = await fetch(`${baseUri}/:fruit-apple-green.svg?link=true`) + expect(res.ok).to.be.true expect(await res.text()) .to.satisfy(isSvg) .and.to.include('fruit') - .and.to.include('apple'); - }); + .and.to.include('apple') + }) - context('with svg2img error', function () { - const expectedError = fs.readFileSync(path.resolve(__dirname, 'public', '500.html')); + context('with svg2img error', function() { + const expectedError = fs.readFileSync( + path.resolve(__dirname, 'public', '500.html') + ) - let toBufferStub; - beforeEach(function () { - toBufferStub = sinon.stub(svg2img._imageMagick.prototype, 'toBuffer') - .callsArgWith(1, Error('whoops')); - }); - afterEach(function () { toBufferStub.restore(); }); + let toBufferStub + beforeEach(function() { + toBufferStub = sinon + .stub(svg2img._imageMagick.prototype, 'toBuffer') + .callsArgWith(1, Error('whoops')) + }) + afterEach(function() { + toBufferStub.restore() + }) - it('should emit the 500 message', async function () { - const res = await fetch(`${baseUri}/:some_new-badge-green.png`); + it('should emit the 500 message', async function() { + const res = await fetch(`${baseUri}/:some_new-badge-green.png`) // This emits status code 200, though 500 would be preferable. - expect(res.status).to.equal(200); - expect(await res.text()).to.include(expectedError); - }); - }); + expect(res.status).to.equal(200) + expect(await res.text()).to.include(expectedError) + }) + }) - describe('analytics endpoint', function () { - it('should return analytics in the expected format', async function () { - const res = await fetch(`${baseUri}/$analytics/v1`); - expect(res.ok).to.be.true; - const json = await res.json(); + describe('analytics endpoint', function() { + it('should return analytics in the expected format', async function() { + const res = await fetch(`${baseUri}/$analytics/v1`) + expect(res.ok).to.be.true + const json = await res.json() const expectedKeys = [ 'vendorMonthly', 'rawMonthly', @@ -97,12 +104,14 @@ describe('The server', function () { 'rawFlatMonthly', 'vendorFlatSquareMonthly', 'rawFlatSquareMonthly', - ]; - expect(json).to.have.all.keys(...expectedKeys); + ] + expect(json).to.have.all.keys(...expectedKeys) Object.values(json).forEach(stats => { - expect(stats).to.be.an('array').with.length(36); - }); - }); - }); -}); + expect(stats) + .to.be.an('array') + .with.length(36) + }) + }) + }) +})