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:
Caleb Cartwright
2019-03-07 23:11:41 -06:00
committed by GitHub
parent 3f4097b355
commit 6e84e9b317
5 changed files with 232 additions and 79 deletions

View 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'),
}),
]

View 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'
)

View File

@@ -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 })
}
}

View 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',
})
})
})

View File

@@ -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%',
})