Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor [Codecov] #3074

Merged
merged 16 commits into from
Mar 8, 2019
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions services/codecov/codecov-redirect.service.js
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions services/codecov/codecov-redirect.tester.js
Original file line number Diff line number Diff line change
@@ -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'
)
171 changes: 101 additions & 70 deletions services/codecov/codecov.service.js
Original file line number Diff line number Diff line change
@@ -1,108 +1,139 @@
'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,
}
}

static get examples() {
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 changes: 19 additions & 0 deletions services/codecov/codecov.spec.js
Original file line number Diff line number Diff line change
@@ -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',
})
})
})
54 changes: 45 additions & 9 deletions services/codecov/codecov.tester.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
'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',
})

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