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:
Caleb Cartwright
2019-09-16 18:59:04 -05:00
committed by repo-ranger[bot]
parent 311ccc8834
commit b80070985c
11 changed files with 189 additions and 108 deletions

View File

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

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

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

View File

@@ -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.

View File

@@ -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',

View File

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

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

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

View File

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

View File

@@ -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',

View File

@@ -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 =>