From 33579dace304d949c87d9d45779aef3a45aa03d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Se=C3=A7kin?= Date: Tue, 27 Nov 2018 11:27:17 +0000 Subject: [PATCH 1/4] Add Azure DevOps tests service Added a test results badge service for an Azure Pipelines build using the ResultSummaryByBuild endpoint. Added basic unit tests for the service. Completes #2411 --- .../azure-devops-tests.service.js | 284 ++++++++++++++++++ .../azure-devops/azure-devops-tests.tester.js | 159 ++++++++++ 2 files changed, 443 insertions(+) create mode 100644 services/azure-devops/azure-devops-tests.service.js create mode 100644 services/azure-devops/azure-devops-tests.tester.js diff --git a/services/azure-devops/azure-devops-tests.service.js b/services/azure-devops/azure-devops-tests.service.js new file mode 100644 index 0000000000000..5d20fa90632b8 --- /dev/null +++ b/services/azure-devops/azure-devops-tests.service.js @@ -0,0 +1,284 @@ +'use strict' + +const Joi = require('joi') +const BaseJsonService = require('../base-json') +const { NotFound } = require('../errors') +const { getHeaders } = require('./azure-devops-helpers') +const { renderTestResultBadge } = require('../../lib/text-formatters') + +const documentation = ` +

+ To obtain your own badge, you need to get 3 pieces of information: + ORGANIZATION, PROJECT and DEFINITION_ID. +

+

+ First, you need to select your build definition and look at the url: +

+ORGANIZATION is after the dev.azure.com part, PROJECT is right after that, DEFINITION_ID is at the end after the id= part. +

+ Your badge will then have the form: + https://img.shields.io/azure-devops/tests/ORGANIZATION/PROJECT/DEFINITION_ID.svg. +

+

+ Optionally, you can specify a named branch: + https://img.shields.io/azure-devops/tests/ORGANIZATION/PROJECT/DEFINITION_ID/NAMED_BRANCH.svg. +

+

+ You may change the "passed", "failed" and "skipped" text on this badge by supplying query parameters &passed_label=, &failed_label= and &skipped_label= respectively. +
+ There is also a &compact_message query parameter, which will default to displaying ✔, ✘ and ➟, separated by a horizontal bar |. +
+ For example, if you want to use a different terminology: +
+ /azure-devops/tests/ORGANIZATION/PROJECT/DEFINITION_ID.svg?passed_label=good&failed_label=bad&skipped_label=n%2Fa +
+ Or, use symbols: +
+ /azure-devops/tests/ORGANIZATION/PROJECT/DEFINITION_ID.svg?compact_message&passed_label=%F0%9F%8E%89&failed_label=%F0%9F%92%A2&skipped_label=%F0%9F%A4%B7 +

