diff --git a/services/scrutinizer/scrutinizer-base.js b/services/scrutinizer/scrutinizer-base.js new file mode 100644 index 0000000000..ffff327d81 --- /dev/null +++ b/services/scrutinizer/scrutinizer-base.js @@ -0,0 +1,42 @@ +'use strict' + +const { BaseJsonService, NotFound } = require('..') + +module.exports = class ScrutinizerBase extends BaseJsonService { + // https://scrutinizer-ci.com/docs/api/#repository-details + async fetch({ schema, vcs, slug }) { + return this._requestJson({ + schema, + url: `https://scrutinizer-ci.com/api/repositories/${vcs}/${slug}`, + errorMessages: { + 401: 'not authorized to access project', + 404: 'project not found', + }, + }) + } + + transformBranchInfo({ json, wantedBranch }) { + if (!wantedBranch) { + return json.applications[json.default_branch] + } + + const branch = json.applications[wantedBranch] + if (!branch) { + throw new NotFound({ prettyMessage: ' branch not found' }) + } + + return branch + } + + transformBranchInfoMetricValue({ json, branch, metric }) { + const { + index: { + _embedded: { + project: { metric_values: metricValues }, + }, + }, + } = this.transformBranchInfo({ json, wantedBranch: branch }) + + return { value: metricValues[metric] } + } +} diff --git a/services/scrutinizer/scrutinizer-build.service.js b/services/scrutinizer/scrutinizer-build.service.js new file mode 100644 index 0000000000..50e04e2832 --- /dev/null +++ b/services/scrutinizer/scrutinizer-build.service.js @@ -0,0 +1,128 @@ +'use strict' + +const Joi = require('joi') +const { isBuildStatus, renderBuildStatusBadge } = require('../build-status') +const ScrutinizerBase = require('./scrutinizer-base') + +const schema = Joi.object({ + default_branch: Joi.string().required(), + applications: Joi.object() + .pattern( + /^/, + Joi.object({ + build_status: Joi.object({ + status: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')), + }).required(), + }) + ) + .required(), +}).required() + +class ScrutinizerBuildBase extends ScrutinizerBase { + static get category() { + return 'build' + } + + static get defaultBadgeData() { + return { + label: 'build', + } + } + + async makeBadge({ vcs, slug, branch }) { + const json = await this.fetch({ schema, vcs, slug }) + const { + build_status: { status }, + } = this.transformBranchInfo({ json, wantedBranch: branch }) + return renderBuildStatusBadge({ status }) + } +} + +class ScrutinizerBuild extends ScrutinizerBuildBase { + static get route() { + return { + base: 'scrutinizer/build', + pattern: ':vcs(g|b)/:user/:repo/:branch*', + } + } + + static get examples() { + return [ + { + title: 'Scrutinizer build (GitHub/Bitbucket)', + pattern: ':vcs(g|b)/:user/:repo/:branch?', + namedParams: { + vcs: 'g', + user: 'filp', + repo: 'whoops', + branch: 'master', + }, + staticPreview: renderBuildStatusBadge({ status: 'passing' }), + }, + ] + } + + async handle({ vcs, user, repo, branch }) { + return this.makeBadge({ + vcs, + slug: `${user}/${repo}`, + branch, + }) + } +} + +class ScrutinizerGitLabBuild extends ScrutinizerBuildBase { + static get route() { + return { + base: 'scrutinizer/build/gl', + pattern: ':instance/:user/:repo/:branch*', + } + } + + static get examples() { + // There are no known anonymous accessible Scrutinizer reports available for GitLab repos. + // The example used is valid, but the project will not be accessible if Shields users try to use it. + // https://gitlab.propertywindow.nl/propertywindow/client + // https://scrutinizer-ci.com/gl/propertywindow/propertywindow/client/badges/quality-score.png?b=master&s=dfae6992a48184cc2333b4c349cec0447f0d67c2 + return [ + { + title: 'Scrutinizer build (GitLab)', + pattern: ':instance/:user/:repo/:branch?', + namedParams: { + instance: 'propertywindow', + user: 'propertywindow', + repo: 'client', + branch: 'master', + }, + staticPreview: renderBuildStatusBadge({ status: 'passing' }), + }, + ] + } + + async handle({ instance, user, repo, branch }) { + return this.makeBadge({ + vcs: 'gl', + slug: `${instance}/${user}/${repo}`, + branch, + }) + } +} + +class ScrutinizerPlainGitBuild extends ScrutinizerBuildBase { + static get route() { + return { + base: 'scrutinizer/build/gp', + pattern: ':slug/:branch*', + } + } + + async handle({ slug, branch }) { + return this.makeBadge({ vcs: 'gp', slug, branch }) + } +} + +module.exports = [ + ScrutinizerBuild, + ScrutinizerGitLabBuild, + ScrutinizerPlainGitBuild, +] diff --git a/services/scrutinizer/scrutinizer-build.tester.js b/services/scrutinizer/scrutinizer-build.tester.js new file mode 100644 index 0000000000..aaa3a5bd96 --- /dev/null +++ b/services/scrutinizer/scrutinizer-build.tester.js @@ -0,0 +1,74 @@ +'use strict' + +const Joi = require('joi') +const { isBuildStatus } = require('../build-status') +const { ServiceTester } = require('../tester') +const t = (module.exports = new ServiceTester({ + id: 'ScrutinizerBuild', + title: 'ScrutinizerBuild', + pathPrefix: '/scrutinizer/build', +})) + +t.create('build (GitHub)') + .get('/g/filp/whoops.json') + .expectBadge({ + label: 'build', + message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')), + }) + +t.create('build (Bitbucket)') + .get('/b/atlassian/python-bitbucket.json') + .expectBadge({ + label: 'build', + message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')), + }) + +t.create('build (branch)') + .get('/g/phpmyadmin/phpmyadmin/master.json') + .expectBadge({ + label: 'build', + message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')), + }) + +t.create('build - unknown status') + .get('/g/filp/whoops.json') + .intercept(nock => + nock('https://scrutinizer-ci.com') + .get('/api/repositories/g/filp/whoops') + .reply(200, { + default_branch: 'master', + applications: { + master: { + build_status: { + status: 'unknown', + }, + }, + }, + }) + ) + .expectBadge({ + label: 'build', + message: 'unknown', + color: 'lightgrey', + }) + +t.create('build private project') + .get('/gl/propertywindow/propertywindow/client.json') + .expectBadge({ + label: 'build', + message: 'not authorized to access project', + }) + +t.create('build nonexistent project') + .get('/gp/foo.json') + .expectBadge({ + label: 'build', + message: 'project not found', + }) + +t.create('build nonexistent branch') + .get('/g/phpmyadmin/phpmyadmin/super-fake/not-real-branch.json') + .expectBadge({ + label: 'build', + message: 'branch not found', + }) diff --git a/services/scrutinizer/scrutinizer-coverage.service.js b/services/scrutinizer/scrutinizer-coverage.service.js new file mode 100644 index 0000000000..1af74c7c9e --- /dev/null +++ b/services/scrutinizer/scrutinizer-coverage.service.js @@ -0,0 +1,160 @@ +'use strict' + +const Joi = require('joi') +const { NotFound } = require('..') +const { colorScale } = require('../color-formatters') +const ScrutinizerBase = require('./scrutinizer-base') + +const schema = Joi.object({ + default_branch: Joi.string().required(), + applications: Joi.object() + .pattern( + /^/, + Joi.object({ + index: Joi.object({ + _embedded: Joi.object({ + project: Joi.object({ + metric_values: Joi.object({ + 'scrutinizer.test_coverage': Joi.number().positive(), + }).required(), + }).required(), + }).required(), + }).required(), + }) + ) + .required(), +}).required() + +// https://scrutinizer-ci.com/g/filp/whoops/code-structure/master/code-coverage +// < 40% - red +// 40-60% (inclusive) - yellow +// > 60% brightgreen +const scale = colorScale([40, 61], ['red', 'yellow', 'brightgreen']) + +class ScrutinizerCoverageBase extends ScrutinizerBase { + static get category() { + return 'coverage' + } + + static get defaultBadgeData() { + return { + label: 'coverage', + } + } + + static render({ coverage }) { + return { + message: `${coverage.toFixed(0)}%`, + color: scale(coverage), + } + } + + transform({ json, branch }) { + const { value: rawCoverage } = this.transformBranchInfoMetricValue({ + json, + branch, + metric: 'scrutinizer.test_coverage', + }) + + if (!rawCoverage) { + throw new NotFound({ prettyMessage: 'coverage not found' }) + } + + return { coverage: rawCoverage * 100 } + } + + async makeBadge({ vcs, slug, branch }) { + const json = await this.fetch({ schema, vcs, slug }) + const { coverage } = this.transform({ json, branch }) + return this.constructor.render({ coverage }) + } +} + +class ScrutinizerCoverage extends ScrutinizerCoverageBase { + static get route() { + return { + base: 'scrutinizer/coverage', + pattern: ':vcs(g|b)/:user/:repo/:branch*', + } + } + + static get examples() { + return [ + { + title: 'Scrutinizer coverage (GitHub/BitBucket)', + pattern: ':vcs(g|b)/:user/:repo/:branch?', + namedParams: { + vcs: 'g', + user: 'filp', + repo: 'whoops', + branch: 'master', + }, + staticPreview: this.render({ coverage: 86 }), + }, + ] + } + + async handle({ vcs, user, repo, branch }) { + return this.makeBadge({ + vcs, + slug: `${user}/${repo}`, + branch, + }) + } +} + +class ScrutinizerCoverageGitLab extends ScrutinizerCoverageBase { + static get route() { + return { + base: 'scrutinizer/coverage/gl', + pattern: ':instance/:user/:repo/:branch*', + } + } + + static get examples() { + // There are no known anonymous accessible Scrutinizer reports available for GitLab repos. + // The example used is valid, but the project will not be accessible if Shields users try to use it. + // https://gitlab.propertywindow.nl/propertywindow/client + // https://scrutinizer-ci.com/gl/propertywindow/propertywindow/client/badges/quality-score.png?b=master&s=dfae6992a48184cc2333b4c349cec0447f0d67c2 + return [ + { + title: 'Scrutinizer coverage (GitLab)', + pattern: ':instance/:user/:repo/:branch?', + namedParams: { + instance: 'propertywindow', + user: 'propertywindow', + repo: 'client', + branch: 'master', + }, + staticPreview: this.render({ coverage: 94 }), + }, + ] + } + + async handle({ instance, user, repo, branch }) { + return this.makeBadge({ + vcs: 'gl', + slug: `${instance}/${user}/${repo}`, + branch, + }) + } +} + +class ScrutinizerCoveragePlainGit extends ScrutinizerCoverageBase { + static get route() { + return { + base: 'scrutinizer/coverage/gp', + pattern: ':slug/:branch*', + } + } + + async handle({ slug, branch }) { + return this.makeBadge({ vcs: 'gp', slug, branch }) + } +} + +module.exports = [ + ScrutinizerCoverage, + ScrutinizerCoverageGitLab, + ScrutinizerCoveragePlainGit, +] diff --git a/services/scrutinizer/scrutinizer-coverage.spec.js b/services/scrutinizer/scrutinizer-coverage.spec.js new file mode 100644 index 0000000000..b8361be325 --- /dev/null +++ b/services/scrutinizer/scrutinizer-coverage.spec.js @@ -0,0 +1,54 @@ +'use strict' + +const { expect } = require('chai') +const { test, given } = require('sazerac') +const { NotFound } = require('..') +const [ScrutinizerCoverage] = require('./scrutinizer-coverage.service') + +describe('ScrutinizerCoverage', function() { + test(ScrutinizerCoverage.render, () => { + given({ coverage: 39 }).expect({ + message: '39%', + color: 'red', + }) + given({ coverage: 40 }).expect({ + message: '40%', + color: 'yellow', + }) + given({ coverage: 60 }).expect({ + message: '60%', + color: 'yellow', + }) + given({ coverage: 61 }).expect({ + message: '61%', + color: 'brightgreen', + }) + }) + + context('transform()', function() { + it('throws NotFound error when there is no coverage data', function() { + try { + ScrutinizerCoverage.prototype.transform({ + branch: 'master', + json: { + applications: { + master: { + index: { + _embedded: { + project: { + metric_values: {}, + }, + }, + }, + }, + }, + }, + }) + expect.fail('Expected to throw') + } catch (e) { + expect(e).to.be.an.instanceof(NotFound) + expect(e.prettyMessage).to.equal('coverage not found') + } + }) + }) +}) diff --git a/services/scrutinizer/scrutinizer-coverage.tester.js b/services/scrutinizer/scrutinizer-coverage.tester.js new file mode 100644 index 0000000000..bfe306b34d --- /dev/null +++ b/services/scrutinizer/scrutinizer-coverage.tester.js @@ -0,0 +1,44 @@ +'use strict' + +const { isIntegerPercentage } = require('../test-validators') +const { ServiceTester } = require('../tester') +const t = (module.exports = new ServiceTester({ + id: 'ScrutinizerCoverage', + title: 'ScrutinizerCoverage', + pathPrefix: '/scrutinizer/coverage', +})) + +t.create('code coverage (GitHub)') + .get('/g/filp/whoops.json') + .expectBadge({ + label: 'coverage', + message: isIntegerPercentage, + }) + +t.create('code coverage branch (GitHub)') + .get('/g/PHPMailer/PHPMailer/master.json') + .expectBadge({ + label: 'coverage', + message: isIntegerPercentage, + }) + +t.create('code coverage (Bitbucket)') + .get('/b/atlassian/python-bitbucket.json') + .expectBadge({ + label: 'coverage', + message: isIntegerPercentage, + }) + +t.create('code coverage private project') + .get('/gl/propertywindow/propertywindow/client.json') + .expectBadge({ + label: 'coverage', + message: 'not authorized to access project', + }) + +t.create('code coverage nonexistent project') + .get('/gp/foo.json') + .expectBadge({ + label: 'coverage', + message: 'project not found', + }) diff --git a/services/scrutinizer/scrutinizer-quality.service.js b/services/scrutinizer/scrutinizer-quality.service.js new file mode 100644 index 0000000000..50b6e32751 --- /dev/null +++ b/services/scrutinizer/scrutinizer-quality.service.js @@ -0,0 +1,148 @@ +'use strict' + +const Joi = require('joi') +const { colorScale } = require('../color-formatters') +const ScrutinizerBase = require('./scrutinizer-base') + +const schema = Joi.object({ + default_branch: Joi.string().required(), + applications: Joi.object() + .pattern( + /^/, + Joi.object({ + index: Joi.object({ + _embedded: Joi.object({ + project: Joi.object({ + metric_values: Joi.object({ + 'scrutinizer.quality': Joi.number().positive(), + }).required(), + }).required(), + }).required(), + }).required(), + }) + ) + .required(), +}).required() + +const scale = colorScale( + [4, 5, 7, 9], + ['red', 'orange', 'yellow', 'green', 'brightgreen'] +) + +class ScrutinizerQualityBase extends ScrutinizerBase { + static get category() { + return 'analysis' + } + + static get defaultBadgeData() { + return { + label: 'code quality', + } + } + + static render({ score }) { + return { + message: `${Math.round(score * 100) / 100}`, + color: scale(score), + } + } + + async makeBadge({ vcs, slug, branch }) { + const json = await this.fetch({ schema, vcs, slug }) + const { value: score } = this.transformBranchInfoMetricValue({ + json, + branch, + metric: 'scrutinizer.quality', + }) + return this.constructor.render({ score }) + } +} + +class ScrutinizerQuality extends ScrutinizerQualityBase { + static get route() { + return { + base: 'scrutinizer/quality', + pattern: ':vcs(g|b)/:user/:repo/:branch*', + } + } + + static get examples() { + return [ + { + title: 'Scrutinizer code quality (GitHub/Bitbucket)', + pattern: ':vcs(g|b)/:user/:repo/:branch?', + namedParams: { + vcs: 'g', + user: 'filp', + repo: 'whoops', + branch: 'master', + }, + staticPreview: this.render({ score: 8.26 }), + }, + ] + } + + async handle({ vcs, user, repo, branch }) { + return this.makeBadge({ + vcs, + slug: `${user}/${repo}`, + branch, + }) + } +} + +class ScrutinizerQualityGitLab extends ScrutinizerQualityBase { + static get route() { + return { + base: 'scrutinizer/quality/gl', + pattern: ':instance/:user/:repo/:branch*', + } + } + + static get examples() { + // There are no known anonymous accessible Scrutinizer reports available for GitLab repos. + // The example used is valid, but the project will not be accessible if Shields users try to use it. + // https://gitlab.propertywindow.nl/propertywindow/client + // https://scrutinizer-ci.com/gl/propertywindow/propertywindow/client/badges/quality-score.png?b=master&s=dfae6992a48184cc2333b4c349cec0447f0d67c2 + return [ + { + title: 'Scrutinizer coverage (GitLab)', + pattern: ':instance/:user/:repo/:branch?', + namedParams: { + instance: 'propertywindow', + user: 'propertywindow', + repo: 'client', + branch: 'master', + }, + staticPreview: this.render({ score: 10.0 }), + }, + ] + } + + async handle({ instance, user, repo, branch }) { + return this.makeBadge({ + vcs: 'gl', + slug: `${instance}/${user}/${repo}`, + branch, + }) + } +} + +class ScrutinizerQualityPlainGit extends ScrutinizerQualityBase { + static get route() { + return { + base: 'scrutinizer/quality/gp', + pattern: ':slug/:branch*', + } + } + + async handle({ slug, branch }) { + return this.makeBadge({ vcs: 'gp', slug, branch }) + } +} + +module.exports = [ + ScrutinizerQuality, + ScrutinizerQualityGitLab, + ScrutinizerQualityPlainGit, +] diff --git a/services/scrutinizer/scrutinizer-quality.tester.js b/services/scrutinizer/scrutinizer-quality.tester.js new file mode 100644 index 0000000000..48ce6d198c --- /dev/null +++ b/services/scrutinizer/scrutinizer-quality.tester.js @@ -0,0 +1,46 @@ +'use strict' + +const Joi = require('joi') +const { ServiceTester } = require('../tester') +const t = (module.exports = new ServiceTester({ + id: 'ScrutinizerQuality', + title: 'ScrutinizerQuality', + pathPrefix: '/scrutinizer/quality', +})) + +const isQualityNumber = Joi.number().positive() + +t.create('code quality (GitHub)') + .get('/g/filp/whoops.json') + .expectBadge({ + label: 'code quality', + message: isQualityNumber, + }) + +t.create('code quality branch (GitHub)') + .get('/g/PHPMailer/PHPMailer/master.json') + .expectBadge({ + label: 'code quality', + message: isQualityNumber, + }) + +t.create('code quality (Bitbucket)') + .get('/b/atlassian/python-bitbucket.json') + .expectBadge({ + label: 'code quality', + message: isQualityNumber, + }) + +t.create('code quality private project') + .get('/gl/propertywindow/propertywindow/client.json') + .expectBadge({ + label: 'code quality', + message: 'not authorized to access project', + }) + +t.create('code quality nonexistent project') + .get('/gp/foo.json') + .expectBadge({ + label: 'code quality', + message: 'project not found', + }) diff --git a/services/scrutinizer/scrutinizer-redirect.service.js b/services/scrutinizer/scrutinizer-redirect.service.js new file mode 100644 index 0000000000..4aee6a5ebf --- /dev/null +++ b/services/scrutinizer/scrutinizer-redirect.service.js @@ -0,0 +1,42 @@ +'use strict' + +const { redirector } = require('..') + +const commonAttrs = { + category: 'analysis', + dateAdded: new Date('2019-04-24'), +} + +module.exports = [ + redirector({ + route: { + base: 'scrutinizer', + pattern: ':vcs(g|b)/:user/:repo/:branch*', + }, + transformPath: ({ vcs, user, repo, branch }) => + `/scrutinizer/quality/${vcs}/${user}/${repo}${ + branch ? `/${branch}` : '' + }`, + ...commonAttrs, + }), + redirector({ + route: { + base: 'scrutinizer/gl', + pattern: ':instance/:user/:repo/:branch*', + }, + transformPath: ({ instance, user, repo, branch }) => + `/scrutinizer/quality/gl/${instance}/${user}/${repo}${ + branch ? `/${branch}` : '' + }`, + ...commonAttrs, + }), + redirector({ + route: { + base: 'scrutinizer/gp', + pattern: ':slug/:branch*', + }, + transformPath: ({ slug, branch }) => + `/scrutinizer/quality/gp/${slug}${branch ? `/${branch}` : ''}`, + ...commonAttrs, + }), +] diff --git a/services/scrutinizer/scrutinizer-redirect.tester.js b/services/scrutinizer/scrutinizer-redirect.tester.js new file mode 100644 index 0000000000..28c5c5b892 --- /dev/null +++ b/services/scrutinizer/scrutinizer-redirect.tester.js @@ -0,0 +1,71 @@ +'use strict' + +const { ServiceTester } = require('../tester') + +const t = (module.exports = new ServiceTester({ + id: 'ScrutinizerQualityRedirect', + title: 'ScrutinizerQualityRedirect', + pathPrefix: '/scrutinizer', +})) + +t.create('scrutinizer quality GitHub') + .get('/g/doctrine/orm.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader('Location', '/scrutinizer/quality/g/doctrine/orm.svg') + +t.create('scrutinizer quality GitHub (branch)') + .get('/g/doctrine/orm/develop.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader('Location', '/scrutinizer/quality/g/doctrine/orm/develop.svg') + +t.create('scrutinizer quality Bitbucket') + .get('/b/doctrine/orm.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader('Location', '/scrutinizer/quality/b/doctrine/orm.svg') + +t.create('scrutinizer quality Bitbucket (branch)') + .get('/b/atlassian/python-bitbucket/develop.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader( + 'Location', + '/scrutinizer/quality/b/atlassian/python-bitbucket/develop.svg' + ) + +t.create('scrutinizer quality GitLab') + .get('/gl/gitlab-com/foo/bar.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader('Location', '/scrutinizer/quality/gl/gitlab-com/foo/bar.svg') + +t.create('scrutinizer quality GitLab (branch)') + .get('/gl/gitlab-com/foo/bar/develop.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader( + 'Location', + '/scrutinizer/quality/gl/gitlab-com/foo/bar/develop.svg' + ) + +t.create('scrutinizer quality Plain Git') + .get('/gp/bar.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader('Location', '/scrutinizer/quality/gp/bar.svg') + +t.create('scrutinizer quality Plain Git (branch)') + .get('/gp/bar/develop.svg', { + followRedirect: false, + }) + .expectStatus(301) + .expectHeader('Location', '/scrutinizer/quality/gp/bar/develop.svg') diff --git a/services/scrutinizer/scrutinizer.service.js b/services/scrutinizer/scrutinizer.service.js deleted file mode 100644 index 9b433d737c..0000000000 --- a/services/scrutinizer/scrutinizer.service.js +++ /dev/null @@ -1,208 +0,0 @@ -'use strict' - -const LegacyService = require('../legacy-service') -const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') -const { checkErrorResponse } = require('../../lib/error-helper') -const { - coveragePercentage: coveragePercentageColor, -} = require('../color-formatters') - -class ScrutinizerBuild extends LegacyService { - static get category() { - return 'build' - } - - static get route() { - return { - base: 'scrutinizer/build', - pattern: ':vcsType/:user/:repo', - } - } - - static get examples() { - return [ - { - title: 'Scrutinizer build', - namedParams: { - vcsType: 'g', - user: 'filp', - repo: 'whoops', - }, - staticPreview: { - label: 'build', - message: 'passing', - color: 'brightgreen', - }, - }, - ] - } - - static registerLegacyRouteHandler({ camp, cache }) {} -} - -class ScrutinizerCoverage extends LegacyService { - static get category() { - return 'coverage' - } - - static get route() { - return { - base: 'scrutinizer/coverage', - pattern: ':vcsType/:user/:repo/:branch*', - } - } - - static get examples() { - return [ - { - title: 'Scrutinizer coverage', - pattern: ':vcsType/:user/:repo', - namedParams: { - vcsType: 'g', - user: 'filp', - repo: 'whoops', - }, - staticPreview: { - label: 'coverage', - message: '56%', - color: 'yellow', - }, - }, - { - title: 'Scrutinizer coverage (branch)', - pattern: ':vcsType/:user/:repo/:branch', - namedParams: { - vcsType: 'g', - user: 'doctrine', - repo: 'orm', - branch: 'master', - }, - staticPreview: { - label: 'coverage', - message: '73%', - color: 'yellow', - }, - }, - ] - } - - static registerLegacyRouteHandler({ camp, cache }) {} -} - -// 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. -class Scrutinizer extends LegacyService { - static get category() { - return 'analysis' - } - - static get route() { - return { - base: 'scrutinizer', - pattern: ':vcsType/:user/:repo', - } - } - - static get examples() { - return [ - { - title: 'Scrutinizer code quality', - namedParams: { - vcsType: 'g', - user: 'filp', - repo: 'whoops', - }, - staticPreview: { - label: 'code quality', - message: '8.26', - color: 'green', - }, - }, - ] - } - - static registerLegacyRouteHandler({ camp, cache }) { - camp.route( - /^\/scrutinizer(?:\/(build|coverage))?\/([^/]+\/[^/]+\/[^/]+|gp\/[^/])(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, - cache((data, match, sendBadge, request) => { - const type = match[1] ? match[1] : 'code quality' - const repo = match[2] // eg, g/phpmyadmin/phpmyadmin - let branch = match[3] - const format = match[4] - const apiUrl = `https://scrutinizer-ci.com/api/repositories/${repo}` - const badgeData = getBadgeData(type, data) - request(apiUrl, {}, (err, res, buffer) => { - if ( - checkErrorResponse(badgeData, err, res, { - 404: 'project or branch not found', - }) - ) { - sendBadge(format, badgeData) - return - } - try { - const parsedData = JSON.parse(buffer) - // Which branch are we dealing with? - if (branch === undefined) { - branch = parsedData.default_branch - } - if (type === 'coverage') { - const percentage = - parsedData.applications[branch].index._embedded.project - .metric_values['scrutinizer.test_coverage'] * 100 - if (isNaN(percentage)) { - badgeData.text[1] = 'unknown' - badgeData.colorscheme = 'gray' - } else { - badgeData.text[1] = `${percentage.toFixed(0)}%` - badgeData.colorscheme = coveragePercentageColor(percentage) - } - } else if (type === 'build') { - const status = parsedData.applications[branch].build_status.status - badgeData.text[1] = status - if (status === 'passed') { - badgeData.colorscheme = 'brightgreen' - badgeData.text[1] = 'passing' - } else if (status === 'failed' || status === 'error') { - badgeData.colorscheme = 'red' - } else if (status === 'pending') { - badgeData.colorscheme = 'orange' - } - } else { - let score = - parsedData.applications[branch].index._embedded.project - .metric_values['scrutinizer.quality'] - score = Math.round(score * 100) / 100 - badgeData.text[1] = score - if (score > 9) { - badgeData.colorscheme = 'brightgreen' - } else if (score > 7) { - badgeData.colorscheme = 'green' - } else if (score > 5) { - badgeData.colorscheme = 'yellow' - } else if (score > 4) { - badgeData.colorscheme = 'orange' - } else { - badgeData.colorscheme = 'red' - } - } - sendBadge(format, badgeData) - } catch (e) { - badgeData.text[1] = 'invalid' - sendBadge(format, badgeData) - } - }) - }) - ) - } -} - -module.exports = { - ScrutinizerBuild, - ScrutinizerCoverage, - Scrutinizer, -} diff --git a/services/scrutinizer/scrutinizer.tester.js b/services/scrutinizer/scrutinizer.tester.js deleted file mode 100644 index 8dff6ccda7..0000000000 --- a/services/scrutinizer/scrutinizer.tester.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' - -const Joi = require('joi') -const { isBuildStatus } = require('../build-status') -const { ServiceTester } = require('../tester') -const { isIntegerPercentage } = require('../test-validators') - -const t = (module.exports = new ServiceTester({ - id: 'scrutinizer', - title: 'Scrutinizer', -})) - -t.create('code quality') - .get('/g/filp/whoops.json') - .expectBadge({ - label: 'code quality', - message: Joi.number().positive(), - }) - -t.create('code quality (branch)') - .get('/g/phpmyadmin/phpmyadmin/master.json') - .expectBadge({ - label: 'code quality', - message: Joi.number().positive(), - }) - -t.create('code coverage') - .get('/coverage/g/filp/whoops.json') - .expectBadge({ - label: 'coverage', - message: isIntegerPercentage, - }) - -t.create('code coverage (branch)') - .get('/coverage/g/PHPMailer/PHPMailer/master.json') - .expectBadge({ - label: 'coverage', - message: isIntegerPercentage, - }) - -t.create('build') - .get('/build/g/filp/whoops.json') - .expectBadge({ - label: 'build', - message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')), - }) - -t.create('build (branch)') - .get('/build/g/phpmyadmin/phpmyadmin/master.json') - .expectBadge({ - label: 'build', - message: Joi.alternatives().try(isBuildStatus, Joi.equal('unknown')), - }) - -t.create('project not found') - .get('/build/g/does-not-exist/does-not-exist.json') - .expectBadge({ - label: 'build', - message: 'project or branch not found', - }) - -t.create('code coverage unknown') - .get('/coverage/g/phpmyadmin/phpmyadmin/master.json') - .expectBadge({ - label: 'coverage', - message: 'unknown', - }) - -t.create('unexpected response data') - .get('/coverage/g/filp/whoops.json') - .intercept(nock => - nock('https://scrutinizer-ci.com') - .get('/api/repositories/g/filp/whoops') - .reply(200, '{"unexpected":"data"}') - ) - .expectBadge({ - label: 'coverage', - message: 'invalid', - }) - -t.create('build - unknown') - .get('/build/g/filp/whoops.json') - .intercept(nock => - nock('https://scrutinizer-ci.com') - .get('/api/repositories/g/filp/whoops') - .reply(200, { - default_branch: 'master', - applications: { - master: { - build_status: { - status: 'unknown', - }, - }, - }, - }) - ) - .expectBadge({ - label: 'build', - message: 'unknown', - color: 'lightgrey', - })