switch [TeamCity] to pattern and move url to query param (#4022)
* refactor(TeamCity): switch to pattern & move url to query param * refactor(TeamCity): rename query param
This commit is contained in:
committed by
repo-ranger[bot]
parent
311ccc8834
commit
b80070985c
@@ -7,19 +7,7 @@ module.exports = class TeamCityBase extends BaseJsonService {
|
||||
return { userKey: 'teamcity_user', passKey: 'teamcity_pass' }
|
||||
}
|
||||
|
||||
async fetch({
|
||||
protocol,
|
||||
hostAndPath,
|
||||
apiPath,
|
||||
schema,
|
||||
qs = {},
|
||||
errorMessages = {},
|
||||
}) {
|
||||
if (!hostAndPath) {
|
||||
// If hostAndPath is undefined then the user specified the legacy default path
|
||||
protocol = 'https'
|
||||
hostAndPath = 'teamcity.jetbrains.com'
|
||||
}
|
||||
async fetch({ url, schema, qs = {}, errorMessages = {} }) {
|
||||
// JetBrains API Auth Docs: https://confluence.jetbrains.com/display/TCD18/REST+API#RESTAPI-RESTAuthentication
|
||||
const options = { qs }
|
||||
const auth = this.authHelper.basicAuth
|
||||
@@ -30,7 +18,7 @@ module.exports = class TeamCityBase extends BaseJsonService {
|
||||
}
|
||||
|
||||
return this._requestJson({
|
||||
url: `${protocol}://${hostAndPath}/${apiPath}`,
|
||||
url,
|
||||
schema,
|
||||
options,
|
||||
errorMessages: { 404: 'build not found', ...errorMessages },
|
||||
|
||||
37
services/teamcity/teamcity-build-redirect.service.js
Normal file
37
services/teamcity/teamcity-build-redirect.service.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict'
|
||||
|
||||
const { redirector } = require('..')
|
||||
|
||||
const commonAttrs = {
|
||||
dateAdded: new Date('2019-09-15'),
|
||||
category: 'build',
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
redirector({
|
||||
...commonAttrs,
|
||||
name: 'TeamCityBuildLegacyCodeBetterRedirect',
|
||||
route: {
|
||||
base: 'teamcity/codebetter',
|
||||
pattern: ':buildId',
|
||||
},
|
||||
transformPath: ({ buildId }) => `/teamcity/build/s/${buildId}`,
|
||||
transformQueryParams: _params => ({
|
||||
server: 'https://teamcity.jetbrains.com',
|
||||
}),
|
||||
}),
|
||||
redirector({
|
||||
...commonAttrs,
|
||||
name: 'TeamCityBuildRedirect',
|
||||
route: {
|
||||
base: 'teamcity',
|
||||
pattern:
|
||||
':protocol(http|https)/:hostAndPath(.+)/:verbosity(s|e)/:buildId',
|
||||
},
|
||||
transformPath: ({ verbosity, buildId }) =>
|
||||
`/teamcity/build/${verbosity}/${buildId}`,
|
||||
transformQueryParams: ({ protocol, hostAndPath }) => ({
|
||||
server: `${protocol}://${hostAndPath}`,
|
||||
}),
|
||||
}),
|
||||
]
|
||||
45
services/teamcity/teamcity-build-redirect.tester.js
Normal file
45
services/teamcity/teamcity-build-redirect.tester.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict'
|
||||
|
||||
const { ServiceTester } = require('../tester')
|
||||
|
||||
const t = (module.exports = new ServiceTester({
|
||||
id: 'TeamCityBuildRedirect',
|
||||
title: 'TeamCityBuildRedirect',
|
||||
pathPrefix: '/teamcity',
|
||||
}))
|
||||
|
||||
t.create('codebetter')
|
||||
.get('/codebetter/IntelliJIdeaCe_JavaDecompilerEngineTests.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
`/teamcity/build/s/IntelliJIdeaCe_JavaDecompilerEngineTests.svg?server=${encodeURIComponent(
|
||||
'https://teamcity.jetbrains.com'
|
||||
)}`
|
||||
)
|
||||
|
||||
t.create('hostAndPath simple build')
|
||||
.get('/https/teamcity.jetbrains.com/s/bt345.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
`/teamcity/build/s/bt345.svg?server=${encodeURIComponent(
|
||||
'https://teamcity.jetbrains.com'
|
||||
)}`
|
||||
)
|
||||
|
||||
t.create('hostAndPath full build')
|
||||
.get('/https/teamcity.jetbrains.com/e/bt345.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
`/teamcity/build/e/bt345.svg?server=${encodeURIComponent(
|
||||
'https://teamcity.jetbrains.com'
|
||||
)}`
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { optionalUrl } = require('../validators')
|
||||
const TeamCityBase = require('./teamcity-base')
|
||||
|
||||
// The statusText field will start with a summary, potentially including test details, followed by an optional suffix.
|
||||
@@ -14,6 +15,10 @@ const buildStatusSchema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
server: optionalUrl,
|
||||
}).required()
|
||||
|
||||
module.exports = class TeamCityBuild extends TeamCityBase {
|
||||
static get category() {
|
||||
return 'build'
|
||||
@@ -21,33 +26,22 @@ module.exports = class TeamCityBuild extends TeamCityBase {
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
base: 'teamcity',
|
||||
// Do not base new services on this route pattern.
|
||||
// See https://github.com/badges/shields/issues/3714
|
||||
format: '(?:codebetter|(http|https)/(.+)/(s|e))/([^/]+?)',
|
||||
capture: ['protocol', 'hostAndPath', 'verbosity', 'buildId'],
|
||||
base: 'teamcity/build',
|
||||
pattern: ':verbosity(s|e)/:buildId',
|
||||
queryParamSchema,
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'TeamCity Build Status (CodeBetter)',
|
||||
pattern: 'codebetter/:buildId',
|
||||
title: 'TeamCity Simple Build Status',
|
||||
namedParams: {
|
||||
verbosity: 's',
|
||||
buildId: 'IntelliJIdeaCe_JavaDecompilerEngineTests',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
status: 'SUCCESS',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'TeamCity Simple Build Status',
|
||||
pattern: ':protocol/:hostAndPath/s/:buildId',
|
||||
namedParams: {
|
||||
protocol: 'https',
|
||||
hostAndPath: 'teamcity.jetbrains.com',
|
||||
buildId: 'IntelliJIdeaCe_JavaDecompilerEngineTests',
|
||||
queryParams: {
|
||||
server: 'https://teamcity.jetbrains.com',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
status: 'SUCCESS',
|
||||
@@ -55,12 +49,13 @@ module.exports = class TeamCityBuild extends TeamCityBase {
|
||||
},
|
||||
{
|
||||
title: 'TeamCity Full Build Status',
|
||||
pattern: ':protocol/:hostAndPath/e/:buildId',
|
||||
namedParams: {
|
||||
protocol: 'https',
|
||||
hostAndPath: 'teamcity.jetbrains.com',
|
||||
verbosity: 'e',
|
||||
buildId: 'bt345',
|
||||
},
|
||||
queryParams: {
|
||||
server: 'https://teamcity.jetbrains.com',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
status: 'FAILURE',
|
||||
statusText: 'Tests failed: 4, passed: 1103, ignored: 2',
|
||||
@@ -96,14 +91,15 @@ module.exports = class TeamCityBuild extends TeamCityBase {
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ protocol, hostAndPath, verbosity, buildId }) {
|
||||
async handle(
|
||||
{ verbosity, buildId },
|
||||
{ server = 'https://teamcity.jetbrains.com' }
|
||||
) {
|
||||
// JetBrains Docs: https://confluence.jetbrains.com/display/TCD18/REST+API#RESTAPI-BuildStatusIcon
|
||||
const buildLocator = `buildType:(id:${buildId})`
|
||||
const apiPath = `app/rest/builds/${encodeURIComponent(buildLocator)}`
|
||||
const json = await this.fetch({
|
||||
protocol,
|
||||
hostAndPath,
|
||||
apiPath,
|
||||
url: `${server}/${apiPath}`,
|
||||
schema: buildStatusSchema,
|
||||
})
|
||||
// If the verbosity is 'e' then the user has requested the verbose (full) build status.
|
||||
|
||||
@@ -22,12 +22,15 @@ describe('TeamCityBuild', function() {
|
||||
})
|
||||
|
||||
expect(
|
||||
await TeamCityBuild.invoke(defaultContext, config, {
|
||||
protocol: 'https',
|
||||
hostAndPath: 'mycompany.teamcity.com',
|
||||
verbosity: 'e',
|
||||
buildId: 'bt678',
|
||||
})
|
||||
await TeamCityBuild.invoke(
|
||||
defaultContext,
|
||||
config,
|
||||
{
|
||||
verbosity: 'e',
|
||||
buildId: 'bt678',
|
||||
},
|
||||
{ server: 'https://mycompany.teamcity.com' }
|
||||
)
|
||||
).to.deep.equal({
|
||||
message: 'tests failed: 1 (1 new), passed: 50246, ignored: 1, muted: 12',
|
||||
color: 'red',
|
||||
|
||||
@@ -7,33 +7,26 @@ const t = (module.exports = require('../tester').createServiceTester())
|
||||
const buildStatusValues = Joi.equal('passing', 'failure', 'error').required()
|
||||
const buildStatusTextRegex = /^success|failure|error|tests( failed: \d+( \(\d+ new\))?)?(,)?( passed: \d+)?(,)?( ignored: \d+)?(,)?( muted: \d+)?/
|
||||
|
||||
t.create('codebetter unknown build')
|
||||
.get('/codebetter/btabc.json')
|
||||
t.create('unknown build')
|
||||
.get('/s/btabc.json?server=https://teamcity.jetbrains.com')
|
||||
.expectBadge({ label: 'build', message: 'build not found' })
|
||||
|
||||
t.create('codebetter known build')
|
||||
.get('/codebetter/IntelliJIdeaCe_JavaDecompilerEngineTests.json')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: buildStatusValues,
|
||||
})
|
||||
|
||||
t.create('simple status for known build')
|
||||
.get('/https/teamcity.jetbrains.com/s/bt345.json')
|
||||
.get('/s/bt345.json?server=https://teamcity.jetbrains.com')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: buildStatusValues,
|
||||
})
|
||||
|
||||
t.create('full status for known build')
|
||||
.get('/https/teamcity.jetbrains.com/e/bt345.json')
|
||||
.get('/e/bt345.json?server=https://teamcity.jetbrains.com')
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
message: withRegex(buildStatusTextRegex),
|
||||
})
|
||||
|
||||
t.create('codebetter success build')
|
||||
.get('/codebetter/bt123.json')
|
||||
.get('/s/bt123.json?server=https://teamcity.jetbrains.com')
|
||||
.intercept(nock =>
|
||||
nock('https://teamcity.jetbrains.com/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt123)')}`)
|
||||
@@ -50,7 +43,7 @@ t.create('codebetter success build')
|
||||
})
|
||||
|
||||
t.create('codebetter failure build')
|
||||
.get('/codebetter/bt123.json')
|
||||
.get('/s/bt123.json?server=https://teamcity.jetbrains.com')
|
||||
.intercept(nock =>
|
||||
nock('https://teamcity.jetbrains.com/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt123)')}`)
|
||||
@@ -67,7 +60,7 @@ t.create('codebetter failure build')
|
||||
})
|
||||
|
||||
t.create('simple build status with passed build')
|
||||
.get('/https/myteamcity.com:8080/s/bt321.json')
|
||||
.get('/s/bt321.json?server=https://myteamcity.com:8080')
|
||||
.intercept(nock =>
|
||||
nock('https://myteamcity.com:8080/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt321)')}`)
|
||||
@@ -84,7 +77,7 @@ t.create('simple build status with passed build')
|
||||
})
|
||||
|
||||
t.create('simple build status with failed build')
|
||||
.get('/https/myteamcity.com:8080/s/bt999.json')
|
||||
.get('/s/bt999.json?server=https://myteamcity.com:8080')
|
||||
.intercept(nock =>
|
||||
nock('https://myteamcity.com:8080/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt999)')}`)
|
||||
@@ -101,7 +94,7 @@ t.create('simple build status with failed build')
|
||||
})
|
||||
|
||||
t.create('full build status with passed build')
|
||||
.get('/https/selfhosted.teamcity.com:4000/e/bt321.json')
|
||||
.get('/e/bt321.json?server=https://selfhosted.teamcity.com:4000')
|
||||
.intercept(nock =>
|
||||
nock('https://selfhosted.teamcity.com:4000/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt321)')}`)
|
||||
@@ -118,7 +111,7 @@ t.create('full build status with passed build')
|
||||
})
|
||||
|
||||
t.create('full build status with failed build')
|
||||
.get('/https/selfhosted.teamcity.com:4000/tc/e/bt567.json')
|
||||
.get('/e/bt567.json?server=https://selfhosted.teamcity.com:4000/tc')
|
||||
.intercept(nock =>
|
||||
nock('https://selfhosted.teamcity.com:4000/tc/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt567)')}`)
|
||||
|
||||
18
services/teamcity/teamcity-coverage-redirect.service.js
Normal file
18
services/teamcity/teamcity-coverage-redirect.service.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict'
|
||||
|
||||
const { redirector } = require('..')
|
||||
|
||||
module.exports = [
|
||||
redirector({
|
||||
category: 'coverage',
|
||||
route: {
|
||||
base: 'teamcity/coverage',
|
||||
pattern: ':protocol(http|https)/:hostAndPath(.+)/:buildId',
|
||||
},
|
||||
transformPath: ({ buildId }) => `/teamcity/coverage/${buildId}`,
|
||||
transformQueryParams: ({ protocol, hostAndPath }) => ({
|
||||
server: `${protocol}://${hostAndPath}`,
|
||||
}),
|
||||
dateAdded: new Date('2019-09-15'),
|
||||
}),
|
||||
]
|
||||
21
services/teamcity/teamcity-coverage-redirect.tester.js
Normal file
21
services/teamcity/teamcity-coverage-redirect.tester.js
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict'
|
||||
|
||||
const { ServiceTester } = require('../tester')
|
||||
|
||||
const t = (module.exports = new ServiceTester({
|
||||
id: 'TeamCityCoverageRedirect',
|
||||
title: 'TeamCityCoverageRedirect',
|
||||
pathPrefix: '/teamcity/coverage',
|
||||
}))
|
||||
|
||||
t.create('coverage')
|
||||
.get('/https/teamcity.jetbrains.com/ReactJSNet_PullRequests.svg', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader(
|
||||
'Location',
|
||||
`/teamcity/coverage/ReactJSNet_PullRequests.svg?server=${encodeURIComponent(
|
||||
'https://teamcity.jetbrains.com'
|
||||
)}`
|
||||
)
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { coveragePercentage } = require('../color-formatters')
|
||||
const { optionalUrl } = require('../validators')
|
||||
const TeamCityBase = require('./teamcity-base')
|
||||
const { InvalidResponse } = require('..')
|
||||
|
||||
@@ -16,6 +17,10 @@ const buildStatisticsSchema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
server: optionalUrl,
|
||||
}).required()
|
||||
|
||||
module.exports = class TeamCityCoverage extends TeamCityBase {
|
||||
static get category() {
|
||||
return 'coverage'
|
||||
@@ -23,38 +28,26 @@ module.exports = class TeamCityCoverage extends TeamCityBase {
|
||||
|
||||
static get route() {
|
||||
return {
|
||||
// Do not base new services on this route pattern.
|
||||
// See https://github.com/badges/shields/issues/3714
|
||||
base: 'teamcity/coverage',
|
||||
format: '(?:(http|https)/(.+)/)?([^/]+?)',
|
||||
capture: ['protocol', 'hostAndPath', 'buildId'],
|
||||
pattern: ':buildId',
|
||||
queryParamSchema,
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
return [
|
||||
{
|
||||
title: 'TeamCity Coverage (CodeBetter)',
|
||||
pattern: ':buildId',
|
||||
title: 'TeamCity Coverage',
|
||||
namedParams: {
|
||||
buildId: 'ReactJSNet_PullRequests',
|
||||
},
|
||||
queryParams: {
|
||||
server: 'https://teamcity.jetbrains.com',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
coverage: 82,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'TeamCity Coverage',
|
||||
pattern: ':protocol/:hostAndPath/s/:buildId',
|
||||
namedParams: {
|
||||
protocol: 'https',
|
||||
hostAndPath: 'teamcity.jetbrains.com',
|
||||
buildId: 'ReactJSNet_PullRequests',
|
||||
},
|
||||
staticPreview: this.render({
|
||||
coverage: 95,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -90,16 +83,14 @@ module.exports = class TeamCityCoverage extends TeamCityBase {
|
||||
throw new InvalidResponse({ prettyMessage: 'no coverage data available' })
|
||||
}
|
||||
|
||||
async handle({ protocol, hostAndPath, buildId }) {
|
||||
async handle({ buildId }, { server = 'https://teamcity.jetbrains.com' }) {
|
||||
// JetBrains Docs: https://confluence.jetbrains.com/display/TCD18/REST+API#RESTAPI-Statistics
|
||||
const buildLocator = `buildType:(id:${buildId})`
|
||||
const apiPath = `app/rest/builds/${encodeURIComponent(
|
||||
buildLocator
|
||||
)}/statistics`
|
||||
const data = await this.fetch({
|
||||
protocol,
|
||||
hostAndPath,
|
||||
apiPath,
|
||||
url: `${server}/${apiPath}`,
|
||||
schema: buildStatisticsSchema,
|
||||
})
|
||||
|
||||
|
||||
@@ -28,11 +28,14 @@ describe('TeamCityCoverage', function() {
|
||||
})
|
||||
|
||||
expect(
|
||||
await TeamCityCoverage.invoke(defaultContext, config, {
|
||||
protocol: 'https',
|
||||
hostAndPath: 'mycompany.teamcity.com',
|
||||
buildId: 'bt678',
|
||||
})
|
||||
await TeamCityCoverage.invoke(
|
||||
defaultContext,
|
||||
config,
|
||||
{
|
||||
buildId: 'bt678',
|
||||
},
|
||||
{ server: 'https://mycompany.teamcity.com' }
|
||||
)
|
||||
).to.deep.equal({
|
||||
message: '82%',
|
||||
color: 'yellowgreen',
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
const { isIntegerPercentage } = require('../test-validators')
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
|
||||
t.create('invalid buildId')
|
||||
.get('/btABC999.json')
|
||||
.expectBadge({ label: 'coverage', message: 'build not found' })
|
||||
|
||||
t.create('valid buildId')
|
||||
.get('/ReactJSNet_PullRequests.json')
|
||||
.expectBadge({
|
||||
@@ -11,30 +15,12 @@ t.create('valid buildId')
|
||||
})
|
||||
|
||||
t.create('specified instance valid buildId')
|
||||
.get('/https/teamcity.jetbrains.com/ReactJSNet_PullRequests.json')
|
||||
.get('/ReactJSNet_PullRequests.json?server=https://teamcity.jetbrains.com')
|
||||
.expectBadge({
|
||||
label: 'coverage',
|
||||
message: isIntegerPercentage,
|
||||
})
|
||||
|
||||
t.create('invalid buildId')
|
||||
.get('/btABC999.json')
|
||||
.expectBadge({ label: 'coverage', message: 'build not found' })
|
||||
|
||||
t.create('specified instance invalid buildId')
|
||||
.get('/https/teamcity.jetbrains.com/btABC000.json')
|
||||
.expectBadge({ label: 'coverage', message: 'build not found' })
|
||||
|
||||
t.create('404 latest build error response')
|
||||
.get('/bt123.json')
|
||||
.intercept(nock =>
|
||||
nock('https://teamcity.jetbrains.com/app/rest/builds')
|
||||
.get(`/${encodeURIComponent('buildType:(id:bt123)')}/statistics`)
|
||||
.query({ guest: 1 })
|
||||
.reply(404)
|
||||
)
|
||||
.expectBadge({ label: 'coverage', message: 'build not found' })
|
||||
|
||||
t.create('no coverage data for build')
|
||||
.get('/bt234.json')
|
||||
.intercept(nock =>
|
||||
|
||||
Reference in New Issue
Block a user