+` + +const latestBuildSchema = Joi.object({ + count: Joi.number().required(), + value: Joi.array() + .items( + Joi.object({ + id: Joi.number().required(), + }) + ) + .required(), +}).required() + +const buildTestResultSummarySchema = Joi.object({ + aggregatedResultsAnalysis: Joi.object({ + totalTests: Joi.number().required(), + resultsByOutcome: Joi.object({ + Passed: Joi.object({ + count: Joi.number().required(), + }).optional(), + Failed: Joi.object({ + count: Joi.number().required(), + }).optional(), + Skipped: Joi.object({ + count: Joi.number().required(), + }).optional(), + }).required(), + }).required(), +}).required() + +module.exports = class AzureDevOpsTests extends BaseJsonService { + static render({ + passed, + failed, + skipped, + total, + passedLabel, + failedLabel, + skippedLabel, + isCompact, + }) { + return renderTestResultBadge({ + passed, + failed, + skipped, + total, + passedLabel, + failedLabel, + skippedLabel, + isCompact, + }) + } + + static get defaultBadgeData() { + return { label: 'tests' } + } + + static get category() { + return 'build' + } + + static get examples() { + return [ + { + title: 'Azure DevOps tests', + pattern: ':organization/:project/:definitionId', + exampleUrl: 'azuredevops-powershell/azuredevops-powershell/1', + staticExample: this.render({ + passed: 20, + failed: 1, + skipped: 1, + total: 22, + }), + keywords: ['vso', 'vsts', 'azure-devops'], + documentation, + }, + { + title: 'Azure DevOps tests (branch)', + pattern: ':organization/:project/:definitionId/:branch', + exampleUrl: 'azuredevops-powershell/azuredevops-powershell/1/master', + staticExample: this.render({ + passed: 20, + failed: 1, + skipped: 1, + total: 22, + }), + keywords: ['vso', 'vsts', 'azure-devops'], + documentation, + }, + { + title: 'Azure DevOps tests (compact)', + pattern: ':organization/:project/:definitionId', + exampleUrl: 'azuredevops-powershell/azuredevops-powershell/1', + query: { + compact_message: null, + }, + keywords: ['vso', 'vsts', 'azure-devops'], + staticExample: this.render({ + passed: 20, + failed: 1, + skipped: 1, + total: 22, + isCompact: true, + }), + documentation, + }, + { + title: 'Azure DevOps tests with custom labels', + pattern: ':organization/:project/:definitionId', + exampleUrl: 'azuredevops-powershell/azuredevops-powershell/1', + keywords: ['vso', 'vsts', 'azure-devops'], + query: { + passed_label: 'good', + failed_label: 'bad', + skipped_label: 'n/a', + }, + staticExample: this.render({ + passed: 20, + failed: 1, + skipped: 1, + total: 22, + passedLabel: 'good', + failedLabel: 'bad', + skippedLabel: 'n/a', + }), + documentation, + }, + ] + } + + static get route() { + return { + base: 'azure-devops/tests', + format: '([^/]+)/([^/]+)/([^/]+)(?:/(.+))?', + capture: ['organization', 'project', 'definitionId', 'branch'], + queryParams: [ + 'compact_message', + 'passed_label', + 'failed_label', + 'skipped_label', + ], + } + } + + async fetch({ url, options, schema }) { + return this._requestJson({ + schema, + url, + options, + errorMessages: { + 404: 'build pipeline or test result summary not found', + }, + }) + } + + async getLatestBuildId(organization, project, definitionId, branch, headers) { + // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.0 + const url = `https://dev.azure.com/${organization}/${project}/_apis/build/builds` + const options = { + qs: { + definitions: definitionId, + $top: 1, + 'api-version': '5.0-preview.4', + }, + headers, + } + if (branch) { + options.qs.branch = branch + } + const json = await this.fetch({ + url, + options, + schema: latestBuildSchema, + }) + + if (json.count !== 1) { + throw new NotFound({ prettyMessage: 'build pipeline not found' }) + } + + return json.value[0].id + } + + async handle( + { organization, project, definitionId, branch }, + { + compact_message: compactMessage, + passed_label: passedLabel, + failed_label: failedLabel, + skipped_label: skippedLabel, + } + ) { + const headers = getHeaders() + const buildId = await this.getLatestBuildId( + organization, + project, + definitionId, + branch, + headers + ) + + // https://dev.azure.com/azuredevops-powershell/azuredevops-powershell/_apis/test/ResultSummaryByBuild?buildId=20 + const url = `https://dev.azure.com/${organization}/${project}/_apis/test/ResultSummaryByBuild` + const options = { + qs: { + buildId, + }, + headers, + } + + const json = await this.fetch({ + url, + options, + schema: buildTestResultSummarySchema, + }) + + const total = json.aggregatedResultsAnalysis.totalTests + + let passed = 0 + const passedTests = json.aggregatedResultsAnalysis.resultsByOutcome.Passed + if (passedTests) { + passed = passedTests.count + } + + let failed = 0 + const failedTests = json.aggregatedResultsAnalysis.resultsByOutcome.Failed + if (failedTests) { + failed = failedTests.count + } + + // assume the rest has been skipped + const skipped = total - passed - failed + const isCompact = compactMessage !== undefined + return this.constructor.render({ + passed, + failed, + skipped, + total, + passedLabel, + failedLabel, + skippedLabel, + isCompact, + }) + } +} diff --git a/services/azure-devops/azure-devops-tests.tester.js b/services/azure-devops/azure-devops-tests.tester.js new file mode 100644 index 0000000000000..d79927a1080a6 --- /dev/null +++ b/services/azure-devops/azure-devops-tests.tester.js @@ -0,0 +1,159 @@ +'use strict' + +const Joi = require('joi') +const t = require('../create-service-tester')() +module.exports = t + +const org = 'azuredevops-powershell' +const project = 'azuredevops-powershell' +const definitionId = 1 +const nonExistentDefinitionId = 9999 +const buildId = 20 +const uriPrefix = `/${org}/${project}` +const azureDevOpsApiBaseUri = `https://dev.azure.com/${org}/${project}/_apis` +const mockBadgeUriPath = `${uriPrefix}/${definitionId}` +const mockBadgeUri = `${mockBadgeUriPath}.json` +const mockBranchBadgeUri = `${mockBadgeUriPath}/master.json` +const mockLatestBuildApiUriPath = `/build/builds?definitions=${definitionId}&%24top=1&api-version=5.0-preview.4` +const mockNonExistendBuildApiUriPath = `/build/builds?definitions=${nonExistentDefinitionId}&%24top=1&api-version=5.0-preview.4` +const mockTestResultSummaryApiUriPath = `/test/ResultSummaryByBuild?buildId=${buildId}` +const latestBuildResponse = { + count: 1, + value: [{ id: buildId }], +} +const mockEmptyTestResultSummaryResponse = { + aggregatedResultsAnalysis: { + totalTests: 0, + resultsByOutcome: {}, + }, +} + +const isAzureDevOpsTestTotals = Joi.string().regex( + /^(?:[0-9]+ (?:passed|skipped|failed)(?:, )?)+$/ +) + +const isCompactAzureDevOpsTestTotals = Joi.string().regex( + /^(?:[0-9]* ?(?:✔|✘|➟) ?[0-9]*(?:, | \| )?)+$/ +) + +const isCustomAzureDevOpsTestTotals = Joi.string().regex( + /^(?:[0-9]+ (?:good|bad|n\/a)(?:, )?)+$/ +) + +const isCompactCustomAzureDevOpsTestTotals = Joi.string().regex( + /^(?:[0-9]* ?(?:💃|🤦‍♀️|🤷) ?[0-9]*(?:, | \| )?)+$/ +) + +t.create('unknown build definition') + .get(`${uriPrefix}/${nonExistentDefinitionId}.json`) + .expectJSON({ name: 'tests', value: 'build pipeline not found' }) + +t.create('404 latest build error response') + .get(mockBadgeUri) + .intercept(nock => + nock(azureDevOpsApiBaseUri) + .get(mockLatestBuildApiUriPath) + .reply(404) + ) + .expectJSON({ + name: 'tests', + value: 'build pipeline or test result summary not found', + }) + +t.create('no build response') + .get(`${uriPrefix}/${nonExistentDefinitionId}.json`) + .intercept(nock => + nock(azureDevOpsApiBaseUri) + .get(mockNonExistendBuildApiUriPath) + .reply(200, { + count: 0, + value: [], + }) + ) + .expectJSON({ name: 'tests', value: 'build pipeline not found' }) + +t.create('no test result summary response') + .get(mockBadgeUri) + .intercept(nock => + nock(azureDevOpsApiBaseUri) + .get(mockLatestBuildApiUriPath) + .reply(200, latestBuildResponse) + .get(mockTestResultSummaryApiUriPath) + .reply(404) + ) + .expectJSON({ + name: 'tests', + value: 'build pipeline or test result summary not found', + }) + +t.create('invalid test result summary response') + .get(mockBadgeUri) + .intercept(nock => + nock(azureDevOpsApiBaseUri) + .get(mockLatestBuildApiUriPath) + .reply(200, latestBuildResponse) + .get(mockTestResultSummaryApiUriPath) + .reply(200, {}) + ) + .expectJSON({ name: 'tests', value: 'invalid response data' }) + +t.create('no tests in test result summary response') + .get(mockBadgeUri) + .intercept(nock => + nock(azureDevOpsApiBaseUri) + .get(mockLatestBuildApiUriPath) + .reply(200, latestBuildResponse) + .get(mockTestResultSummaryApiUriPath) + .reply(200, mockEmptyTestResultSummaryResponse) + ) + .expectJSON({ name: 'tests', value: 'no tests' }) + +t.create('test status') + .get(mockBadgeUri) + .expectJSONTypes( + Joi.object().keys({ name: 'tests', value: isAzureDevOpsTestTotals }) + ) + +t.create('test status on branch') + .get(mockBranchBadgeUri) + .expectJSONTypes( + Joi.object().keys({ name: 'tests', value: isAzureDevOpsTestTotals }) + ) + +t.create('test status with compact message') + .get(mockBadgeUri, { + qs: { + compact_message: null, + }, + }) + .expectJSONTypes( + Joi.object().keys({ name: 'tests', value: isCompactAzureDevOpsTestTotals }) + ) + +t.create('test status with custom labels') + .get(mockBadgeUri, { + qs: { + passed_label: 'good', + failed_label: 'bad', + skipped_label: 'n/a', + }, + }) + .expectJSONTypes( + Joi.object().keys({ name: 'tests', value: isCustomAzureDevOpsTestTotals }) + ) + +t.create('test status with compact message and custom labels') + .get(mockBadgeUri, { + qs: { + compact_message: null, + passed_label: '💃', + failed_label: '🤦‍♀️', + skipped_label: '🤷', + }, + }) + .expectJSONTypes( + Joi.object().keys({ + name: 'tests', + value: isCompactCustomAzureDevOpsTestTotals, + }) + ) From 111cfe9ea6072b5f53e5fac85f226bce0d8034dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Se=C3=A7kin?= Date: Tue, 27 Nov 2018 17:32:55 +0000 Subject: [PATCH 2/4] Improve result tests Added logic to generate only expected regex combinations. This makes the result tests more strict, and also addresses the lgtm alerts about ReDos vulnerabilities. --- .../azure-devops/azure-devops-tests.tester.js | 70 +++++++++++++++---- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/services/azure-devops/azure-devops-tests.tester.js b/services/azure-devops/azure-devops-tests.tester.js index d79927a1080a6..171c7fcbc50ac 100644 --- a/services/azure-devops/azure-devops-tests.tester.js +++ b/services/azure-devops/azure-devops-tests.tester.js @@ -28,20 +28,64 @@ const mockEmptyTestResultSummaryResponse = { }, } -const isAzureDevOpsTestTotals = Joi.string().regex( - /^(?:[0-9]+ (?:passed|skipped|failed)(?:, )?)+$/ -) +function getLabelRegex(label, isCompact) { + return isCompact ? `(?:${label} [0-9]*)` : `(?:[0-9]* ${label})` +} -const isCompactAzureDevOpsTestTotals = Joi.string().regex( - /^(?:[0-9]* ?(?:✔|✘|➟) ?[0-9]*(?:, | \| )?)+$/ -) +function isAzureDevOpsTestTotals( + passedLabel, + failedLabel, + skippedLabel, + isCompact +) { + const regexStrings = [ + `^${getLabelRegex(passedLabel, isCompact)}$`, + `^${getLabelRegex(failedLabel, isCompact)}$`, + `^${getLabelRegex(skippedLabel, isCompact)}$`, + `^${getLabelRegex(passedLabel, isCompact)} ${getLabelRegex( + failedLabel, + isCompact + )}$`, + `^${getLabelRegex(failedLabel, isCompact)} ${getLabelRegex( + skippedLabel, + isCompact + )}$`, + `^${getLabelRegex(passedLabel, isCompact)} ${getLabelRegex( + skippedLabel, + isCompact + )}$`, + `^${getLabelRegex(passedLabel, isCompact)} ${getLabelRegex( + failedLabel, + isCompact + )} ${getLabelRegex(skippedLabel, isCompact)}$`, + ] + + return Joi.alternatives().try( + regexStrings.map(regexStr => Joi.string().regex(new RegExp(regexStr))) + ) +} -const isCustomAzureDevOpsTestTotals = Joi.string().regex( - /^(?:[0-9]+ (?:good|bad|n\/a)(?:, )?)+$/ +const isDefaultAzureDevOpsTestTotals = isAzureDevOpsTestTotals( + 'passed', + 'skipped', + 'failed' ) - -const isCompactCustomAzureDevOpsTestTotals = Joi.string().regex( - /^(?:[0-9]* ?(?:💃|🤦‍♀️|🤷) ?[0-9]*(?:, | \| )?)+$/ +const isCompactAzureDevOpsTestTotals = isAzureDevOpsTestTotals( + '✔', + '✘', + '➟', + true +) +const isCustomAzureDevOpsTestTotals = isAzureDevOpsTestTotals( + 'good', + 'bad', + 'n\\/a' +) +const isCompactCustomAzureDevOpsTestTotals = isAzureDevOpsTestTotals( + '💃', + '🤦‍♀️', + '🤷', + true ) t.create('unknown build definition') @@ -111,13 +155,13 @@ t.create('no tests in test result summary response') t.create('test status') .get(mockBadgeUri) .expectJSONTypes( - Joi.object().keys({ name: 'tests', value: isAzureDevOpsTestTotals }) + Joi.object().keys({ name: 'tests', value: isDefaultAzureDevOpsTestTotals }) ) t.create('test status on branch') .get(mockBranchBadgeUri) .expectJSONTypes( - Joi.object().keys({ name: 'tests', value: isAzureDevOpsTestTotals }) + Joi.object().keys({ name: 'tests', value: isDefaultAzureDevOpsTestTotals }) ) t.create('test status with compact message') From 7f3798880b6f63a338772e6f96f4a6c0ea258060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Se=C3=A7kin?= Date: Mon, 3 Dec 2018 12:39:13 +0000 Subject: [PATCH 3/4] Fix compact mode tests Improved regex list construction and added a conditional separator for compact / default modes. --- .../azure-devops/azure-devops-tests.tester.js | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/services/azure-devops/azure-devops-tests.tester.js b/services/azure-devops/azure-devops-tests.tester.js index 171c7fcbc50ac..4c3ddc49065e7 100644 --- a/services/azure-devops/azure-devops-tests.tester.js +++ b/services/azure-devops/azure-devops-tests.tester.js @@ -38,26 +38,18 @@ function isAzureDevOpsTestTotals( skippedLabel, isCompact ) { + const passedRegex = getLabelRegex(passedLabel, isCompact) + const failedRegex = getLabelRegex(failedLabel, isCompact) + const skippedRegex = getLabelRegex(skippedLabel, isCompact) + const separator = isCompact ? ' | ' : ', ' const regexStrings = [ - `^${getLabelRegex(passedLabel, isCompact)}$`, - `^${getLabelRegex(failedLabel, isCompact)}$`, - `^${getLabelRegex(skippedLabel, isCompact)}$`, - `^${getLabelRegex(passedLabel, isCompact)} ${getLabelRegex( - failedLabel, - isCompact - )}$`, - `^${getLabelRegex(failedLabel, isCompact)} ${getLabelRegex( - skippedLabel, - isCompact - )}$`, - `^${getLabelRegex(passedLabel, isCompact)} ${getLabelRegex( - skippedLabel, - isCompact - )}$`, - `^${getLabelRegex(passedLabel, isCompact)} ${getLabelRegex( - failedLabel, - isCompact - )} ${getLabelRegex(skippedLabel, isCompact)}$`, + `^${passedRegex}$`, + `^${failedRegex}$`, + `^${skippedRegex}`, + `^${passedRegex}${separator}${failedRegex}$`, + `^${failedRegex}${separator}${skippedRegex}$`, + `^${passedRegex}${separator}${skippedRegex}$`, + `^${passedRegex}${separator}${failedRegex}${separator}${skippedLabel}$`, ] return Joi.alternatives().try( From 93b2e259be35323cfa8b7cacc2b836224112be30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Se=C3=A7kin?= Date: Mon, 3 Dec 2018 12:44:13 +0000 Subject: [PATCH 4/4] Extract identical methods to a base class Added a base service class and extracted `getLatestBuildId` method and extended both coverage and tests services from the base class. Tests should be run for: [AzureDevOpsCoverage] and [AzureDevOpsTests]. --- services/azure-devops/azure-devops-base.js | 64 +++++++++++++++++++ .../azure-devops-coverage.service.js | 61 +++--------------- .../azure-devops-tests.service.js | 61 +++--------------- 3 files changed, 80 insertions(+), 106 deletions(-) create mode 100644 services/azure-devops/azure-devops-base.js diff --git a/services/azure-devops/azure-devops-base.js b/services/azure-devops/azure-devops-base.js new file mode 100644 index 0000000000000..831a23df4da3a --- /dev/null +++ b/services/azure-devops/azure-devops-base.js @@ -0,0 +1,64 @@ +'use strict' + +const Joi = require('joi') +const BaseJsonService = require('../base-json') +const { NotFound } = require('../errors') + +const latestBuildSchema = Joi.object({ + count: Joi.number().required(), + value: Joi.array() + .items( + Joi.object({ + id: Joi.number().required(), + }) + ) + .required(), +}).required() + +module.exports = class BaseAzureDevOpsService extends BaseJsonService { + async fetch({ url, options, schema, errorMessages }) { + return this._requestJson({ + schema, + url, + options, + errorMessages, + }) + } + + async getLatestBuildId( + organization, + project, + definitionId, + branch, + headers, + errorMessages + ) { + // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.0 + const url = `https://dev.azure.com/${organization}/${project}/_apis/build/builds` + const options = { + qs: { + definitions: definitionId, + $top: 1, + 'api-version': '5.0-preview.4', + }, + headers, + } + + if (branch) { + options.qs.branch = branch + } + + const json = await this.fetch({ + url, + options, + schema: latestBuildSchema, + errorMessages, + }) + + if (json.count !== 1) { + throw new NotFound({ prettyMessage: 'build pipeline not found' }) + } + + return json.value[0].id + } +} diff --git a/services/azure-devops/azure-devops-coverage.service.js b/services/azure-devops/azure-devops-coverage.service.js index 07b5174686f08..3cc39fc339fd8 100644 --- a/services/azure-devops/azure-devops-coverage.service.js +++ b/services/azure-devops/azure-devops-coverage.service.js @@ -1,8 +1,7 @@ 'use strict' const Joi = require('joi') -const BaseJsonService = require('../base-json') -const { NotFound } = require('../errors') +const BaseAzureDevOpsService = require('./azure-devops-base') const { getHeaders } = require('./azure-devops-helpers') const documentation = ` @@ -29,17 +28,6 @@ const { coveragePercentage: coveragePercentageColor, } = require('../../lib/color-formatters') -const latestBuildSchema = Joi.object({ - count: Joi.number().required(), - value: Joi.array() - .items( - Joi.object({ - id: Joi.number().required(), - }) - ) - .required(), -}).required() - const buildCodeCoverageSchema = Joi.object({ coverageData: Joi.array() .items( @@ -59,7 +47,7 @@ const buildCodeCoverageSchema = Joi.object({ .required(), }).required() -module.exports = class AzureDevOpsCoverage extends BaseJsonService { +module.exports = class AzureDevOpsCoverage extends BaseAzureDevOpsService { static render({ coverage }) { return { message: `${coverage.toFixed(0)}%`, @@ -104,52 +92,18 @@ module.exports = class AzureDevOpsCoverage extends BaseJsonService { } } - async fetch({ url, options, schema }) { - return this._requestJson({ - schema, - url, - options, - errorMessages: { - 404: 'build pipeline or coverage not found', - }, - }) - } - - async getLatestBuildId(organization, project, definitionId, branch, headers) { - // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.0 - const url = `https://dev.azure.com/${organization}/${project}/_apis/build/builds` - const options = { - qs: { - definitions: definitionId, - $top: 1, - 'api-version': '5.0-preview.4', - }, - headers, - } - if (branch) { - options.qs.branch = branch - } - const json = await this.fetch({ - url, - options, - schema: latestBuildSchema, - }) - - if (json.count !== 1) { - throw new NotFound({ prettyMessage: 'build pipeline not found' }) - } - - return json.value[0].id - } - async handle({ organization, project, definitionId, branch }) { const headers = getHeaders() + const errorMessages = { + 404: 'build pipeline or coverage not found', + } const buildId = await this.getLatestBuildId( organization, project, definitionId, branch, - headers + headers, + errorMessages ) // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/test/code%20coverage/get%20build%20code%20coverage?view=azure-devops-rest-5.0 const url = `https://dev.azure.com/${organization}/${project}/_apis/test/codecoverage` @@ -164,6 +118,7 @@ module.exports = class AzureDevOpsCoverage extends BaseJsonService { url, options, schema: buildCodeCoverageSchema, + errorMessages, }) let covered = 0 diff --git a/services/azure-devops/azure-devops-tests.service.js b/services/azure-devops/azure-devops-tests.service.js index 5d20fa90632b8..f52c2c0aeaa19 100644 --- a/services/azure-devops/azure-devops-tests.service.js +++ b/services/azure-devops/azure-devops-tests.service.js @@ -1,8 +1,7 @@ 'use strict' const Joi = require('joi') -const BaseJsonService = require('../base-json') -const { NotFound } = require('../errors') +const BaseAzureDevOpsService = require('./azure-devops-base') const { getHeaders } = require('./azure-devops-helpers') const { renderTestResultBadge } = require('../../lib/text-formatters') @@ -40,17 +39,6 @@ const documentation = `

