Refactor [Codecov] (#3074)
* feat: started refactoring codecov * tests: removed erroneous test from draft PR * chore: prettified for prettier * feat: more codecov updates and tests * feat: more codecov refactor updates * feat: added codecov redirect content * refactor: removed legacy codecov service file * refactor(codecov): added redirect for legacy token route path * docs(codecov): added documentation to examples for token info * docs(codecov): updated vcs param in example patterns * refactor(codecov): update redirect service date Co-Authored-By: calebcartwright <calebcartwright@users.noreply.github.com> * refactor(codecov): various updates based on PR feedback * chore: added comment to codecov 401 test
This commit is contained in:
26
services/codecov/codecov-redirect.service.js
Normal file
26
services/codecov/codecov-redirect.service.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict'
|
||||
|
||||
const { redirector } = require('..')
|
||||
|
||||
const vcsSNameShortFormMap = {
|
||||
bb: 'bitbucket',
|
||||
gh: 'github',
|
||||
gl: 'gitlab',
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
redirector({
|
||||
category: 'coverage',
|
||||
route: {
|
||||
base: 'codecov/c',
|
||||
pattern:
|
||||
'token/:token/:vcsName(github|gh|bitbucket|bb|gl|gitlab)/:user/:repo/:branch*',
|
||||
},
|
||||
transformPath: ({ vcsName, user, repo, branch }) => {
|
||||
const vcs = vcsSNameShortFormMap[vcsName] || vcsName
|
||||
return `/codecov/c/${vcs}/${user}/${repo}${branch ? `/${branch}` : ''}`
|
||||
},
|
||||
transformQueryParams: ({ token }) => ({ token }),
|
||||
dateAdded: new Date('2019-03-04'),
|
||||
}),
|
||||
]
|
||||
39
services/codecov/codecov-redirect.tester.js
Normal file
39
services/codecov/codecov-redirect.tester.js
Normal file
@@ -0,0 +1,39 @@
|
||||
'use strict'
|
||||
|
||||
const { ServiceTester } = require('../tester')
|
||||
|
||||
const t = (module.exports = new ServiceTester({
|
||||
id: 'CodecovTokenRedirect',
|
||||
title: 'CodecovTokenRedirect',
|
||||
pathPrefix: '/codecov',
|
||||
}))
|
||||
|
||||
t.create('codecov token')
|
||||
.get('/c/token/abc123def456/gh/codecov/private-example.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
'/codecov/c/github/codecov/private-example.svg?token=abc123def456'
|
||||
)
|
||||
|
||||
t.create('codecov branch token')
|
||||
.get('/c/token/abc123def456/bb/private-shields/private-badges/master.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
'/codecov/c/bitbucket/private-shields/private-badges/master.svg?token=abc123def456'
|
||||
)
|
||||
|
||||
t.create('codecov gl short form expanded to long form')
|
||||
.get('/c/token/abc123def456/gl/private-shields/private-badges/master.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
'/codecov/c/gitlab/private-shields/private-badges/master.svg?token=abc123def456'
|
||||
)
|
||||
@@ -1,27 +1,67 @@
|
||||
'use strict'
|
||||
|
||||
const queryString = require('query-string')
|
||||
const LegacyService = require('../legacy-service')
|
||||
const { makeBadgeData: getBadgeData } = require('../../lib/badge-data')
|
||||
const {
|
||||
coveragePercentage: coveragePercentageColor,
|
||||
} = require('../color-formatters')
|
||||
const Joi = require('joi')
|
||||
const { coveragePercentage } = require('../color-formatters')
|
||||
const { BaseJsonService } = require('..')
|
||||
|
||||
// This legacy service should be rewritten to use e.g. BaseJsonService.
|
||||
//
|
||||
// Tips for rewriting:
|
||||
// https://github.com/badges/shields/blob/master/doc/rewriting-services.md
|
||||
//
|
||||
// Do not base new services on this code.
|
||||
module.exports = class Codecov extends LegacyService {
|
||||
// https://docs.codecov.io/reference#totals
|
||||
// A new repository that's been added but never had any coverage reports
|
||||
// uploaded will not have a `commit` object in the response and sometimes
|
||||
// the `totals` object can also be missing for the latest commit.
|
||||
// Accordingly the schema is a bit relaxed to support those scenarios
|
||||
// and then they are handled in the transform and render functions.
|
||||
const schema = Joi.object({
|
||||
commit: Joi.object({
|
||||
totals: Joi.object({
|
||||
c: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
token: Joi.string(),
|
||||
}).required()
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
You may specify a Codecov token to get coverage for a private repository.
|
||||
</p>
|
||||
<p>
|
||||
See the <a href="https://docs.codecov.io/reference#authorization">Codecov Docs</a>
|
||||
for more information about creating a token.
|
||||
</p>
|
||||
`
|
||||
|
||||
module.exports = class Codecov extends BaseJsonService {
|
||||
static get category() {
|
||||
return 'coverage'
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'coverage' }
|
||||
}
|
||||
|
||||
static render({ coverage }) {
|
||||
if (coverage === 'unknown') {
|
||||
return {
|
||||
message: coverage,
|
||||
color: 'lightgrey',
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: `${coverage.toFixed(0)}%`,
|
||||
color: coveragePercentage(coverage),
|
||||
}
|
||||
}
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'codecov/c',
|
||||
pattern: '',
|
||||
// https://docs.codecov.io/docs#section-common-questions
|
||||
// Github, BitBucket, and GitLab are the only supported options (long or short form)
|
||||
pattern:
|
||||
':vcsName(github|gh|bitbucket|bb|gl|gitlab)/:user/:repo/:branch*',
|
||||
queryParamSchema,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,80 +69,71 @@ module.exports = class Codecov extends LegacyService {
|
||||
return [
|
||||
{
|
||||
title: 'Codecov',
|
||||
pattern: ':vcsName/:user/:repo',
|
||||
pattern: ':vcsName(github|gh|bitbucket|bb|gl|gitlab)/:user/:repo',
|
||||
namedParams: {
|
||||
vcsName: 'github',
|
||||
user: 'codecov',
|
||||
repo: 'example-python',
|
||||
},
|
||||
staticPreview: { label: 'coverage', message: '90%', color: 'green' },
|
||||
queryParams: {
|
||||
token: 'abc123def456',
|
||||
},
|
||||
staticPreview: this.render({ coverage: 90 }),
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Codecov branch',
|
||||
pattern: ':vcsName/:user/:repo/:branch',
|
||||
pattern:
|
||||
':vcsName(github|gh|bitbucket|bb|gl|gitlab)/:user/:repo/:branch',
|
||||
namedParams: {
|
||||
vcsName: 'github',
|
||||
user: 'codecov',
|
||||
repo: 'example-python',
|
||||
branch: 'master',
|
||||
},
|
||||
staticPreview: { label: 'coverage', message: '90%', color: 'green' },
|
||||
},
|
||||
{
|
||||
title: 'Codecov private',
|
||||
pattern: 'token/:token/:vcsName/:user/:repo',
|
||||
namedParams: {
|
||||
token: 'My0A8VL917',
|
||||
vcsName: 'github',
|
||||
user: 'codecov',
|
||||
repo: 'example-python',
|
||||
queryParams: {
|
||||
token: 'abc123def456',
|
||||
},
|
||||
staticPreview: { label: 'coverage', message: '90%', color: 'green' },
|
||||
staticPreview: this.render({ coverage: 90 }),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static registerLegacyRouteHandler({ camp, cache }) {
|
||||
camp.route(
|
||||
/^\/codecov\/c\/(?:token\/(\w+))?[+/]?([^/]+\/[^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/,
|
||||
cache((data, match, sendBadge, request) => {
|
||||
const token = match[1]
|
||||
const userRepo = match[2] // eg, `github/codecov/example-python`.
|
||||
const branch = match[3]
|
||||
const format = match[4]
|
||||
let apiUrl
|
||||
if (branch) {
|
||||
apiUrl = `https://codecov.io/${userRepo}/branch/${branch}/graphs/badge.txt`
|
||||
} else {
|
||||
apiUrl = `https://codecov.io/${userRepo}/graphs/badge.txt`
|
||||
}
|
||||
if (token) {
|
||||
apiUrl += `?${queryString.stringify({ token })}`
|
||||
}
|
||||
const badgeData = getBadgeData('coverage', data)
|
||||
request(apiUrl, (err, res, body) => {
|
||||
if (err != null) {
|
||||
badgeData.text[1] = 'invalid'
|
||||
sendBadge(format, badgeData)
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Body: range(0, 100) or "unknown"
|
||||
const coverage = body.trim()
|
||||
if (Number.isNaN(+coverage)) {
|
||||
badgeData.text[1] = 'unknown'
|
||||
sendBadge(format, badgeData)
|
||||
return
|
||||
}
|
||||
badgeData.text[1] = `${coverage}%`
|
||||
badgeData.colorscheme = coveragePercentageColor(coverage)
|
||||
sendBadge(format, badgeData)
|
||||
} catch (e) {
|
||||
badgeData.text[1] = 'malformed'
|
||||
sendBadge(format, badgeData)
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
async fetch({ vcsName, user, repo, branch, token }) {
|
||||
// Codecov Docs: https://docs.codecov.io/reference#section-get-a-single-repository
|
||||
let url = `https://codecov.io/api/${vcsName}/${user}/${repo}`
|
||||
if (branch) {
|
||||
url += `/branches/${branch}`
|
||||
}
|
||||
const options = {}
|
||||
if (token) {
|
||||
options.headers = {
|
||||
Authorization: `token ${token}`,
|
||||
}
|
||||
}
|
||||
return this._requestJson({
|
||||
schema,
|
||||
options,
|
||||
url,
|
||||
errorMessages: {
|
||||
401: 'not authorized to access repository',
|
||||
404: 'repository not found',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
transform({ json }) {
|
||||
if (!json.commit || !json.commit.totals) {
|
||||
return { coverage: 'unknown' }
|
||||
}
|
||||
|
||||
return { coverage: +json.commit.totals.c }
|
||||
}
|
||||
|
||||
async handle({ vcsName, user, repo, branch }, { token }) {
|
||||
const json = await this.fetch({ vcsName, user, repo, branch, token })
|
||||
const { coverage } = this.transform({ json })
|
||||
return this.constructor.render({ coverage })
|
||||
}
|
||||
}
|
||||
|
||||
19
services/codecov/codecov.spec.js
Normal file
19
services/codecov/codecov.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
const { test, forCases, given } = require('sazerac')
|
||||
const Codecov = require('./codecov.service')
|
||||
|
||||
describe('Codecov', function() {
|
||||
test(Codecov.prototype.transform, () => {
|
||||
forCases([given({ json: {} }), given({ json: { commit: {} } })]).expect({
|
||||
coverage: 'unknown',
|
||||
})
|
||||
})
|
||||
|
||||
test(Codecov.render, () => {
|
||||
given({ coverage: 'unknown' }).expect({
|
||||
message: 'unknown',
|
||||
color: 'lightgrey',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,23 +1,61 @@
|
||||
'use strict'
|
||||
|
||||
const { ServiceTester } = require('../tester')
|
||||
const { isIntegerPercentage } = require('../test-validators')
|
||||
|
||||
const t = (module.exports = new ServiceTester({
|
||||
id: 'codecov',
|
||||
title: 'Codecov.io',
|
||||
}))
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
|
||||
t.create('gets coverage status')
|
||||
.get('/c/github/codecov/example-python.json')
|
||||
.get('/github/codecov/example-python.json')
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: isIntegerPercentage,
|
||||
})
|
||||
|
||||
t.create('gets coverate status for branch')
|
||||
.get('/c/github/codecov/example-python/master.json')
|
||||
t.create('gets coverage status for branch')
|
||||
.get('/github/codecov/example-python/master.json')
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: isIntegerPercentage,
|
||||
})
|
||||
|
||||
t.create('handles unknown repository')
|
||||
.get('/github/codecov2/fake-not-even-a-little-bit-real-python.json')
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: 'repository not found',
|
||||
})
|
||||
|
||||
// Using a mocked response here because we did not have a known
|
||||
// private repository hooked up with Codecov that we could use.
|
||||
t.create('handles unauthorized error')
|
||||
.get('/github/codecov/private-example-python.json')
|
||||
.intercept(nock =>
|
||||
nock('https://codecov.io/api')
|
||||
.get('/github/codecov/private-example-python')
|
||||
.reply(401)
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: 'not authorized to access repository',
|
||||
})
|
||||
|
||||
t.create('gets coverage for private repository')
|
||||
.get('/github/codecov/private-example-python.json?token=abc123def456')
|
||||
.intercept(nock =>
|
||||
nock('https://codecov.io/api', {
|
||||
reqheaders: {
|
||||
authorization: 'token abc123def456',
|
||||
},
|
||||
})
|
||||
.get('/github/codecov/private-example-python')
|
||||
.reply(200, {
|
||||
commit: {
|
||||
totals: {
|
||||
c: 94.75,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: '95%',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user