Compare commits
52 Commits
better-err
...
custom-fet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39c7901b5 | ||
|
|
a1cdd620e9 | ||
|
|
d02c3f045a | ||
|
|
06eb88eb31 | ||
|
|
85f65734a0 | ||
|
|
e66cfa3c21 | ||
|
|
1c48c2207f | ||
|
|
a2e0e11ead | ||
|
|
2c89a8c59f | ||
|
|
d11fa30f06 | ||
|
|
05b324093a | ||
|
|
42ed874112 | ||
|
|
e2d8c94dab | ||
|
|
91577cb6e9 | ||
|
|
d578faab50 | ||
|
|
b0f367cdfb | ||
|
|
430be7e7d1 | ||
|
|
2df8289ec8 | ||
|
|
478d14300c | ||
|
|
23ceea1d72 | ||
|
|
2bf6dfdeea | ||
|
|
e4b1fd23b1 | ||
|
|
8df5eed088 | ||
|
|
4c1eee9218 | ||
|
|
1c2920ac31 | ||
|
|
9df6aade11 | ||
|
|
01950a7852 | ||
|
|
935dd25264 | ||
|
|
c320a58a24 | ||
|
|
10f06ff175 | ||
|
|
68b1a0cfe5 | ||
|
|
39b8ff0aa8 | ||
|
|
6dc672a03d | ||
|
|
642aac6408 | ||
|
|
b2bb50234f | ||
|
|
d16c8404d6 | ||
|
|
b36de3dbf3 | ||
|
|
20d4143dfb | ||
|
|
fbe865e149 | ||
|
|
da29c92910 | ||
|
|
aa185ea07c | ||
|
|
b3b772d95c | ||
|
|
670dc2bf77 | ||
|
|
4b53ffbd3b | ||
|
|
18ff7db947 | ||
|
|
cce0104ea1 | ||
|
|
56a30ef139 | ||
|
|
46fa8adeb9 | ||
|
|
f73f828aaf | ||
|
|
9e7dfea103 | ||
|
|
9f6f064193 | ||
|
|
d5812cbce8 |
@@ -33,6 +33,9 @@ update_configs:
|
||||
- match:
|
||||
dependency_name: babel-preset-gatsby
|
||||
version_requirement: '>=0.3.0'
|
||||
- match:
|
||||
dependency_name: camelcase
|
||||
version_requirement: '>=6.0.0'
|
||||
- match:
|
||||
dependency_name: chalk
|
||||
version_requirement: '>=4.0.0'
|
||||
@@ -42,6 +45,9 @@ update_configs:
|
||||
- match:
|
||||
dependency_name: decamelize
|
||||
version_requirement: '>=4.0.0'
|
||||
- match:
|
||||
dependency_name: escape-string-regexp
|
||||
version_requirement: '>=3.0.0'
|
||||
- match:
|
||||
dependency_name: eslint-plugin-jsdoc
|
||||
version_requirement: '>=21.0.0'
|
||||
|
||||
@@ -6,6 +6,15 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: 'METRICS_PROMETHEUS_ENABLED'
|
||||
endpointEnabled: 'METRICS_PROMETHEUS_ENDPOINT_ENABLED'
|
||||
influx:
|
||||
enabled: 'METRICS_INFLUX_ENABLED'
|
||||
url: 'METRICS_INFLUX_URL'
|
||||
timeoutMilliseconds: 'METRICS_INFLUX_TIMEOUT_MILLISECONDS'
|
||||
intervalSeconds: 'METRICS_INFLUX_INTERVAL_SECONDS'
|
||||
instanceIdFrom: 'METRICS_INFLUX_INSTANCE_ID_FROM'
|
||||
instanceIdEnvVarName: 'METRICS_INFLUX_INSTANCE_ID_ENV_VAR_NAME'
|
||||
envLabel: 'METRICS_INFLUX_ENV_LABEL'
|
||||
|
||||
ssl:
|
||||
isSecure: 'HTTPS'
|
||||
@@ -53,7 +62,9 @@ public:
|
||||
|
||||
rateLimit: 'RATE_LIMIT'
|
||||
|
||||
fetchLimit: 'FETCH_LIMIT'
|
||||
integrations:
|
||||
default:
|
||||
fetchLimit: 'FETCH_LIMIT'
|
||||
|
||||
private:
|
||||
azure_devops_token: 'AZURE_DEVOPS_TOKEN'
|
||||
@@ -85,3 +96,5 @@ private:
|
||||
twitch_client_id: 'TWITCH_CLIENT_ID'
|
||||
twitch_client_secret: 'TWITCH_CLIENT_SECRET'
|
||||
wheelmap_token: 'WHEELMAP_TOKEN'
|
||||
influx_username: 'INFLUX_USERNAME'
|
||||
influx_password: 'INFLUX_PASSWORD'
|
||||
|
||||
@@ -5,7 +5,11 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
|
||||
endpointEnabled: false
|
||||
influx:
|
||||
enabled: false
|
||||
timeoutMilliseconds: 1000
|
||||
intervalSeconds: 15
|
||||
ssl:
|
||||
isSecure: false
|
||||
|
||||
@@ -30,6 +34,17 @@ public:
|
||||
|
||||
handleInternalErrors: true
|
||||
|
||||
fetchLimit: '10MB'
|
||||
integrations:
|
||||
default:
|
||||
fetchLimit: '10MB'
|
||||
|
||||
DynamicJson:
|
||||
fetchLimit: '2MB'
|
||||
|
||||
DynamicXml:
|
||||
fetchLimit: '128KB'
|
||||
|
||||
DynamicYaml:
|
||||
fetchLimit: '64KB'
|
||||
|
||||
private: {}
|
||||
|
||||
@@ -2,6 +2,7 @@ public:
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: true
|
||||
endpointEnabled: true
|
||||
|
||||
ssl:
|
||||
isSecure: true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
const bytes = require('bytes')
|
||||
// See available emoji at http://emoji.muan.co/
|
||||
const emojic = require('emojic')
|
||||
const Joi = require('@hapi/joi')
|
||||
@@ -425,7 +426,8 @@ class BaseService {
|
||||
{ camp, handleRequest, githubApiProvider, metricInstance },
|
||||
serviceConfig
|
||||
) {
|
||||
const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
|
||||
const { cacheHeaders: cacheHeaderConfig, fetchLimit } = serviceConfig
|
||||
const fetchLimitBytes = bytes.parse(fetchLimit)
|
||||
const { regex, captureNames } = prepareRoute(this.route)
|
||||
const queryParams = getQueryParamNames(this.route)
|
||||
|
||||
|
||||
38
core/server/config.js
Normal file
38
core/server/config.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict'
|
||||
const deepmerge = require('deepmerge')
|
||||
|
||||
class RedundantCustomConfiguration extends Error {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
this.name = 'RedundantCustomConfiguration'
|
||||
}
|
||||
}
|
||||
|
||||
function merge(_default, custom) {
|
||||
// Overwrites the existing array values completely rather than concatenating them
|
||||
// recipe from https://github.com/TehShrike/deepmerge#arraymerge
|
||||
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
|
||||
return deepmerge(_default, custom, {
|
||||
arrayMerge: overwriteMerge,
|
||||
})
|
||||
}
|
||||
|
||||
function checkCustomIntegrationConfiguration(config, serviceClasses) {
|
||||
const serviceNames = new Set(
|
||||
serviceClasses.map(serviceClass => serviceClass.name)
|
||||
)
|
||||
const redundantConfigurations = Object.keys(config.public.integrations)
|
||||
.filter(configName => configName !== 'default')
|
||||
.filter(configName => !serviceNames.has(configName))
|
||||
if (redundantConfigurations.length) {
|
||||
throw new RedundantCustomConfiguration(
|
||||
`Custom configurations found without a corresponding service: ${redundantConfigurations}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RedundantCustomConfiguration,
|
||||
merge,
|
||||
checkCustomIntegrationConfiguration,
|
||||
}
|
||||
61
core/server/config.spec.js
Normal file
61
core/server/config.spec.js
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict'
|
||||
|
||||
const { test, given } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const {
|
||||
RedundantCustomConfiguration,
|
||||
merge,
|
||||
checkCustomIntegrationConfiguration,
|
||||
} = require('./config')
|
||||
|
||||
describe('configuration', function() {
|
||||
test(merge, function() {
|
||||
given({ a: 2 }, {})
|
||||
.describe('copies the default value')
|
||||
.expect({ a: 2 })
|
||||
given({ a: 2 }, { a: 3 })
|
||||
.describe('overrides a primitive value')
|
||||
.expect({ a: 3 })
|
||||
given({ a: { a1: 1, a2: 2 } }, { a: { a1: 2 } })
|
||||
.describe('merges objects')
|
||||
.expect({ a: { a1: 2, a2: 2 } })
|
||||
given({ a: { a1: 1, a2: 2 } }, { a: 3 })
|
||||
.describe('overrides an object with a primitive')
|
||||
.expect({ a: 3 })
|
||||
given({ a: { a1: 1, a2: 2 } }, { a: {} })
|
||||
.describe('does not override an object with an empty object')
|
||||
.expect({ a: { a1: 1, a2: 2 } })
|
||||
given({ a: [2, 3, 4] }, { a: [5, 6] })
|
||||
.describe('overrides array')
|
||||
.expect({ a: [5, 6] })
|
||||
})
|
||||
|
||||
describe('checkCustomIntegrationConfiguration function', function() {
|
||||
it('accepts the default configuration', function() {
|
||||
const config = { public: { integrations: { default: {} } } }
|
||||
const serviceClasses = [{ name: 'SomeService' }]
|
||||
|
||||
expect(() =>
|
||||
checkCustomIntegrationConfiguration(config, serviceClasses)
|
||||
).not.throw()
|
||||
})
|
||||
|
||||
it('accepts a configuration for an existing service', function() {
|
||||
const config = { public: { integrations: { SomeService: {} } } }
|
||||
const serviceClasses = [{ name: 'SomeService' }]
|
||||
|
||||
expect(() =>
|
||||
checkCustomIntegrationConfiguration(config, serviceClasses)
|
||||
).not.throw()
|
||||
})
|
||||
|
||||
it('throws an error if a custom config does not have a corresponding service', function() {
|
||||
const config = { public: { integrations: { UnknownService: {} } } }
|
||||
const serviceClasses = [{ name: 'KnownService' }]
|
||||
|
||||
expect(() =>
|
||||
checkCustomIntegrationConfiguration(config, serviceClasses)
|
||||
).to.throw(RedundantCustomConfiguration)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,21 +1,16 @@
|
||||
'use strict'
|
||||
|
||||
const merge = require('deepmerge')
|
||||
const config = require('config').util.toObject()
|
||||
const portfinder = require('portfinder')
|
||||
const Server = require('./server')
|
||||
|
||||
function createTestServer({ port }) {
|
||||
const serverConfig = {
|
||||
...config,
|
||||
public: {
|
||||
...config.public,
|
||||
bind: {
|
||||
...config.public.bind,
|
||||
port,
|
||||
},
|
||||
},
|
||||
async function createTestServer(customConfig = {}) {
|
||||
const mergedConfig = merge(config, customConfig)
|
||||
if (!mergedConfig.public.bind.port) {
|
||||
mergedConfig.public.bind.port = await portfinder.getPortPromise()
|
||||
}
|
||||
|
||||
return new Server(serverConfig)
|
||||
return new Server(mergedConfig)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
86
core/server/influx-metrics.js
Normal file
86
core/server/influx-metrics.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const { promisify } = require('util')
|
||||
const { post } = require('request')
|
||||
const postAsync = promisify(post)
|
||||
const generateInstanceId = require('./instance-id-generator')
|
||||
const { promClientJsonToInfluxV2 } = require('./metrics/format-converters')
|
||||
const log = require('./log')
|
||||
|
||||
module.exports = class InfluxMetrics {
|
||||
constructor(metricInstance, config) {
|
||||
this._metricInstance = metricInstance
|
||||
this._config = config
|
||||
this._instanceId = this.getInstanceId()
|
||||
}
|
||||
|
||||
async sendMetrics() {
|
||||
const auth = {
|
||||
user: this._config.username,
|
||||
pass: this._config.password,
|
||||
}
|
||||
const request = {
|
||||
uri: this._config.url,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: this.metrics(),
|
||||
timeout: this._config.timeoutMillseconds,
|
||||
auth,
|
||||
}
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await postAsync(request)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
new Error(`Cannot push metrics. Cause: ${error.name}: ${error.message}`)
|
||||
)
|
||||
}
|
||||
if (response && response.statusCode >= 300) {
|
||||
log.error(
|
||||
new Error(
|
||||
`Cannot push metrics. ${response.request.href} responded with status code ${response.statusCode}`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
startPushingMetrics() {
|
||||
this._intervalId = setInterval(
|
||||
() => this.sendMetrics(),
|
||||
this._config.intervalSeconds * 1000
|
||||
)
|
||||
}
|
||||
|
||||
metrics() {
|
||||
return promClientJsonToInfluxV2(this._metricInstance.metrics(), {
|
||||
env: this._config.envLabel,
|
||||
application: 'shields',
|
||||
instance: this._instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
getInstanceId() {
|
||||
const {
|
||||
hostnameAliases = {},
|
||||
instanceIdFrom,
|
||||
instanceIdEnvVarName,
|
||||
} = this._config
|
||||
let instance
|
||||
if (instanceIdFrom === 'env-var') {
|
||||
instance = process.env[instanceIdEnvVarName]
|
||||
} else if (instanceIdFrom === 'hostname') {
|
||||
const hostname = os.hostname()
|
||||
instance = hostnameAliases[hostname] || hostname
|
||||
} else if (instanceIdFrom === 'random') {
|
||||
instance = generateInstanceId()
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
stopPushingMetrics() {
|
||||
if (this._intervalId) {
|
||||
clearInterval(this._intervalId)
|
||||
this._intervalId = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
177
core/server/influx-metrics.spec.js
Normal file
177
core/server/influx-metrics.spec.js
Normal file
@@ -0,0 +1,177 @@
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const nock = require('nock')
|
||||
const sinon = require('sinon')
|
||||
const { expect } = require('chai')
|
||||
const log = require('./log')
|
||||
const InfluxMetrics = require('./influx-metrics')
|
||||
require('../register-chai-plugins.spec')
|
||||
describe('Influx metrics', function() {
|
||||
const metricInstance = {
|
||||
metrics() {
|
||||
return [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
describe('"metrics" function', function() {
|
||||
let osHostnameStub
|
||||
afterEach(function() {
|
||||
nock.enableNetConnect()
|
||||
delete process.env.INSTANCE_ID
|
||||
if (osHostnameStub) {
|
||||
osHostnameStub.restore()
|
||||
}
|
||||
})
|
||||
it('should use an environment variable value as an instance label', async function() {
|
||||
process.env.INSTANCE_ID = 'instance3'
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
instanceIdFrom: 'env-var',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
})
|
||||
|
||||
expect(influxMetrics.metrics()).to.contain('instance=instance3')
|
||||
})
|
||||
|
||||
it('should use a hostname as an instance label', async function() {
|
||||
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'hostname',
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname')
|
||||
})
|
||||
|
||||
it('should use a random string as an instance label', async function() {
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'random',
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.match(/instance=\w+ /)
|
||||
})
|
||||
|
||||
it('should use a hostname alias as an instance label', async function() {
|
||||
osHostnameStub = sinon.stub(os, 'hostname').returns('test-hostname')
|
||||
const customConfig = {
|
||||
instanceIdFrom: 'hostname',
|
||||
hostnameAliases: { 'test-hostname': 'test-hostname-alias' },
|
||||
}
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, customConfig)
|
||||
|
||||
expect(influxMetrics.metrics()).to.be.contain(
|
||||
'instance=test-hostname-alias'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('startPushingMetrics', function() {
|
||||
let influxMetrics, clock
|
||||
beforeEach(function() {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
afterEach(function() {
|
||||
influxMetrics.stopPushingMetrics()
|
||||
nock.cleanAll()
|
||||
nock.enableNetConnect()
|
||||
delete process.env.INSTANCE_ID
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('should send metrics', async function() {
|
||||
const scope = nock('http://shields-metrics.io/', {
|
||||
reqheaders: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
.persist()
|
||||
.post(
|
||||
'/metrics',
|
||||
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11'
|
||||
)
|
||||
.basicAuth({ user: 'metrics-username', pass: 'metrics-password' })
|
||||
.reply(200)
|
||||
process.env.INSTANCE_ID = 'instance2'
|
||||
influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
url: 'http://shields-metrics.io/metrics',
|
||||
timeoutMillseconds: 100,
|
||||
intervalSeconds: 0.001,
|
||||
username: 'metrics-username',
|
||||
password: 'metrics-password',
|
||||
instanceIdFrom: 'env-var',
|
||||
instanceIdEnvVarName: 'INSTANCE_ID',
|
||||
envLabel: 'test-env',
|
||||
})
|
||||
|
||||
influxMetrics.startPushingMetrics()
|
||||
|
||||
await clock.tickAsync(10)
|
||||
expect(scope.isDone()).to.be.equal(
|
||||
true,
|
||||
`pending mocks: ${scope.pendingMocks()}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendMetrics', function() {
|
||||
beforeEach(function() {
|
||||
sinon.spy(log, 'error')
|
||||
})
|
||||
afterEach(function() {
|
||||
log.error.restore()
|
||||
nock.cleanAll()
|
||||
nock.enableNetConnect()
|
||||
})
|
||||
|
||||
const influxMetrics = new InfluxMetrics(metricInstance, {
|
||||
url: 'http://shields-metrics.io/metrics',
|
||||
timeoutMillseconds: 50,
|
||||
intervalSeconds: 0,
|
||||
username: 'metrics-username',
|
||||
password: 'metrics-password',
|
||||
})
|
||||
it('should log errors', async function() {
|
||||
nock.disableNetConnect()
|
||||
|
||||
await influxMetrics.sendMetrics()
|
||||
|
||||
expect(log.error).to.be.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. Cause: NetConnectNotAllowedError: Nock: Disallowed net connect for "shields-metrics.io:80/metrics"'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('should log error responses', async function() {
|
||||
nock('http://shields-metrics.io/')
|
||||
.persist()
|
||||
.post('/metrics')
|
||||
.reply(400)
|
||||
|
||||
await influxMetrics.sendMetrics()
|
||||
|
||||
expect(log.error).to.be.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'Cannot push metrics. http://shields-metrics.io/metrics responded with status code 400'
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
10
core/server/instance-id-generator.js
Normal file
10
core/server/instance-id-generator.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict'
|
||||
|
||||
function generateInstanceId() {
|
||||
// from https://gist.github.com/gordonbrander/2230317
|
||||
return Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)
|
||||
}
|
||||
|
||||
module.exports = generateInstanceId
|
||||
27
core/server/metrics/format-converters.js
Normal file
27
core/server/metrics/format-converters.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict'
|
||||
const groupBy = require('lodash.groupby')
|
||||
|
||||
function promClientJsonToInfluxV2(metrics, extraLabels = {}) {
|
||||
// TODO Replace with Array.prototype.flatMap() after migrating to Node.js >= 11
|
||||
const flatMap = (f, arr) => arr.reduce((acc, x) => acc.concat(f(x)), [])
|
||||
return flatMap(metric => {
|
||||
const valuesByLabels = groupBy(metric.values, value =>
|
||||
JSON.stringify(Object.entries(value.labels).sort())
|
||||
)
|
||||
return Object.values(valuesByLabels).map(metricsWithSameLabel => {
|
||||
const labels = Object.entries(metricsWithSameLabel[0].labels)
|
||||
.concat(Object.entries(extraLabels))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`)
|
||||
.join(',')
|
||||
const labelsFormatted = labels ? `,${labels}` : ''
|
||||
const values = metricsWithSameLabel
|
||||
.sort((a, b) => a.metricName.localeCompare(b.metricName))
|
||||
.map(value => `${value.metricName || metric.name}=${value.value}`)
|
||||
.join(',')
|
||||
return `prometheus${labelsFormatted} ${values}`
|
||||
})
|
||||
}, metrics).join('\n')
|
||||
}
|
||||
|
||||
module.exports = { promClientJsonToInfluxV2 }
|
||||
217
core/server/metrics/format-converters.spec.js
Normal file
217
core/server/metrics/format-converters.spec.js
Normal file
@@ -0,0 +1,217 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const prometheus = require('prom-client')
|
||||
const { promClientJsonToInfluxV2 } = require('./format-converters')
|
||||
|
||||
describe('Metric format converters', function() {
|
||||
describe('prom-client JSON to InfluxDB line protocol (version 2)', function() {
|
||||
it('converts a counter', function() {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter1=11')
|
||||
})
|
||||
|
||||
it('converts a counter (from prometheus registry)', function() {
|
||||
const register = new prometheus.Registry()
|
||||
const counter = new prometheus.Counter({
|
||||
name: 'counter1',
|
||||
help: 'counter 1 help',
|
||||
registers: [register],
|
||||
})
|
||||
counter.inc(11)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter1=11')
|
||||
})
|
||||
|
||||
it('converts a gauge', function() {
|
||||
const json = [
|
||||
{
|
||||
help: 'gause 1 help',
|
||||
name: 'gauge1',
|
||||
type: 'gauge',
|
||||
values: [{ value: 20, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||
})
|
||||
|
||||
it('converts a gauge (from prometheus registry)', function() {
|
||||
const register = new prometheus.Registry()
|
||||
const gauge = new prometheus.Gauge({
|
||||
name: 'gauge1',
|
||||
help: 'gauge 1 help',
|
||||
registers: [register],
|
||||
})
|
||||
gauge.inc(20)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(influx).to.be.equal('prometheus gauge1=20')
|
||||
})
|
||||
|
||||
const sortLines = text =>
|
||||
text
|
||||
.split('\n')
|
||||
.sort()
|
||||
.join('\n')
|
||||
|
||||
it('converts a histogram', function() {
|
||||
const json = [
|
||||
{
|
||||
name: 'histogram1',
|
||||
help: 'histogram 1 help',
|
||||
type: 'histogram',
|
||||
values: [
|
||||
{ labels: { le: 5 }, value: 1, metricName: 'histogram1_bucket' },
|
||||
{ labels: { le: 15 }, value: 2, metricName: 'histogram1_bucket' },
|
||||
{ labels: { le: 50 }, value: 2, metricName: 'histogram1_bucket' },
|
||||
{
|
||||
labels: { le: '+Inf' },
|
||||
value: 3,
|
||||
metricName: 'histogram1_bucket',
|
||||
},
|
||||
{ labels: {}, value: 111, metricName: 'histogram1_sum' },
|
||||
{ labels: {}, value: 3, metricName: 'histogram1_count' },
|
||||
],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||
prometheus,le=50 histogram1_bucket=2
|
||||
prometheus,le=15 histogram1_bucket=2
|
||||
prometheus,le=5 histogram1_bucket=1
|
||||
prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a histogram (from prometheus registry)', function() {
|
||||
const register = new prometheus.Registry()
|
||||
const histogram = new prometheus.Histogram({
|
||||
name: 'histogram1',
|
||||
help: 'histogram 1 help',
|
||||
buckets: [5, 15, 50],
|
||||
registers: [register],
|
||||
})
|
||||
histogram.observe(100)
|
||||
histogram.observe(10)
|
||||
histogram.observe(1)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,le=+Inf histogram1_bucket=3
|
||||
prometheus,le=50 histogram1_bucket=2
|
||||
prometheus,le=15 histogram1_bucket=2
|
||||
prometheus,le=5 histogram1_bucket=1
|
||||
prometheus histogram1_count=3,histogram1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a summary', function() {
|
||||
const json = [
|
||||
{
|
||||
name: 'summary1',
|
||||
help: 'summary 1 help',
|
||||
type: 'summary',
|
||||
values: [
|
||||
{ labels: { quantile: 0.1 }, value: 1 },
|
||||
{ labels: { quantile: 0.9 }, value: 100 },
|
||||
{ labels: { quantile: 0.99 }, value: 100 },
|
||||
{ metricName: 'summary1_sum', labels: {}, value: 111 },
|
||||
{ metricName: 'summary1_count', labels: {}, value: 3 },
|
||||
],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||
prometheus,quantile=0.9 summary1=100
|
||||
prometheus,quantile=0.1 summary1=1
|
||||
prometheus summary1_count=3,summary1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a summary (from prometheus registry)', function() {
|
||||
const register = new prometheus.Registry()
|
||||
const summary = new prometheus.Summary({
|
||||
name: 'summary1',
|
||||
help: 'summary 1 help',
|
||||
percentiles: [0.1, 0.9, 0.99],
|
||||
registers: [register],
|
||||
})
|
||||
summary.observe(100)
|
||||
summary.observe(10)
|
||||
summary.observe(1)
|
||||
|
||||
const influx = promClientJsonToInfluxV2(register.getMetricsAsJSON())
|
||||
|
||||
expect(sortLines(influx)).to.be.equal(
|
||||
sortLines(`prometheus,quantile=0.99 summary1=100
|
||||
prometheus,quantile=0.9 summary1=100
|
||||
prometheus,quantile=0.1 summary1=1
|
||||
prometheus summary1_count=3,summary1_sum=111`)
|
||||
)
|
||||
})
|
||||
|
||||
it('converts a counter and skip a timestamp', function() {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 4 help',
|
||||
name: 'counter4',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {}, timestamp: 1581850552292 }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json)
|
||||
|
||||
expect(influx).to.be.equal('prometheus counter4=11')
|
||||
})
|
||||
|
||||
it('converts a counter and adds extra labels', function() {
|
||||
const json = [
|
||||
{
|
||||
help: 'counter 1 help',
|
||||
name: 'counter1',
|
||||
type: 'counter',
|
||||
values: [{ value: 11, labels: {} }],
|
||||
aggregator: 'sum',
|
||||
},
|
||||
]
|
||||
|
||||
const influx = promClientJsonToInfluxV2(json, {
|
||||
instance: 'instance1',
|
||||
env: 'production',
|
||||
})
|
||||
|
||||
expect(influx).to.be.equal(
|
||||
'prometheus,env=production,instance=instance1 counter1=11'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -68,11 +68,13 @@ module.exports = class PrometheusMetrics {
|
||||
registers: [this.register],
|
||||
}),
|
||||
}
|
||||
this.interval = prometheus.collectDefaultMetrics({
|
||||
register: this.register,
|
||||
})
|
||||
}
|
||||
|
||||
async initialize(server) {
|
||||
async registerMetricsEndpoint(server) {
|
||||
const { register } = this
|
||||
this.interval = prometheus.collectDefaultMetrics({ register })
|
||||
|
||||
server.route(/^\/metrics$/, (data, match, end, ask) => {
|
||||
ask.res.setHeader('Content-Type', register.contentType)
|
||||
@@ -88,6 +90,10 @@ module.exports = class PrometheusMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
metrics() {
|
||||
return this.register.getMetricsAsJSON()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} `{ inc() {} }`.
|
||||
*/
|
||||
|
||||
@@ -7,26 +7,26 @@ const got = require('../got-test-client')
|
||||
const Metrics = require('./prometheus-metrics')
|
||||
|
||||
describe('Prometheus metrics route', function() {
|
||||
let port, baseUrl
|
||||
let port, baseUrl, camp, metrics
|
||||
beforeEach(async function() {
|
||||
port = await portfinder.getPortPromise()
|
||||
baseUrl = `http://127.0.0.1:${port}`
|
||||
})
|
||||
|
||||
let camp
|
||||
beforeEach(async function() {
|
||||
camp = Camp.start({ port, hostname: '::' })
|
||||
await new Promise(resolve => camp.on('listening', () => resolve()))
|
||||
})
|
||||
afterEach(async function() {
|
||||
if (metrics) {
|
||||
metrics.stop()
|
||||
}
|
||||
if (camp) {
|
||||
await new Promise(resolve => camp.close(resolve))
|
||||
camp = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it('returns metrics', async function() {
|
||||
new Metrics({ enabled: true }).initialize(camp)
|
||||
it('returns default metrics', async function() {
|
||||
metrics = new Metrics()
|
||||
metrics.registerMetricsEndpoint(camp)
|
||||
|
||||
const { statusCode, body } = await got(`${baseUrl}/metrics`)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
const path = require('path')
|
||||
const url = require('url')
|
||||
const { URL } = url
|
||||
const bytes = require('bytes')
|
||||
const Camp = require('camp')
|
||||
const originalJoi = require('@hapi/joi')
|
||||
const makeBadge = require('../../gh-badges/lib/make-badge')
|
||||
@@ -20,9 +19,11 @@ const {
|
||||
} = require('../base-service/legacy-request-handler')
|
||||
const { clearRegularUpdateCache } = require('../legacy/regular-update')
|
||||
const { rasterRedirectUrl } = require('../badge-urls/make-badge-url')
|
||||
const { merge, checkCustomIntegrationConfiguration } = require('./config')
|
||||
const log = require('./log')
|
||||
const sysMonitor = require('./monitor')
|
||||
const PrometheusMetrics = require('./prometheus-metrics')
|
||||
const InfluxMetrics = require('./influx-metrics')
|
||||
|
||||
const Joi = originalJoi
|
||||
.extend(base => ({
|
||||
@@ -58,6 +59,13 @@ const Joi = originalJoi
|
||||
|
||||
const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
|
||||
const requiredUrl = optionalUrl.required()
|
||||
const bytes = Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i)
|
||||
const requireFields = {
|
||||
required: schema => schema.required(),
|
||||
}
|
||||
const integrationSchema = Joi.object({
|
||||
fetchLimit: bytes.alter(requireFields),
|
||||
})
|
||||
const origins = Joi.arrayFromString().items(Joi.string().origin())
|
||||
const defaultService = Joi.object({ authorizedOrigins: origins }).default({
|
||||
authorizedOrigins: [],
|
||||
@@ -81,6 +89,33 @@ const publicConfigSchema = Joi.object({
|
||||
metrics: {
|
||||
prometheus: {
|
||||
enabled: Joi.boolean().required(),
|
||||
endpointEnabled: Joi.boolean().required(),
|
||||
},
|
||||
influx: {
|
||||
enabled: Joi.boolean().required(),
|
||||
url: Joi.string()
|
||||
.uri()
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
timeoutMilliseconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
intervalSeconds: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
instanceIdFrom: Joi.string()
|
||||
.equal('hostname', 'env-var', 'random')
|
||||
.when('enabled', { is: true, then: Joi.required() }),
|
||||
instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
|
||||
is: 'env-var',
|
||||
then: Joi.required(),
|
||||
}),
|
||||
envLabel: Joi.string().when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required(),
|
||||
}),
|
||||
hostnameAliases: Joi.object(),
|
||||
},
|
||||
},
|
||||
ssl: {
|
||||
@@ -130,7 +165,9 @@ const publicConfigSchema = Joi.object({
|
||||
},
|
||||
rateLimit: Joi.boolean().required(),
|
||||
handleInternalErrors: Joi.boolean().required(),
|
||||
fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i),
|
||||
integrations: Joi.object({
|
||||
default: integrationSchema.tailor('required').required(),
|
||||
}).pattern(Joi.string(), integrationSchema),
|
||||
}).required()
|
||||
|
||||
const privateConfigSchema = Joi.object({
|
||||
@@ -160,8 +197,13 @@ const privateConfigSchema = Joi.object({
|
||||
twitch_client_id: Joi.string(),
|
||||
twitch_client_secret: Joi.string(),
|
||||
wheelmap_token: Joi.string(),
|
||||
influx_username: Joi.string(),
|
||||
influx_password: Joi.string(),
|
||||
}).required()
|
||||
|
||||
const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
||||
influx_username: Joi.string().required(),
|
||||
influx_password: Joi.string().required(),
|
||||
})
|
||||
/**
|
||||
* The Server is based on the web framework Scoutcamp. It creates
|
||||
* an http server, sets up helpers for token persistence and monitoring.
|
||||
@@ -173,22 +215,25 @@ class Server {
|
||||
* Badge Server Constructor
|
||||
*
|
||||
* @param {object} config Configuration object read from config yaml files
|
||||
* by https://www.npmjs.com/package/config and validated against
|
||||
* publicConfigSchema and privateConfigSchema
|
||||
* by https://www.npmjs.com/package/config and validated against
|
||||
* publicConfigSchema and privateConfigSchema
|
||||
* @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
|
||||
* @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
|
||||
*/
|
||||
constructor(config) {
|
||||
const publicConfig = Joi.attempt(config.public, publicConfigSchema)
|
||||
let privateConfig
|
||||
try {
|
||||
privateConfig = Joi.attempt(config.private, privateConfigSchema)
|
||||
} catch (e) {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
const privateConfig = this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateConfigSchema
|
||||
)
|
||||
// We want to require an username and a password for the influx metrics
|
||||
// only if the influx metrics are enabled. The private config schema
|
||||
// and the public config schema are two separate schemas so we have to run
|
||||
// validation manually.
|
||||
if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
|
||||
this.validatePrivateConfig(
|
||||
config.private,
|
||||
privateMetricsInfluxConfigSchema
|
||||
)
|
||||
}
|
||||
this.config = {
|
||||
@@ -201,8 +246,31 @@ class Server {
|
||||
service: publicConfig.services.github,
|
||||
private: privateConfig,
|
||||
})
|
||||
|
||||
if (publicConfig.metrics.prometheus.enabled) {
|
||||
this.metricInstance = new PrometheusMetrics()
|
||||
if (publicConfig.metrics.influx.enabled) {
|
||||
this.influxMetrics = new InfluxMetrics(
|
||||
this.metricInstance,
|
||||
Object.assign({}, publicConfig.metrics.influx, {
|
||||
username: privateConfig.influx_username,
|
||||
password: privateConfig.influx_password,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePrivateConfig(privateConfig, privateConfigSchema) {
|
||||
try {
|
||||
return Joi.attempt(privateConfig, privateConfigSchema)
|
||||
} catch (e) {
|
||||
const badPaths = e.details.map(({ path }) => path)
|
||||
throw Error(
|
||||
`Private configuration is invalid. Check these paths: ${badPaths.join(
|
||||
','
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,19 +400,25 @@ class Server {
|
||||
const { config, camp, metricInstance } = this
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
|
||||
loadServiceClasses().forEach(serviceClass =>
|
||||
const serviceClasses = loadServiceClasses()
|
||||
checkCustomIntegrationConfiguration(config, serviceClasses)
|
||||
serviceClasses.forEach(serviceClass => {
|
||||
const serviceConfig = merge(
|
||||
config.public.integrations.default,
|
||||
config.public.integrations[serviceClass.name] || {}
|
||||
)
|
||||
serviceClass.register(
|
||||
{ camp, handleRequest, githubApiProvider, metricInstance },
|
||||
{
|
||||
handleInternalErrors: config.public.handleInternalErrors,
|
||||
cacheHeaders: config.public.cacheHeaders,
|
||||
fetchLimitBytes: bytes(config.public.fetchLimit),
|
||||
rasterUrl: config.public.rasterUrl,
|
||||
private: config.private,
|
||||
public: config.public,
|
||||
...serviceConfig,
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,7 +455,12 @@ class Server {
|
||||
const { githubConstellation } = this
|
||||
githubConstellation.initialize(camp)
|
||||
if (metricInstance) {
|
||||
metricInstance.initialize(camp)
|
||||
if (this.config.public.metrics.prometheus.endpointEnabled) {
|
||||
metricInstance.registerMetricsEndpoint(camp)
|
||||
}
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.startPushingMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
const { apiProvider: githubApiProvider } = this.githubConstellation
|
||||
@@ -425,6 +504,9 @@ class Server {
|
||||
}
|
||||
|
||||
if (this.metricInstance) {
|
||||
if (this.influxMetrics) {
|
||||
this.influxMetrics.stopPushingMetrics()
|
||||
}
|
||||
this.metricInstance.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,163 +2,333 @@
|
||||
|
||||
const { expect } = require('chai')
|
||||
const isSvg = require('is-svg')
|
||||
const portfinder = require('portfinder')
|
||||
const config = require('config')
|
||||
const got = require('../got-test-client')
|
||||
const Server = require('./server')
|
||||
const { createTestServer } = require('./in-process-server-test-helpers')
|
||||
|
||||
describe('The server', function() {
|
||||
let server, baseUrl
|
||||
before('Start the server', async function() {
|
||||
// Fixes https://github.com/badges/shields/issues/2611
|
||||
this.timeout(10000)
|
||||
const port = await portfinder.getPortPromise()
|
||||
server = createTestServer({ port })
|
||||
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 = createTestServer({
|
||||
port: '\\\\.\\pipe\\9c137306-7c4d-461e-b7cf-5213a3939ad6',
|
||||
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()
|
||||
baseUrl = server.baseUrl
|
||||
await server.start()
|
||||
})
|
||||
after('Shut down the server', async function() {
|
||||
if (server) {
|
||||
await server.stop()
|
||||
}
|
||||
server = undefined
|
||||
})
|
||||
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 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 redirect colorscheme PNG badges as configured', async function() {
|
||||
const { statusCode, headers } = await got(
|
||||
`${baseUrl}:fruit-apple-green.png`,
|
||||
{
|
||||
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 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}npm/v/express.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}npm/v/express.png`, {
|
||||
followRedirect: false,
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
expect(statusCode).to.equal(301)
|
||||
expect(headers.location).to.equal(
|
||||
'http://raster.example.test/npm/v/express.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('should produce json badges', async function() {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}twitter/follow/_Pyves.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
})
|
||||
it('should produce json badges', async function() {
|
||||
const { statusCode, body, headers } = await got(
|
||||
`${baseUrl}twitter/follow/_Pyves.json`
|
||||
)
|
||||
expect(statusCode).to.equal(200)
|
||||
expect(headers['content-type']).to.equal('application/json')
|
||||
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')
|
||||
})
|
||||
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')
|
||||
})
|
||||
// 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 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 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 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`,
|
||||
{
|
||||
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}npm/v/express.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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('configuration', function() {
|
||||
let server
|
||||
afterEach(async function() {
|
||||
if (server) {
|
||||
server.stop()
|
||||
}
|
||||
)
|
||||
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 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)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the 410 badge for obsolete formats', async function() {
|
||||
const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
|
||||
throwHttpErrors: false,
|
||||
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()
|
||||
})
|
||||
})
|
||||
// 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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,8 +73,14 @@ if (process.env.TESTED_SERVER_URL) {
|
||||
} else {
|
||||
const port = 1111
|
||||
baseUrl = 'http://localhost:1111'
|
||||
before('Start running the server', function() {
|
||||
server = createTestServer({ port })
|
||||
before('Start running the server', async function() {
|
||||
server = await createTestServer({
|
||||
public: {
|
||||
bind: {
|
||||
port,
|
||||
},
|
||||
},
|
||||
})
|
||||
server.start()
|
||||
})
|
||||
after('Shut down the server', async function() {
|
||||
|
||||
@@ -8,7 +8,7 @@ You will need Node 8 or later, which you can install using a
|
||||
On Ubuntu / Debian:
|
||||
|
||||
```sh
|
||||
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -; sudo apt-get install -y nodejs
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -; sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
```sh
|
||||
|
||||
1192
package-lock.json
generated
1192
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -29,11 +29,12 @@
|
||||
"camp": "~17.2.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chalk": "^3.0.0",
|
||||
"check-node-version": "^4.0.2",
|
||||
"check-node-version": "^4.0.3",
|
||||
"chrome-web-store-item-property": "~1.2.0",
|
||||
"config": "^3.3.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"decamelize": "^3.2.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dotenv": "^8.2.0",
|
||||
"emojic": "^1.1.15",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
@@ -43,11 +44,12 @@
|
||||
"glob": "^7.1.6",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"ioredis": "4.16.0",
|
||||
"ioredis": "4.16.2",
|
||||
"joi-extension-semver": "4.0.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"jsonpath": "~1.0.2",
|
||||
"lodash.countby": "^4.6.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"moment": "^2.24.0",
|
||||
"node-env-flag": "^0.1.0",
|
||||
@@ -56,10 +58,10 @@
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"priorityqueuejs": "^1.0.0",
|
||||
"prom-client": "^11.5.3",
|
||||
"query-string": "^6.11.1",
|
||||
"query-string": "^6.12.1",
|
||||
"request": "~2.88.2",
|
||||
"semver": "~7.1.3",
|
||||
"simple-icons": "2.8.0",
|
||||
"semver": "~7.3.2",
|
||||
"simple-icons": "2.9.0",
|
||||
"xmldom": "~0.2.1",
|
||||
"xpath": "~0.0.27"
|
||||
},
|
||||
@@ -151,13 +153,13 @@
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.groupby": "^4.6.6",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/node": "^13.11.1",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/react-modal": "^3.10.5",
|
||||
"@types/react-select": "^3.0.11",
|
||||
"@types/styled-components": "4.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^2.25.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.28.0",
|
||||
"@typescript-eslint/parser": "^2.28.0",
|
||||
"babel-plugin-inline-react-svg": "^1.1.1",
|
||||
"babel-plugin-istanbul": "^6.0.0",
|
||||
"babel-preset-gatsby": "^0.2.36",
|
||||
@@ -170,9 +172,10 @@
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^3.1.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"cypress": "^4.3.0",
|
||||
"danger": "^10.0.0",
|
||||
"cypress": "^4.4.0",
|
||||
"danger": "^10.1.1",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -181,7 +184,7 @@
|
||||
"eslint-config-standard-react": "^9.2.0",
|
||||
"eslint-plugin-chai-friendly": "^0.5.0",
|
||||
"eslint-plugin-cypress": "^2.10.3",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-jsdoc": "^20.4.0",
|
||||
"eslint-plugin-mocha": "^6.3.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
@@ -189,7 +192,7 @@
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^2.5.1",
|
||||
"eslint-plugin-sort-class-members": "^1.6.0",
|
||||
"eslint-plugin-sort-class-members": "^1.7.0",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"fetch-ponyfill": "^6.1.0",
|
||||
"fs-readfile-promise": "^3.0.1",
|
||||
@@ -208,11 +211,10 @@
|
||||
"is-png": "^2.0.0",
|
||||
"is-svg": "^4.2.1",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^3.6.3",
|
||||
"jsdoc": "^3.6.4",
|
||||
"lint-staged": "^9.5.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"minimist": "^1.2.5",
|
||||
"mocha": "^6.2.3",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
@@ -220,9 +222,9 @@
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "11.9.1",
|
||||
"node-mocks-http": "^1.8.1",
|
||||
"nodemon": "^2.0.2",
|
||||
"nodemon": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^15.0.0",
|
||||
"nyc": "^15.0.1",
|
||||
"opn-cli": "^5.0.0",
|
||||
"portfinder": "^1.0.25",
|
||||
"prettier": "1.19.1",
|
||||
@@ -242,7 +244,7 @@
|
||||
"sinon-chai": "^3.5.0",
|
||||
"snap-shot-it": "^7.9.3",
|
||||
"start-server-and-test": "1.10.7",
|
||||
"styled-components": "^5.0.1",
|
||||
"styled-components": "^5.1.0",
|
||||
"tmp": "0.1.0",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"typescript": "^3.8.3"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const { metric } = require('../text-formatters')
|
||||
const { downloadCount } = require('../color-formatters')
|
||||
const { BaseAmoService, keywords } = require('./amo-base')
|
||||
const { redirector } = require('..')
|
||||
const { BaseAmoService, keywords } = require('./amo-base')
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { renderBuildStatusBadge } = require('../build-status')
|
||||
const AppVeyorBase = require('./appveyor-base')
|
||||
const { NotFound } = require('..')
|
||||
const AppVeyorBase = require('./appveyor-base')
|
||||
|
||||
module.exports = class AppVeyorJobBuild extends AppVeyorBase {
|
||||
static get route() {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { test, given } = require('sazerac')
|
||||
const AppveyorJobBuild = require('./appveyor-job-build.service')
|
||||
const { NotFound } = require('..')
|
||||
const AppveyorJobBuild = require('./appveyor-job-build.service')
|
||||
|
||||
describe('AppveyorJobBuild', function() {
|
||||
test(AppveyorJobBuild.prototype.transform, () => {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { renderBuildStatusBadge } = require('../build-status')
|
||||
const { keywords, fetch } = require('./azure-devops-helpers')
|
||||
const { BaseSvgScrapingService, NotFound } = require('..')
|
||||
const { keywords, fetch } = require('./azure-devops-helpers')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
stage: Joi.string(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { renderBuildStatusBadge } = require('../build-status')
|
||||
const { keywords, fetch } = require('./azure-devops-helpers')
|
||||
const { BaseSvgScrapingService } = require('..')
|
||||
const { keywords, fetch } = require('./azure-devops-helpers')
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { renderVersionBadge } = require('../version')
|
||||
const BaseBowerService = require('./bower-base')
|
||||
const { InvalidResponse, redirector } = require('..')
|
||||
const BaseBowerService = require('./bower-base')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
include_prereleases: Joi.equal(''),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const { metric } = require('../text-formatters')
|
||||
const { downloadCount } = require('../color-formatters')
|
||||
const BaseChromeWebStoreService = require('./chrome-web-store-base')
|
||||
const { redirector, NotFound } = require('..')
|
||||
const BaseChromeWebStoreService = require('./chrome-web-store-base')
|
||||
|
||||
class ChromeWebStoreUsers extends BaseChromeWebStoreService {
|
||||
static get category() {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { version: versionColor } = require('../color-formatters')
|
||||
const { BaseClojarsService } = require('./clojars-base')
|
||||
const { redirector } = require('..')
|
||||
const { BaseClojarsService } = require('./clojars-base')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
include_prereleases: Joi.equal(''),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { codacyGrade } = require('./codacy-helpers')
|
||||
const { BaseSvgScrapingService } = require('..')
|
||||
const { codacyGrade } = require('./codacy-helpers')
|
||||
|
||||
const schema = Joi.object({ message: codacyGrade }).required()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const Joi = require('@hapi/joi')
|
||||
const { colorScale, letterScore } = require('../color-formatters')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
|
||||
const { BaseJsonService, NotFound } = require('..')
|
||||
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
|
||||
@@ -4,6 +4,9 @@ const Joi = require('@hapi/joi')
|
||||
const { isIntegerPercentage } = require('../test-validators')
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
|
||||
// Examples for this service can be found through the explore page:
|
||||
// https://codeclimate.com/explore
|
||||
|
||||
t.create('issues count')
|
||||
.get('/issues/angular/angular.json')
|
||||
.expectBadge({
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { coveragePercentage, letterScore } = require('../color-formatters')
|
||||
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
|
||||
const { BaseJsonService, NotFound } = require('..')
|
||||
const { keywords, isLetterGrade, fetchRepo } = require('./codeclimate-common')
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
|
||||
@@ -4,15 +4,18 @@ const Joi = require('@hapi/joi')
|
||||
const { isIntegerPercentage } = require('../test-validators')
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
|
||||
// Examples for this service can be found through the explore page:
|
||||
// https://codeclimate.com/explore
|
||||
|
||||
t.create('test coverage percentage')
|
||||
.get('/coverage/jekyll/jekyll.json')
|
||||
.get('/coverage/codeclimate/minidoc.json')
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: isIntegerPercentage,
|
||||
})
|
||||
|
||||
t.create('test coverage letter')
|
||||
.get('/coverage-letter/jekyll/jekyll.json')
|
||||
.get('/coverage-letter/codeclimate/minidoc.json')
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { isValidGrade, gradeColor } = require('./codefactor-helpers')
|
||||
const { BaseSvgScrapingService } = require('..')
|
||||
const { isValidGrade, gradeColor } = require('./codefactor-helpers')
|
||||
|
||||
const schema = Joi.object({
|
||||
message: isValidGrade,
|
||||
|
||||
59
services/conda/conda-license.service.js
Normal file
59
services/conda/conda-license.service.js
Normal file
@@ -0,0 +1,59 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { renderLicenseBadge } = require('../licenses')
|
||||
const toArray = require('../../core/base-service/to-array')
|
||||
const BaseCondaService = require('./conda-base')
|
||||
|
||||
const schema = Joi.object({
|
||||
license: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
module.exports = class CondaLicense extends BaseCondaService {
|
||||
static get category() {
|
||||
return 'license'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'conda',
|
||||
pattern: 'l/:channel/:pkg',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'Conda - License',
|
||||
pattern: 'l/:channel/:package',
|
||||
namedParams: {
|
||||
channel: 'conda-forge',
|
||||
package: 'setuptools',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
variant: 'l',
|
||||
channel: 'conda-forge',
|
||||
licenses: ['MIT'],
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'license' }
|
||||
}
|
||||
|
||||
static render({ licenses }) {
|
||||
return renderLicenseBadge({ licenses })
|
||||
}
|
||||
|
||||
async handle({ channel, pkg }) {
|
||||
const json = await this._requestJson({
|
||||
schema,
|
||||
url: `https://api.anaconda.org/package/${channel}/${pkg}`,
|
||||
})
|
||||
return this.constructor.render({
|
||||
licenses: toArray(json.license),
|
||||
})
|
||||
}
|
||||
}
|
||||
11
services/conda/conda-license.tester.js
Normal file
11
services/conda/conda-license.tester.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
|
||||
t.create('license')
|
||||
.get('/l/conda-forge/setuptools.json')
|
||||
.expectBadge({ label: 'license', message: 'MIT', color: 'green' })
|
||||
|
||||
t.create('license (invalid)')
|
||||
.get('/l/conda-forge/some-bogus-package-that-never-exists.json')
|
||||
.expectBadge({ label: 'license', message: 'not found', color: 'red' })
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const { downloadCount: downloadCountColor } = require('../color-formatters')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { BaseCratesService, keywords } = require('./crates-base')
|
||||
const { InvalidParameter, NotFound } = require('..')
|
||||
const { BaseCratesService, keywords } = require('./crates-base')
|
||||
|
||||
module.exports = class CratesDownloads extends BaseCratesService {
|
||||
static get category() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { renderVersionBadge } = require('../version')
|
||||
const { BaseCratesService, keywords } = require('./crates-base')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { BaseCratesService, keywords } = require('./crates-base')
|
||||
|
||||
module.exports = class CratesVersion extends BaseCratesService {
|
||||
static get category() {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const { test, given } = require('sazerac')
|
||||
const { expect } = require('chai')
|
||||
const CratesVersion = require('./crates-version.service')
|
||||
const { InvalidResponse } = require('..')
|
||||
const CratesVersion = require('./crates-version.service')
|
||||
|
||||
describe('CratesVersion', function() {
|
||||
test(CratesVersion.prototype.transform, () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { BaseJsonService } = require('..')
|
||||
const {
|
||||
dockerBlue,
|
||||
buildDockerUrl,
|
||||
getDockerHubUser,
|
||||
} = require('./docker-helpers')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const automatedBuildSchema = Joi.object({
|
||||
is_automated: Joi.boolean().required(),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { anyInteger } = require('../validators')
|
||||
const { BaseJsonService } = require('..')
|
||||
const {
|
||||
dockerBlue,
|
||||
buildDockerUrl,
|
||||
getDockerHubUser,
|
||||
} = require('./docker-helpers')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const buildSchema = Joi.object({
|
||||
results: Joi.array()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { BaseJsonService } = require('..')
|
||||
const { dockerBlue, buildDockerUrl } = require('./docker-helpers')
|
||||
const { fetchBuild } = require('./docker-cloud-common-fetch')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
module.exports = class DockerCloudAutomatedBuild extends BaseJsonService {
|
||||
static get category() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { BaseJsonService } = require('..')
|
||||
const { dockerBlue, buildDockerUrl } = require('./docker-helpers')
|
||||
const { fetchBuild } = require('./docker-cloud-common-fetch')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
module.exports = class DockerCloudBuild extends BaseJsonService {
|
||||
static get category() {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
const Joi = require('@hapi/joi')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { BaseJsonService } = require('..')
|
||||
const {
|
||||
dockerBlue,
|
||||
buildDockerUrl,
|
||||
getDockerHubUser,
|
||||
} = require('./docker-helpers')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const pullsSchema = Joi.object({
|
||||
pull_count: nonNegativeInteger,
|
||||
|
||||
@@ -4,13 +4,12 @@ const Joi = require('@hapi/joi')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { latest } = require('../version')
|
||||
const { BaseJsonService, NotFound } = require('..')
|
||||
const {
|
||||
buildDockerUrl,
|
||||
getDockerHubUser,
|
||||
getMultiPageData,
|
||||
} = require('./docker-helpers')
|
||||
const { NotFound } = require('..')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const buildSchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
const { metric } = require('../text-formatters')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { BaseService } = require('..')
|
||||
const {
|
||||
dockerBlue,
|
||||
buildDockerUrl,
|
||||
getDockerHubUser,
|
||||
} = require('./docker-helpers')
|
||||
const { BaseService } = require('..')
|
||||
|
||||
module.exports = class DockerStars extends BaseService {
|
||||
static get category() {
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
const Joi = require('@hapi/joi')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { latest, renderVersionBadge } = require('../version')
|
||||
const { BaseJsonService, NotFound, InvalidResponse } = require('..')
|
||||
const {
|
||||
buildDockerUrl,
|
||||
getDockerHubUser,
|
||||
getMultiPageData,
|
||||
getDigestSemVerMatches,
|
||||
} = require('./docker-helpers')
|
||||
const { NotFound, InvalidResponse } = require('..')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const buildSchema = Joi.object({
|
||||
count: nonNegativeInteger.required(),
|
||||
@@ -97,14 +96,15 @@ module.exports = class DockerVersion extends BaseJsonService {
|
||||
if (version !== 'latest') {
|
||||
return { version }
|
||||
}
|
||||
if (Object.keys(data.results[0].images).length === 0) {
|
||||
const imageTag = data.results[0].images.find(
|
||||
i => i.architecture === 'amd64'
|
||||
) // Digest is the unique field that we utilise to match images
|
||||
if (!imageTag) {
|
||||
throw new InvalidResponse({
|
||||
prettyMessage: 'digest not found for latest tag',
|
||||
})
|
||||
}
|
||||
const { digest } = data.results[0].images.find(
|
||||
i => i.architecture === 'amd64'
|
||||
) // Digest is the unique field that we utilise to match images
|
||||
const { digest } = imageTag
|
||||
return { version: getDigestSemVerMatches({ data: pagedData, digest }) }
|
||||
} else if (!tag && sort === 'semver') {
|
||||
const matches = data.map(d => d.name)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { test, given } = require('sazerac')
|
||||
const { InvalidResponse } = require('..')
|
||||
const DockerVersion = require('./docker-version.service')
|
||||
const {
|
||||
versionDataNoTagDateSort,
|
||||
@@ -47,4 +49,33 @@ describe('DockerVersion', function() {
|
||||
version: '3.10.4',
|
||||
})
|
||||
})
|
||||
|
||||
it('throws InvalidResponse error with latest tag and no amd64 architecture digests', function() {
|
||||
expect(() => {
|
||||
DockerVersion.prototype.transform({
|
||||
sort: 'date',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
name: 'latest',
|
||||
images: [
|
||||
{
|
||||
architecture: 'arm64',
|
||||
digest:
|
||||
'sha256:597bd5c319cc09d6bb295b4ef23cac50ec7c373fff5fe923cfd246ec09967b31',
|
||||
},
|
||||
{
|
||||
architecture: 'arm',
|
||||
digest:
|
||||
'sha256:c5ea49127cd44d0f50eafda229a056bb83b6e691883c56fd863d42675fae3909',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
.to.throw(InvalidResponse)
|
||||
.with.property('prettyMessage', 'digest not found for latest tag')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const { MetricNames } = require('../../core/base-service/metric-helper')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { createRoute } = require('./dynamic-helpers')
|
||||
const jsonPath = require('./json-path')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
module.exports = class DynamicJson extends jsonPath(BaseJsonService) {
|
||||
static get enabledMetrics() {
|
||||
|
||||
@@ -4,8 +4,8 @@ const { DOMParser } = require('xmldom')
|
||||
const xpath = require('xpath')
|
||||
const { MetricNames } = require('../../core/base-service/metric-helper')
|
||||
const { renderDynamicBadge, errorMessages } = require('../dynamic-common')
|
||||
const { createRoute } = require('./dynamic-helpers')
|
||||
const { BaseService, InvalidResponse, InvalidParameter } = require('..')
|
||||
const { createRoute } = require('./dynamic-helpers')
|
||||
|
||||
// This service extends BaseService because it uses a different XML parser
|
||||
// than BaseXmlService which can be used with xpath.
|
||||
|
||||
@@ -4,8 +4,8 @@ const { expect } = require('chai')
|
||||
const sinon = require('sinon')
|
||||
const xpath = require('xpath')
|
||||
const { test, given } = require('sazerac')
|
||||
const DynamicXml = require('./dynamic-xml.service')
|
||||
const { InvalidResponse } = require('..')
|
||||
const DynamicXml = require('./dynamic-xml.service')
|
||||
|
||||
const exampleXml = `<?xml version="1.0"?>
|
||||
<catalog>
|
||||
|
||||
@@ -153,7 +153,7 @@ t.create('XPath parse error')
|
||||
|
||||
t.create('XML from url | invalid url')
|
||||
.get(
|
||||
'.json?url=https://github.com/badges/shields/raw/master/notafile.xml&query=//version'
|
||||
'.json?url=https://raw.githubusercontent.com/badges/shields/master/notafile.xml&query=//version'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'custom badge',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const { MetricNames } = require('../../core/base-service/metric-helper')
|
||||
const { BaseYamlService } = require('..')
|
||||
const { createRoute } = require('./dynamic-helpers')
|
||||
const jsonPath = require('./json-path')
|
||||
const { BaseYamlService } = require('..')
|
||||
|
||||
module.exports = class DynamicYaml extends jsonPath(BaseYamlService) {
|
||||
static get enabledMetrics() {
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
const gql = require('graphql-tag')
|
||||
const { mergeQueries } = require('../../core/base-service/graphql')
|
||||
const { BaseGraphqlService, BaseJsonService } = require('..')
|
||||
const { staticAuthConfigured } = require('./github-helpers')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { BaseGraphqlService } = require('..')
|
||||
|
||||
function createRequestFetcher(context, config) {
|
||||
const { sendAndCacheRequestWithCallbacks, githubApiProvider } = context
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { NotFound, InvalidParameter } = require('..')
|
||||
const { GithubAuthV3Service } = require('./github-auth-service')
|
||||
const { documentation, errorMessagesFor } = require('./github-helpers')
|
||||
const { NotFound, InvalidParameter } = require('..')
|
||||
|
||||
const schema = Joi.object({
|
||||
// https://stackoverflow.com/a/23969867/893113
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { errorMessagesFor } = require('./github-helpers')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { errorMessagesFor } = require('./github-helpers')
|
||||
|
||||
const issueSchema = Joi.object({
|
||||
head: Joi.object({
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { latest } = require('../version')
|
||||
const { errorMessagesFor } = require('./github-helpers')
|
||||
const { NotFound } = require('..')
|
||||
const { errorMessagesFor } = require('./github-helpers')
|
||||
|
||||
const releaseInfoSchema = Joi.object({
|
||||
tag_name: Joi.string().required(),
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
const gql = require('graphql-tag')
|
||||
const Joi = require('@hapi/joi')
|
||||
const { NotFound } = require('..')
|
||||
const { GithubAuthV4Service } = require('./github-auth-service')
|
||||
const { documentation, transformErrors } = require('./github-helpers')
|
||||
const { NotFound } = require('..')
|
||||
|
||||
const greenStates = ['SUCCESS']
|
||||
const redStates = ['ERROR', 'FAILURE']
|
||||
|
||||
@@ -4,9 +4,9 @@ const Joi = require('@hapi/joi')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { downloadCount: downloadCountColor } = require('../color-formatters')
|
||||
const { NotFound } = require('..')
|
||||
const { GithubAuthV3Service } = require('./github-auth-service')
|
||||
const { documentation, errorMessagesFor } = require('./github-helpers')
|
||||
const { NotFound } = require('..')
|
||||
|
||||
const releaseSchema = Joi.object({
|
||||
assets: Joi.array()
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { renderVersionBadge } = require('../version')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { ConditionalGithubAuthV3Service } = require('./github-auth-service')
|
||||
const { fetchRepoContent } = require('./github-common-fetch')
|
||||
const { documentation } = require('./github-helpers')
|
||||
const { InvalidResponse } = require('..')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
filename: Joi.string(),
|
||||
|
||||
@@ -4,6 +4,7 @@ const Joi = require('@hapi/joi')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { formatDate, metric } = require('../text-formatters')
|
||||
const { age } = require('../color-formatters')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { GithubAuthV3Service } = require('./github-auth-service')
|
||||
const {
|
||||
documentation,
|
||||
@@ -11,7 +12,6 @@ const {
|
||||
stateColor,
|
||||
commentsColor,
|
||||
} = require('./github-helpers')
|
||||
const { InvalidResponse } = require('..')
|
||||
|
||||
const commonSchemaFields = {
|
||||
number: nonNegativeInteger,
|
||||
|
||||
@@ -4,9 +4,9 @@ const { expect } = require('chai')
|
||||
const { test, given } = require('sazerac')
|
||||
const { age } = require('../color-formatters')
|
||||
const { formatDate, metric } = require('../text-formatters')
|
||||
const { InvalidResponse } = require('..')
|
||||
const GithubIssueDetail = require('./github-issue-detail.service')
|
||||
const { stateColor, commentsColor } = require('./github-helpers')
|
||||
const { InvalidResponse } = require('..')
|
||||
|
||||
describe('GithubIssueDetail', function() {
|
||||
test(GithubIssueDetail.render, () => {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
const { renderVersionBadge } = require('../version')
|
||||
const { isLockfile, getDependencyVersion } = require('../pipenv-helpers')
|
||||
const { addv } = require('../text-formatters')
|
||||
const { NotFound } = require('..')
|
||||
const { ConditionalGithubAuthV3Service } = require('./github-auth-service')
|
||||
const { fetchJsonFromRepo } = require('./github-common-fetch')
|
||||
const { documentation: githubDocumentation } = require('./github-helpers')
|
||||
const { NotFound } = require('..')
|
||||
|
||||
const keywords = ['pipfile']
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
const { addv } = require('../text-formatters')
|
||||
const { version: versionColor } = require('../color-formatters')
|
||||
const { redirector } = require('..')
|
||||
const { GithubAuthV3Service } = require('./github-auth-service')
|
||||
const {
|
||||
fetchLatestRelease,
|
||||
queryParamSchema,
|
||||
} = require('./github-common-release')
|
||||
const { documentation } = require('./github-helpers')
|
||||
const { redirector } = require('..')
|
||||
|
||||
class GithubRelease extends GithubAuthV3Service {
|
||||
static get category() {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
const Joi = require('@hapi/joi')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { NotFound } = require('..')
|
||||
const { GithubAuthV3Service } = require('./github-auth-service')
|
||||
const { documentation, errorMessagesFor } = require('./github-helpers')
|
||||
const { NotFound } = require('..')
|
||||
|
||||
const schema = Joi.alternatives(
|
||||
Joi.object({
|
||||
|
||||
@@ -5,10 +5,10 @@ const Joi = require('@hapi/joi')
|
||||
const { addv } = require('../text-formatters')
|
||||
const { version: versionColor } = require('../color-formatters')
|
||||
const { latest } = require('../version')
|
||||
const { NotFound, redirector } = require('..')
|
||||
const { GithubAuthV4Service } = require('./github-auth-service')
|
||||
const { queryParamSchema } = require('./github-common-release')
|
||||
const { documentation, transformErrors } = require('./github-helpers')
|
||||
const { NotFound, redirector } = require('..')
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { isBuildStatus, renderBuildStatusBadge } = require('../build-status')
|
||||
const { documentation } = require('./github-helpers')
|
||||
const { BaseSvgScrapingService } = require('..')
|
||||
const { documentation } = require('./github-helpers')
|
||||
|
||||
const schema = Joi.object({
|
||||
message: Joi.alternatives()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { buildRedirectUrl } = require('./jenkins-common')
|
||||
const { redirector } = require('..')
|
||||
const { buildRedirectUrl } = require('./jenkins-common')
|
||||
|
||||
const commonProps = {
|
||||
category: 'build',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { buildRedirectUrl } = require('./jenkins-common')
|
||||
const { redirector } = require('..')
|
||||
const { buildRedirectUrl } = require('./jenkins-common')
|
||||
|
||||
const commonProps = {
|
||||
category: 'coverage',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { buildRedirectUrl } = require('./jenkins-common')
|
||||
const { redirector } = require('..')
|
||||
const { buildRedirectUrl } = require('./jenkins-common')
|
||||
|
||||
const commonProps = {
|
||||
category: 'build',
|
||||
|
||||
@@ -7,13 +7,13 @@ const {
|
||||
renderTestResultBadge,
|
||||
} = require('../test-results')
|
||||
const { optionalNonNegativeInteger } = require('../validators')
|
||||
const { InvalidResponse } = require('..')
|
||||
const JenkinsBase = require('./jenkins-base')
|
||||
const {
|
||||
buildTreeParamQueryString,
|
||||
buildUrl,
|
||||
queryParamSchema,
|
||||
} = require('./jenkins-common')
|
||||
const { InvalidResponse } = require('..')
|
||||
|
||||
// In the API response, the `actions` array can be empty, and when it is not empty it will contain a
|
||||
// mix of objects. Some will be empty objects, and several will not have the test count properties.
|
||||
|
||||
86
services/jetbrains/jetbrains-rating.service.js
Normal file
86
services/jetbrains/jetbrains-rating.service.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { starRating } = require('../text-formatters')
|
||||
const { colorScale } = require('../color-formatters')
|
||||
const JetbrainsBase = require('./jetbrains-base')
|
||||
|
||||
const pluginRatingColor = colorScale([2, 3, 4])
|
||||
|
||||
const schema = Joi.object({
|
||||
'plugin-repository': Joi.object({
|
||||
category: Joi.object({
|
||||
'idea-plugin': Joi.array()
|
||||
.min(1)
|
||||
.items(
|
||||
Joi.object({
|
||||
rating: Joi.string().required(),
|
||||
})
|
||||
)
|
||||
.single()
|
||||
.required(),
|
||||
}),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
module.exports = class JetbrainsRating extends JetbrainsBase {
|
||||
static get category() {
|
||||
return 'rating'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'jetbrains/plugin/r',
|
||||
pattern: ':format(rating|stars)/:pluginId',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'JetBrains IntelliJ Plugins',
|
||||
pattern: 'rating/:pluginId',
|
||||
namedParams: {
|
||||
pluginId: '11941-automatic-power-saver',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
rating: '4.5',
|
||||
format: 'rating',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'JetBrains IntelliJ Plugins',
|
||||
pattern: 'stars/:pluginId',
|
||||
namedParams: {
|
||||
pluginId: '11941-automatic-power-saver',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
rating: '4.5',
|
||||
format: 'stars',
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'rating' }
|
||||
}
|
||||
|
||||
static render({ rating, format }) {
|
||||
const message =
|
||||
format === 'rating'
|
||||
? `${+parseFloat(rating).toFixed(1)}/5`
|
||||
: starRating(rating)
|
||||
return {
|
||||
message,
|
||||
color: pluginRatingColor(rating),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ format, pluginId }) {
|
||||
const pluginData = await this.fetchPackageData({ pluginId, schema })
|
||||
const pluginRating =
|
||||
pluginData['plugin-repository'].category['idea-plugin'][0].rating
|
||||
return this.constructor.render({ rating: pluginRating, format })
|
||||
}
|
||||
}
|
||||
84
services/jetbrains/jetbrains-rating.tester.js
Normal file
84
services/jetbrains/jetbrains-rating.tester.js
Normal file
@@ -0,0 +1,84 @@
|
||||
'use strict'
|
||||
|
||||
const { withRegex, isStarRating } = require('../test-validators')
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
|
||||
const isRating = withRegex(/^(([0-4](.?([0-9]))?)|5)\/5$/)
|
||||
|
||||
t.create('rating number (user friendly plugin id)')
|
||||
.get('/rating/11941-automatic-power-saver.json')
|
||||
.expectBadge({ label: 'rating', message: isRating })
|
||||
|
||||
t.create('rating number (plugin id from plugin.xml)')
|
||||
.get('/rating/com.chriscarini.jetbrains.jetbrains-auto-power-saver.json')
|
||||
.expectBadge({ label: 'rating', message: isRating })
|
||||
|
||||
t.create('rating number (number as a plugin id)')
|
||||
.get('/rating/11941.json')
|
||||
.expectBadge({ label: 'rating', message: isRating })
|
||||
|
||||
t.create('rating number for unknown plugin')
|
||||
.get('/rating/unknown-plugin.json')
|
||||
.expectBadge({ label: 'rating', message: 'not found' })
|
||||
|
||||
t.create('rating stars (user friendly plugin id)')
|
||||
.get('/stars/11941-automatic-power-saver.json')
|
||||
.expectBadge({ label: 'rating', message: isStarRating })
|
||||
|
||||
t.create('rating stars (plugin id from plugin.xml)')
|
||||
.get('/stars/com.chriscarini.jetbrains.jetbrains-auto-power-saver.json')
|
||||
.expectBadge({ label: 'rating', message: isStarRating })
|
||||
|
||||
t.create('rating stars (number as a plugin id)')
|
||||
.get('/stars/11941.json')
|
||||
.expectBadge({ label: 'rating', message: isStarRating })
|
||||
|
||||
t.create('rating stars for unknown plugin')
|
||||
.get('/stars/unknown-plugin.json')
|
||||
.expectBadge({ label: 'rating', message: 'not found' })
|
||||
|
||||
t.create('rating number')
|
||||
.get('/rating/11941.json')
|
||||
.intercept(
|
||||
nock =>
|
||||
nock('https://plugins.jetbrains.com')
|
||||
.get('/plugins/list?pluginId=11941')
|
||||
.reply(
|
||||
200,
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plugin-repository>
|
||||
<category name="User Interface">
|
||||
<idea-plugin downloads="1714" size="208537" date="1586449109000" updatedDate="1586449109000" url="">
|
||||
<rating>4.5</rating>
|
||||
</idea-plugin>
|
||||
</category>
|
||||
</plugin-repository>`
|
||||
),
|
||||
{
|
||||
'Content-Type': 'text/xml;charset=UTF-8',
|
||||
}
|
||||
)
|
||||
.expectBadge({ label: 'rating', message: '4.5/5' })
|
||||
|
||||
t.create('rating stars')
|
||||
.get('/stars/11941.json')
|
||||
.intercept(
|
||||
nock =>
|
||||
nock('https://plugins.jetbrains.com')
|
||||
.get('/plugins/list?pluginId=11941')
|
||||
.reply(
|
||||
200,
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plugin-repository>
|
||||
<category name="User Interface">
|
||||
<idea-plugin downloads="1714" size="208537" date="1586449109000" updatedDate="1586449109000" url="">
|
||||
<rating>4.5</rating>
|
||||
</idea-plugin>
|
||||
</category>
|
||||
</plugin-repository>`
|
||||
),
|
||||
{
|
||||
'Content-Type': 'text/xml;charset=UTF-8',
|
||||
}
|
||||
)
|
||||
.expectBadge({ label: 'rating', message: '★★★★½' })
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { optionalUrl } = require('../validators')
|
||||
const { authConfig } = require('./jira-common')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { authConfig } = require('./jira-common')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
baseUrl: optionalUrl.required(),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { optionalUrl } = require('../validators')
|
||||
const { authConfig } = require('./jira-common')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { authConfig } = require('./jira-common')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
baseUrl: optionalUrl.required(),
|
||||
|
||||
@@ -1,83 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { downloadCount } = require('../color-formatters')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { deprecatedService } = require('..')
|
||||
|
||||
const schema = Joi.object({
|
||||
week: nonNegativeInteger,
|
||||
month: nonNegativeInteger,
|
||||
}).required()
|
||||
|
||||
const intervalMap = {
|
||||
dw: {
|
||||
api_field: 'week',
|
||||
suffix: '/week',
|
||||
module.exports = deprecatedService({
|
||||
route: {
|
||||
base: 'jitpack',
|
||||
pattern: ':interval(dw|dm)/:various*',
|
||||
},
|
||||
dm: {
|
||||
api_field: 'month',
|
||||
suffix: '/month',
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = class JitpackDownloads extends BaseJsonService {
|
||||
static get category() {
|
||||
return 'downloads'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'jitpack',
|
||||
pattern:
|
||||
':interval(dw|dm)/:vcs(github|bitbucket|gitlab|gitee)/:user/:repo',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'JitPack - Downloads',
|
||||
namedParams: {
|
||||
interval: 'dm',
|
||||
vcs: 'github',
|
||||
user: 'jitpack',
|
||||
repo: 'maven-simple',
|
||||
},
|
||||
staticPreview: JitpackDownloads.render({
|
||||
downloads: 14000,
|
||||
interval: 'dm',
|
||||
}),
|
||||
keywords: ['java', 'maven'],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'downloads' }
|
||||
}
|
||||
|
||||
static render({ downloads, interval }) {
|
||||
return {
|
||||
message: `${metric(downloads)}${intervalMap[interval].suffix}`,
|
||||
color: downloadCount(downloads),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ vcs, user, repo }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://jitpack.io/api/downloads/com.${vcs}.${user}/${repo}`,
|
||||
errorMessages: { 401: 'project not found or private' },
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ interval, vcs, user, repo }) {
|
||||
const json = await this.fetch({ vcs, user, repo })
|
||||
return this.constructor.render({
|
||||
downloads: json[intervalMap[interval].api_field],
|
||||
interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
label: 'jitpack',
|
||||
category: 'downloads',
|
||||
dateAdded: new Date('2020-04-05'),
|
||||
})
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
const { isMetricOverTimePeriod } = require('../test-validators')
|
||||
const { ServiceTester } = require('../tester')
|
||||
|
||||
t.create('weekly (github)')
|
||||
const t = (module.exports = new ServiceTester({
|
||||
id: 'JitPackDownloads',
|
||||
title: 'JitPackDownloads',
|
||||
pathPrefix: '/jitpack',
|
||||
}))
|
||||
|
||||
t.create('no longer available (dw)')
|
||||
.get('/dw/github/jitpack/maven-simple.json')
|
||||
.timeout(10000)
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('monthly (github)')
|
||||
.get('/dm/github/dcendents/android-maven-gradle-plugin.json')
|
||||
.timeout(10000)
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('unknown package (github)')
|
||||
.get('/dw/github/some-bogus-user/super-fake-project.json')
|
||||
.timeout(10000)
|
||||
.expectBadge({
|
||||
label: 'downloads',
|
||||
message: '0/week',
|
||||
label: 'jitpack',
|
||||
message: 'no longer available',
|
||||
})
|
||||
|
||||
t.create('no longer available (dm)')
|
||||
.get('/dm/github/jitpack/maven-simple.json')
|
||||
.expectBadge({
|
||||
label: 'jitpack',
|
||||
message: 'no longer available',
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
|
||||
|
||||
module.exports = class LiberapayGives extends LiberapayBase {
|
||||
static get route() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { colorScale } = require('../color-formatters')
|
||||
const { LiberapayBase } = require('./liberapay-base')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { LiberapayBase } = require('./liberapay-base')
|
||||
|
||||
module.exports = class LiberapayGoal extends LiberapayBase {
|
||||
static get route() {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const { expect } = require('chai')
|
||||
const { test, given } = require('sazerac')
|
||||
const LiberapayGoal = require('./liberapay-goal.service')
|
||||
const { InvalidResponse } = require('..')
|
||||
const LiberapayGoal = require('./liberapay-goal.service')
|
||||
|
||||
describe('LiberapayGoal', function() {
|
||||
test(LiberapayGoal.prototype.transform, () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
|
||||
const { InvalidResponse } = require('..')
|
||||
const { renderCurrencyBadge, LiberapayBase } = require('./liberapay-base')
|
||||
|
||||
module.exports = class LiberapayReceives extends LiberapayBase {
|
||||
static get route() {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { BaseJsonService } = require('..')
|
||||
const {
|
||||
transform,
|
||||
renderDependenciesBadge,
|
||||
} = require('./librariesio-dependencies-helpers')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const schema = Joi.object({
|
||||
dependencies: Joi.array()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { metric } = require('../text-formatters')
|
||||
const { fetchProject } = require('./librariesio-common')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { fetchProject } = require('./librariesio-common')
|
||||
|
||||
// https://libraries.io/api#project-dependent-repositories
|
||||
module.exports = class LibrariesIoDependentRepos extends BaseJsonService {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { metric } = require('../text-formatters')
|
||||
const { fetchProject } = require('./librariesio-common')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { fetchProject } = require('./librariesio-common')
|
||||
|
||||
// https://libraries.io/api#project-dependents
|
||||
module.exports = class LibrariesIoDependents extends BaseJsonService {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { colorScale } = require('../color-formatters')
|
||||
const { fetchProject } = require('./librariesio-common')
|
||||
const { BaseJsonService } = require('..')
|
||||
const { fetchProject } = require('./librariesio-common')
|
||||
|
||||
const sourceRankColor = colorScale([10, 15, 20, 25, 30])
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { addv } = require('../text-formatters')
|
||||
const { latestVersion } = require('./luarocks-version-helpers')
|
||||
const { BaseJsonService, NotFound } = require('..')
|
||||
const { latestVersion } = require('./luarocks-version-helpers')
|
||||
|
||||
const schema = Joi.object({
|
||||
repository: Joi.object()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const BaseMicrobadgerService = require('./microbadger-base')
|
||||
const { NotFound } = require('..')
|
||||
const BaseMicrobadgerService = require('./microbadger-base')
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
|
||||
@@ -7,8 +7,8 @@ const { optionalUrl } = require('../validators')
|
||||
const {
|
||||
optionalDottedVersionNClausesWithOptionalSuffix,
|
||||
} = require('../validators')
|
||||
const { isSnapshotVersion } = require('./nexus-version')
|
||||
const { BaseJsonService, InvalidResponse, NotFound } = require('..')
|
||||
const { isSnapshotVersion } = require('./nexus-version')
|
||||
|
||||
const nexus2SearchApiSchema = Joi.object({
|
||||
data: Joi.array()
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
const { expect } = require('chai')
|
||||
const nock = require('nock')
|
||||
const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
|
||||
const Nexus = require('./nexus.service')
|
||||
const { InvalidResponse, NotFound } = require('..')
|
||||
const Nexus = require('./nexus.service')
|
||||
|
||||
describe('Nexus', function() {
|
||||
context('transform2()', function() {
|
||||
|
||||
@@ -264,7 +264,7 @@ t.create('Nexus 3 - search snapshot version for artifact without snapshots')
|
||||
|
||||
t.create('Nexus 3 - repository version')
|
||||
.get(
|
||||
'/proxy-public-3rd-party-release/com.fasterxml.jackson.core/jackson-databind.json?server=https://nexus.pentaho.org&nexusVersion=3'
|
||||
'/proxy-public-3rd-party-release/com.h2database/h2.json?server=https://nexus.pentaho.org&nexusVersion=3'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nexus',
|
||||
@@ -276,7 +276,7 @@ t.create(
|
||||
)
|
||||
.timeout(15000)
|
||||
.get(
|
||||
'/proxy-public-3rd-party-release/com.fasterxml.jackson.core/jackson-databind.json?server=https://nexus.pentaho.org'
|
||||
'/proxy-public-3rd-party-release/com.h2database/h2.json?server=https://nexus.pentaho.org'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nexus',
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { renderVersionBadge } = require('../version')
|
||||
const NpmBase = require('./npm-base')
|
||||
const { NotFound } = require('..')
|
||||
const NpmBase = require('./npm-base')
|
||||
|
||||
const keywords = ['node']
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { BaseJsonService, BaseXmlService, NotFound, redirector } = require('..')
|
||||
const {
|
||||
renderVersionBadge,
|
||||
renderDownloadBadge,
|
||||
odataToObject,
|
||||
} = require('./nuget-helpers')
|
||||
const { BaseJsonService, BaseXmlService, NotFound, redirector } = require('..')
|
||||
|
||||
function createFilter({ packageName, includePrereleases }) {
|
||||
const releaseTypeFilter = includePrereleases
|
||||
|
||||
@@ -5,8 +5,8 @@ const Joi = require('@hapi/joi')
|
||||
const semver = require('semver')
|
||||
const { regularUpdate } = require('../../core/legacy/regular-update')
|
||||
const RouteBuilder = require('../route-builder')
|
||||
const { renderVersionBadge, renderDownloadBadge } = require('./nuget-helpers')
|
||||
const { BaseJsonService, NotFound } = require('..')
|
||||
const { renderVersionBadge, renderDownloadBadge } = require('./nuget-helpers')
|
||||
|
||||
/*
|
||||
* Build the Shields service URL object for the given service configuration. Return
|
||||
|
||||
61
services/offset-earth/offset-earth-carbon.service.js
Normal file
61
services/offset-earth/offset-earth-carbon.service.js
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { floorCount } = require('../color-formatters')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const apiSchema = Joi.object({
|
||||
total: Joi.number()
|
||||
.positive()
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
module.exports = class OffsetEarthCarbonOffset extends BaseJsonService {
|
||||
static get category() {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'offset-earth/carbon',
|
||||
pattern: ':username',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'Offset Earth (Carbon Offset)',
|
||||
namedParams: { username: 'offsetearth' },
|
||||
staticPreview: this.render({ count: 15.05 }),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'carbon offset' }
|
||||
}
|
||||
|
||||
static render({ count }) {
|
||||
const tonnes = metric(count)
|
||||
return { message: `${tonnes} tonnes`, color: floorCount(count, 0.5, 1, 5) }
|
||||
}
|
||||
|
||||
async fetch({ username }) {
|
||||
const url = `https://public.offset.earth/users/${username}/carbon-offset`
|
||||
return this._requestJson({
|
||||
url,
|
||||
schema: apiSchema,
|
||||
errorMessages: {
|
||||
404: 'username not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ username }) {
|
||||
const { total } = await this.fetch({ username })
|
||||
|
||||
return this.constructor.render({ count: total })
|
||||
}
|
||||
}
|
||||
19
services/offset-earth/offset-earth-carbon.tester.js
Normal file
19
services/offset-earth/offset-earth-carbon.tester.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
const { withRegex } = require('../test-validators')
|
||||
|
||||
t.create('request for existing username')
|
||||
.get('/offsetearth.json')
|
||||
.expectBadge({
|
||||
label: 'carbon offset',
|
||||
message: withRegex(/[\d.]+ tonnes/),
|
||||
})
|
||||
|
||||
t.create('invalid username')
|
||||
.get('/non-existent-username.json')
|
||||
.expectBadge({
|
||||
label: 'carbon offset',
|
||||
message: 'username not found',
|
||||
color: 'red',
|
||||
})
|
||||
59
services/offset-earth/offset-earth-trees.service.js
Normal file
59
services/offset-earth/offset-earth-trees.service.js
Normal file
@@ -0,0 +1,59 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { floorCount } = require('../color-formatters')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
const apiSchema = Joi.object({
|
||||
total: nonNegativeInteger,
|
||||
}).required()
|
||||
|
||||
module.exports = class OffsetEarthTrees extends BaseJsonService {
|
||||
static get category() {
|
||||
return 'other'
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'offset-earth/trees',
|
||||
pattern: ':username',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'Offset Earth (Trees)',
|
||||
namedParams: { username: 'offsetearth' },
|
||||
staticPreview: this.render({ count: 250 }),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'trees' }
|
||||
}
|
||||
|
||||
static render({ count }) {
|
||||
return { message: metric(count), color: floorCount(count, 10, 50, 100) }
|
||||
}
|
||||
|
||||
async fetch({ username }) {
|
||||
const url = `https://public.offset.earth/users/${username}/trees`
|
||||
return this._requestJson({
|
||||
url,
|
||||
schema: apiSchema,
|
||||
errorMessages: {
|
||||
404: 'username not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ username }) {
|
||||
const { total } = await this.fetch({ username })
|
||||
|
||||
return this.constructor.render({ count: total })
|
||||
}
|
||||
}
|
||||
28
services/offset-earth/offset-earth-trees.tester.js
Normal file
28
services/offset-earth/offset-earth-trees.tester.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict'
|
||||
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
const { isMetric } = require('../test-validators')
|
||||
|
||||
t.create('request for existing username')
|
||||
.get('/offsetearth.json')
|
||||
.expectBadge({
|
||||
label: 'trees',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('request for existing username')
|
||||
.get('/offsetearth.json')
|
||||
.intercept(nock =>
|
||||
nock('https://public.offset.earth')
|
||||
.get('/users/offsetearth/trees')
|
||||
.reply(200, { total: 50 })
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'trees',
|
||||
message: '50',
|
||||
color: 'green',
|
||||
})
|
||||
|
||||
t.create('invalid username')
|
||||
.get('/non-existent-username.json')
|
||||
.expectBadge({ label: 'trees', message: 'username not found', color: 'red' })
|
||||
@@ -3,12 +3,12 @@
|
||||
const Joi = require('@hapi/joi')
|
||||
const { renderLicenseBadge } = require('../licenses')
|
||||
const { optionalUrl } = require('../validators')
|
||||
const { NotFound } = require('..')
|
||||
const {
|
||||
keywords,
|
||||
BasePackagistService,
|
||||
customServerDocumentationFragment,
|
||||
} = require('./packagist-base')
|
||||
const { NotFound } = require('..')
|
||||
|
||||
const packageSchema = Joi.object()
|
||||
.pattern(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const PackagistLicense = require('./packagist-license.service')
|
||||
const { NotFound } = require('..')
|
||||
const PackagistLicense = require('./packagist-license.service')
|
||||
|
||||
describe('PackagistLicense', function() {
|
||||
it('should throw NotFound when default branch is missing', function() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user