` -const latestBuildSchema = Joi.object({ - count: Joi.number().required(), - value: Joi.array() - .items( - Joi.object({ - id: Joi.number().required(), - }) - ) - .required(), -}).required() - const buildTestResultSummarySchema = Joi.object({ aggregatedResultsAnalysis: Joi.object({ totalTests: Joi.number().required(), @@ -68,7 +56,7 @@ const buildTestResultSummarySchema = Joi.object({ }).required(), }).required() -module.exports = class AzureDevOpsTests extends BaseJsonService { +module.exports = class AzureDevOpsTests extends BaseAzureDevOpsService { static render({ passed, failed, @@ -182,44 +170,6 @@ module.exports = class AzureDevOpsTests extends BaseJsonService { } } - async fetch({ url, options, schema }) { - return this._requestJson({ - schema, - url, - options, - errorMessages: { - 404: 'build pipeline or test result summary not found', - }, - }) - } - - async getLatestBuildId(organization, project, definitionId, branch, headers) { - // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.0 - const url = `https://dev.azure.com/${organization}/${project}/_apis/build/builds` - const options = { - qs: { - definitions: definitionId, - $top: 1, - 'api-version': '5.0-preview.4', - }, - headers, - } - if (branch) { - options.qs.branch = branch - } - const json = await this.fetch({ - url, - options, - schema: latestBuildSchema, - }) - - if (json.count !== 1) { - throw new NotFound({ prettyMessage: 'build pipeline not found' }) - } - - return json.value[0].id - } - async handle( { organization, project, definitionId, branch }, { @@ -230,12 +180,16 @@ module.exports = class AzureDevOpsTests extends BaseJsonService { } ) { const headers = getHeaders() + const errorMessages = { + 404: 'build pipeline or test result summary not found', + } const buildId = await this.getLatestBuildId( organization, project, definitionId, branch, - headers + headers, + errorMessages ) // https://dev.azure.com/azuredevops-powershell/azuredevops-powershell/_apis/test/ResultSummaryByBuild?buildId=20 @@ -251,6 +205,7 @@ module.exports = class AzureDevOpsTests extends BaseJsonService { url, options, schema: buildTestResultSummarySchema, + errorMessages, }) const total = json.aggregatedResultsAnalysis.totalTests