Repository: badges/shields (js)

Issue:
Move remaining hostnames from route params to query params
A follow up to #3714 with the last services that still have url/server/host parameters specified as route params instead of query params:

* [ ] [Jenkins (build, coverage, and tests)](https://github.com/badges/shields/tree/master/services/jenkins)
* [ ] ~~[Mozilla Observatory](https://github.com/badges/shields/blob/master/services/mozilla-observatory/mozilla-observatory.service.js#L61)~~
* [x] [Swagger](https://github.com/badges/shields/blob/master/services/swagger/swagger.service.js)

I _think_ that's all of them but if anyone sees any others please update this list/leave a comment

Pull request description:
Move [Jenkins] job url to query parameter
Migrates the Jenkins Build, Coverage and Tests services. Closes #4178.

Patch:
diff --git a/services/jenkins/jenkins-build-redirect.service.js b/services/jenkins/jenkins-build-redirect.service.js
index 44d310773c..42a0236e02 100644
--- a/services/jenkins/jenkins-build-redirect.service.js
+++ b/services/jenkins/jenkins-build-redirect.service.js
@@ -1,12 +1,14 @@
 'use strict'
 
+const { buildRedirectUrl } = require('./jenkins-common')
 const { redirector } = require('..')
 
 const commonProps = {
   category: 'build',
-  transformPath: ({ protocol, host, job }) =>
-    `/jenkins/build/${protocol}/${host}/${job}`,
-  dateAdded: new Date('2019-04-20'),
+  transformPath: () => '/jenkins/build',
+  transformQueryParams: ({ protocol, host, job }) => ({
+    jobUrl: buildRedirectUrl({ protocol, host, job }),
+  }),
 }
 
 module.exports = [
@@ -15,6 +17,7 @@ module.exports = [
       base: 'jenkins-ci/s',
       pattern: ':protocol(http|https)/:host/:job+',
     },
+    dateAdded: new Date('2019-04-20'),
     ...commonProps,
   }),
   redirector({
@@ -22,6 +25,15 @@ module.exports = [
       base: 'jenkins/s',
       pattern: ':protocol(http|https)/:host/:job+',
     },
+    dateAdded: new Date('2019-04-20'),
+    ...commonProps,
+  }),
+  redirector({
+    route: {
+      base: 'jenkins/build',
+      pattern: ':protocol(http|https)/:host/:job+',
+    },
+    dateAdded: new Date('2019-11-29'),
     ...commonProps,
   }),
 ]
diff --git a/services/jenkins/jenkins-build.service.js b/services/jenkins/jenkins-build.service.js
index b8db081ad5..fab88a72f8 100644
--- a/services/jenkins/jenkins-build.service.js
+++ b/services/jenkins/jenkins-build.service.js
@@ -40,8 +40,8 @@ module.exports = class JenkinsBuild extends JenkinsBase {
 
   static get route() {
     return {
-      base: 'jenkins/build',
-      pattern: ':protocol(http|https)/:host/:job+',
+      base: 'jenkins',
+      pattern: 'build',
       queryParamSchema,
     }
   }
@@ -50,11 +50,9 @@ module.exports = class JenkinsBuild extends JenkinsBase {
     return [
       {
         title: 'Jenkins',
-        namedParams: {
-          protocol: 'https',
-          host: 'jenkins.qa.ubuntu.com',
-          job:
-            'view/Precise/view/All%20Precise/job/precise-desktop-amd64_default',
+        namedParams: {},
+        queryParams: {
+          jobUrl: 'https://wso2.org/jenkins/view/All%20Builds/job/archetypes',
         },
         staticPreview: renderBuildStatusBadge({ status: 'passing' }),
       },
@@ -82,9 +80,9 @@ module.exports = class JenkinsBuild extends JenkinsBase {
     return { status: colorStatusMap[json.color] }
   }
 
-  async handle({ protocol, host, job }, { disableStrictSSL }) {
+  async handle(namedParams, { jobUrl, disableStrictSSL }) {
     const json = await this.fetch({
-      url: buildUrl({ protocol, host, job, lastCompletedBuild: false }),
+      url: buildUrl({ jobUrl, lastCompletedBuild: false }),
       schema,
       qs: buildTreeParamQueryString('color'),
       disableStrictSSL,
diff --git a/services/jenkins/jenkins-build.spec.js b/services/jenkins/jenkins-build.spec.js
index 32cb856518..225290c94b 100644
--- a/services/jenkins/jenkins-build.spec.js
+++ b/services/jenkins/jenkins-build.spec.js
@@ -74,11 +74,15 @@ describe('JenkinsBuild', function() {
         .reply(200, { color: 'blue' })
 
       expect(
-        await JenkinsBuild.invoke(defaultContext, config, {
-          protocol: 'https',
-          host: 'jenkins.ubuntu.com',
-          job: 'server/job/curtin-vmtest-daily-x',
-        })
+        await JenkinsBuild.invoke(
+          defaultContext,
+          config,
+          {},
+          {
+            jobUrl:
+              'https://jenkins.ubuntu.com/server/job/curtin-vmtest-daily-x',
+          }
+        )
       ).to.deep.equal({
         label: undefined,
         message: 'passing',
diff --git a/services/jenkins/jenkins-common.js b/services/jenkins/jenkins-common.js
index bf71e8949b..c555abd746 100644
--- a/services/jenkins/jenkins-common.js
+++ b/services/jenkins/jenkins-common.js
@@ -1,26 +1,29 @@
 'use strict'
 
 const Joi = require('@hapi/joi')
+const { optionalUrl } = require('../validators')
 
 const queryParamSchema = Joi.object({
   disableStrictSSL: Joi.equal(''),
+  jobUrl: optionalUrl,
 }).required()
 
-const buildUrl = ({
-  protocol,
-  host,
-  job,
-  lastCompletedBuild = true,
-  plugin,
-}) => {
+const buildRedirectUrl = ({ protocol, host, job }) => {
   const jobPrefix = job.indexOf('/') > -1 ? '' : 'job/'
-  return `${protocol}://${host}/${jobPrefix}${job}/${
-    lastCompletedBuild ? 'lastCompletedBuild/' : ''
-  }${plugin ? `${plugin}/` : ''}api/json`
+  return `${protocol}://${host}/${jobPrefix}${job}`
+}
+
+const buildUrl = ({ jobUrl, lastCompletedBuild = true, plugin }) => {
+  const lastCompletedBuildElement = lastCompletedBuild
+    ? 'lastCompletedBuild/'
+    : ''
+  const pluginElement = plugin ? `${plugin}/` : ''
+  return `${jobUrl}/${lastCompletedBuildElement}${pluginElement}api/json`
 }
 
 module.exports = {
   queryParamSchema,
   buildTreeParamQueryString: tree => ({ tree }),
   buildUrl,
+  buildRedirectUrl,
 }
diff --git a/services/jenkins/jenkins-common.spec.js b/services/jenkins/jenkins-common.spec.js
new file mode 100644
index 0000000000..914690336c
--- /dev/null
+++ b/services/jenkins/jenkins-common.spec.js
@@ -0,0 +1,65 @@
+'use strict'
+const { expect } = require('chai')
+const { buildRedirectUrl, buildUrl } = require('./jenkins-common')
+
+describe('jenkins-common', function() {
+  describe('buildUrl', function() {
+    it('returns the json api url', function() {
+      const actualResult = buildUrl({
+        jobUrl: 'https://ci.eclipse.org/jgit/job/jgit',
+      })
+
+      expect(actualResult).to.equal(
+        'https://ci.eclipse.org/jgit/job/jgit/lastCompletedBuild/api/json'
+      )
+    })
+
+    it('returns the json api url including a plugin name', function() {
+      const actualResult = buildUrl({
+        jobUrl: 'https://ci.eclipse.org/jgit/job/jgit',
+        plugin: 'cobertura',
+      })
+
+      expect(actualResult).to.equal(
+        'https://ci.eclipse.org/jgit/job/jgit/lastCompletedBuild/cobertura/api/json'
+      )
+    })
+
+    it('returns the json api url without the lastCompletedBuild element', function() {
+      const actualResult = buildUrl({
+        jobUrl: 'https://ci.eclipse.org/jgit/job/jgit',
+        lastCompletedBuild: false,
+      })
+
+      expect(actualResult).to.equal(
+        'https://ci.eclipse.org/jgit/job/jgit/api/json'
+      )
+    })
+  })
+
+  describe('buildRedirectUrl', function() {
+    it('returns the job url', function() {
+      const actualResult = buildRedirectUrl({
+        protocol: 'https',
+        host: 'jenkins.sqlalchemy.org',
+        job: 'job/alembic_coverage',
+      })
+
+      expect(actualResult).to.equal(
+        'https://jenkins.sqlalchemy.org/job/alembic_coverage'
+      )
+    })
+
+    it('returns the job url and adds missing /job prefixes', function() {
+      const actualResult = buildRedirectUrl({
+        protocol: 'https',
+        host: 'jenkins.sqlalchemy.org',
+        job: 'alembic_coverage',
+      })
+
+      expect(actualResult).to.equal(
+        'https://jenkins.sqlalchemy.org/job/alembic_coverage'
+      )
+    })
+  })
+})
diff --git a/services/jenkins/jenkins-coverage-redirector.service.js b/services/jenkins/jenkins-coverage-redirector.service.js
index 37051003ea..f61208e5ce 100644
--- a/services/jenkins/jenkins-coverage-redirector.service.js
+++ b/services/jenkins/jenkins-coverage-redirector.service.js
@@ -1,16 +1,35 @@
 'use strict'
 
+const { buildRedirectUrl } = require('./jenkins-common')
 const { redirector } = require('..')
 
-module.exports = redirector({
+const commonProps = {
   category: 'coverage',
-  route: {
-    base: 'jenkins',
-    pattern: ':coverageFormat(j|c)/:protocol(http|https)/:host/:job+',
-  },
-  transformPath: ({ coverageFormat, protocol, host, job }) =>
-    `/jenkins/coverage/${
-      coverageFormat === 'j' ? 'jacoco' : 'cobertura'
-    }/${protocol}/${host}/${job}`,
-  dateAdded: new Date('2019-04-20'),
-})
+  transformQueryParams: ({ protocol, host, job }) => ({
+    jobUrl: buildRedirectUrl({ protocol, host, job }),
+  }),
+}
+
+module.exports = [
+  redirector({
+    route: {
+      base: 'jenkins',
+      pattern: ':coverageFormat(j|c)/:protocol(http|https)/:host/:job+',
+    },
+    transformPath: ({ coverageFormat }) =>
+      `/jenkins/coverage/${coverageFormat === 'j' ? 'jacoco' : 'cobertura'}`,
+    dateAdded: new Date('2019-04-20'),
+    ...commonProps,
+  }),
+  redirector({
+    route: {
+      base: 'jenkins/coverage',
+      pattern:
+        ':coverageFormat(jacoco|cobertura|api)/:protocol(http|https)/:host/:job+',
+    },
+    transformPath: ({ coverageFormat }) =>
+      `/jenkins/coverage/${coverageFormat}`,
+    dateAdded: new Date('2019-11-29'),
+    ...commonProps,
+  }),
+]
diff --git a/services/jenkins/jenkins-coverage.service.js b/services/jenkins/jenkins-coverage.service.js
index c0d7f8aa2f..be31e4b183 100644
--- a/services/jenkins/jenkins-coverage.service.js
+++ b/services/jenkins/jenkins-coverage.service.js
@@ -98,8 +98,7 @@ module.exports = class JenkinsCoverage extends JenkinsBase {
   static get route() {
     return {
       base: 'jenkins/coverage',
-      pattern:
-        ':format(jacoco|cobertura|api)/:protocol(http|https)/:host/:job+',
+      pattern: ':format(jacoco|cobertura|api)',
       queryParamSchema,
     }
   }
@@ -110,9 +109,9 @@ module.exports = class JenkinsCoverage extends JenkinsBase {
         title: 'Jenkins Coverage',
         namedParams: {
           format: 'cobertura',
-          protocol: 'https',
-          host: 'jenkins.sqlalchemy.org',
-          job: 'job/alembic_coverage',
+        },
+        queryParams: {
+          jobUrl: 'https://jenkins.sqlalchemy.org/job/alembic_coverage',
         },
         keywords: ['jacoco', 'cobertura', 'llvm-cov', 'istanbul'],
         staticPreview: this.render({ coverage: 95 }),
@@ -132,12 +131,12 @@ module.exports = class JenkinsCoverage extends JenkinsBase {
     }
   }
 
-  async handle({ format, protocol, host, job }, { disableStrictSSL }) {
+  async handle({ format }, { jobUrl, disableStrictSSL }) {
     const { schema, transform, treeQueryParam, pluginSpecificPath } = formatMap[
       format
     ]
     const json = await this.fetch({
-      url: buildUrl({ protocol, host, job, plugin: pluginSpecificPath }),
+      url: buildUrl({ jobUrl, plugin: pluginSpecificPath }),
       schema,
       qs: buildTreeParamQueryString(treeQueryParam),
       disableStrictSSL,


Test patch:
diff --git a/services/jenkins/jenkins-build-redirect.tester.js b/services/jenkins/jenkins-build-redirect.tester.js
index 11d5186f08..18cf61a5bb 100644
--- a/services/jenkins/jenkins-build-redirect.tester.js
+++ b/services/jenkins/jenkins-build-redirect.tester.js
@@ -8,22 +8,38 @@ const t = (module.exports = new ServiceTester({
   pathPrefix: '/',
 }))
 
-t.create('jenkins ci')
+t.create('old jenkins ci prefix + job url in path')
   .get('jenkins-ci/s/https/updates.jenkins-ci.org/job/foo.svg', {
     followRedirect: false,
   })
   .expectStatus(301)
   .expectHeader(
     'Location',
-    '/jenkins/build/https/updates.jenkins-ci.org/job/foo.svg'
+    `/jenkins/build.svg?jobUrl=${encodeURIComponent(
+      'https://updates.jenkins-ci.org/job/foo'
+    )}`
   )
 
-t.create('jenkins shorthand')
+t.create('old jenkins shorthand prefix + job url in path')
   .get('jenkins/s/https/updates.jenkins-ci.org/job/foo.svg', {
     followRedirect: false,
   })
   .expectStatus(301)
   .expectHeader(
     'Location',
-    '/jenkins/build/https/updates.jenkins-ci.org/job/foo.svg'
+    `/jenkins/build.svg?jobUrl=${encodeURIComponent(
+      'https://updates.jenkins-ci.org/job/foo'
+    )}`
+  )
+
+t.create('new jenkins build prefix + job url in path')
+  .get('jenkins/build/https/updates.jenkins-ci.org/job/foo.svg', {
+    followRedirect: false,
+  })
+  .expectStatus(301)
+  .expectHeader(
+    'Location',
+    `/jenkins/build.svg?jobUrl=${encodeURIComponent(
+      'https://updates.jenkins-ci.org/job/foo'
+    )}`
   )
diff --git a/services/jenkins/jenkins-build.tester.js b/services/jenkins/jenkins-build.tester.js
index fb3dbff938..7de9c6cdb1 100644
--- a/services/jenkins/jenkins-build.tester.js
+++ b/services/jenkins/jenkins-build.tester.js
@@ -10,13 +10,17 @@ const isJenkinsBuildStatus = Joi.alternatives(
 )
 
 t.create('build job not found')
-  .get('/https/ci.eclipse.org/jgit/job/does-not-exist.json')
+  .get('/build.json?jobUrl=https://ci.eclipse.org/jgit/job/does-not-exist')
   .expectBadge({ label: 'build', message: 'instance or job not found' })
 
 t.create('build found (view)')
-  .get('/https/wso2.org/jenkins/view/All%20Builds/job/archetypes.json')
+  .get(
+    `/build.json?jobUrl=${encodeURIComponent(
+      'https://wso2.org/jenkins/view/All Builds/job/archetypes'
+    )}`
+  )
   .expectBadge({ label: 'build', message: isJenkinsBuildStatus })
 
 t.create('build found (job)')
-  .get('/https/ci.eclipse.org/jgit/job/jgit.json')
+  .get('/build.json?jobUrl=https://ci.eclipse.org/jgit/job/jgit')
   .expectBadge({ label: 'build', message: isJenkinsBuildStatus })
diff --git a/services/jenkins/jenkins-coverage-redirector.tester.js b/services/jenkins/jenkins-coverage-redirector.tester.js
index d5bbee03c9..aea439fd94 100644
--- a/services/jenkins/jenkins-coverage-redirector.tester.js
+++ b/services/jenkins/jenkins-coverage-redirector.tester.js
@@ -8,7 +8,7 @@ const t = (module.exports = new ServiceTester({
   pathPrefix: '/jenkins',
 }))
 
-t.create('Jacoco')
+t.create('old Jacoco prefix + job url in path')
   .get(
     '/j/https/wso2.org/jenkins/view/All%20Builds/job/sonar/job/sonar-carbon-dashboards.svg',
     {
@@ -18,15 +18,64 @@ t.create('Jacoco')
   .expectStatus(301)
   .expectHeader(
     'Location',
-    '/jenkins/coverage/jacoco/https/wso2.org/jenkins/view/All Builds/job/sonar/job/sonar-carbon-dashboards.svg'
+    `/jenkins/coverage/jacoco.svg?jobUrl=${encodeURIComponent(
+      'https://wso2.org/jenkins/view/All Builds/job/sonar/job/sonar-carbon-dashboards'
+    )}`
   )
 
-t.create('Cobertura')
+t.create('new Jacoco prefix + job url in path')
+  .get(
+    '/coverage/jacoco/https/wso2.org/jenkins/view/All%20Builds/job/sonar/job/sonar-carbon-dashboards.svg',
+    {
+      followRedirect: false,
+    }
+  )
+  .expectStatus(301)
+  .expectHeader(
+    'Location',
+    `/jenkins/coverage/jacoco.svg?jobUrl=${encodeURIComponent(
+      'https://wso2.org/jenkins/view/All Builds/job/sonar/job/sonar-carbon-dashboards'
+    )}`
+  )
+
+t.create('old Cobertura prefix + job url in path')
   .get('/c/https/jenkins.sqlalchemy.org/job/alembic_coverage.svg', {
     followRedirect: false,
   })
   .expectStatus(301)
   .expectHeader(
     'Location',
-    '/jenkins/coverage/cobertura/https/jenkins.sqlalchemy.org/job/alembic_coverage.svg'
+    `/jenkins/coverage/cobertura.svg?jobUrl=${encodeURIComponent(
+      'https://jenkins.sqlalchemy.org/job/alembic_coverage'
+    )}`
+  )
+
+t.create('new Cobertura prefix + job url in path')
+  .get(
+    '/coverage/cobertura/https/jenkins.sqlalchemy.org/job/alembic_coverage.svg',
+    {
+      followRedirect: false,
+    }
+  )
+  .expectStatus(301)
+  .expectHeader(
+    'Location',
+    `/jenkins/coverage/cobertura.svg?jobUrl=${encodeURIComponent(
+      'https://jenkins.sqlalchemy.org/job/alembic_coverage'
+    )}`
+  )
+
+t.create('api prefix + job url in path')
+  .get(
+    '/coverage/api/https/jenkins.library.illinois.edu/job/OpenSourceProjects/job/Speedwagon/job/master.svg',
+    {
+      followRedirect: false,
+    }
+  )
+  .expectStatus(301)
+  .expectHeader(
+    'Location',
+    `/jenkins/coverage/api.svg?jobUrl=${encodeURIComponent(
+      'https://jenkins.library.illinois.edu/job/OpenSourceProjects/job/Speedwagon/job/master'
+    )}`
   )
diff --git a/services/jenkins/jenkins-coverage.tester.js b/services/jenkins/jenkins-coverage.tester.js
index 1394407e23..525187cd58 100644
--- a/services/jenkins/jenkins-coverage.tester.js
+++ b/services/jenkins/jenkins-coverage.tester.js
@@ -9,27 +9,37 @@ const t = (module.exports = require('../tester').createServiceTester())
 // https://wiki.jenkins.io/pages/viewpage.action?pageId=58001258
 
 t.create('jacoco: job found')
-  .get('/jacoco/https/wso2.org/jenkins/view/All%20Builds/job/archetypes.json')
+  .get(
+    `/jacoco.json?jobUrl=${encodeURIComponent(
+      'https://wso2.org/jenkins/view/All%20Builds/job/archetypes'
+    )}`
+  )
   .expectBadge({ label: 'coverage', message: isIntegerPercentage })
 
 t.create('jacoco: job not found')
-  .get('/jacoco/https/wso2.org/jenkins/job/does-not-exist.json')
+  .get('/jacoco.json?jobUrl=https://wso2.org/jenkins/job/does-not-exist')
   .expectBadge({ label: 'coverage', message: 'job or coverage not found' })
 
 t.create('cobertura: job not found')
-  .get('/cobertura/https/jenkins.sqlalchemy.org/job/does-not-exist.json')
+  .get(
+    '/cobertura.json?jobUrl=https://jenkins.sqlalchemy.org/job/does-not-exist'
+  )
   .expectBadge({ label: 'coverage', message: 'job or coverage not found' })
 
 t.create('cobertura: job found')
-  .get('/cobertura/https/jenkins.sqlalchemy.org/alembic_coverage.json')
+  .get(
+    '/cobertura.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage'
+  )
   .expectBadge({ label: 'coverage', message: isIntegerPercentage })
 
 t.create('code coverage API: job not found')
-  .get('/api/https/jenkins.library.illinois.edu/job/does-not-exist.json')
+  .get(
+    '/api.json?jobUrl=https://jenkins.library.illinois.edu/job/does-not-exist'
+  )
   .expectBadge({ label: 'coverage', message: 'job or coverage not found' })
 
 t.create('code coverage API: job found')
   .get(
-    '/api/https/jenkins.library.illinois.edu/job/OpenSourceProjects/job/Speedwagon/job/master.json'
+    '/api.json?jobUrl=https://jenkins.library.illinois.edu/job/OpenSourceProjects/job/Speedwagon/job/master'
   )
   .expectBadge({ label: 'coverage', message: isIntegerPercentage })
diff --git a/services/jenkins/jenkins-tests-redirector.service.js b/services/jenkins/jenkins-tests-redirector.service.js
index d0a5482381..57848ddca5 100644
--- a/services/jenkins/jenkins-tests-redirector.service.js
+++ b/services/jenkins/jenkins-tests-redirector.service.js
@@ -1,16 +1,31 @@
 'use strict'
 
+const { buildRedirectUrl } = require('./jenkins-common')
 const { redirector } = require('..')
 
+const commonProps = {
+  category: 'build',
+  transformPath: () => '/jenkins/tests',
+  transformQueryParams: ({ protocol, host, job }) => ({
+    jobUrl: buildRedirectUrl({ protocol, host, job }),
+  }),
+}
+
 module.exports = [
   redirector({
-    category: 'build',
     route: {
       base: 'jenkins/t',
       pattern: ':protocol(http|https)/:host/:job+',
     },
-    transformPath: ({ protocol, host, job }) =>
-      `/jenkins/tests/${protocol}/${host}/${job}`,
     dateAdded: new Date('2019-04-20'),
+    ...commonProps,
+  }),
+  redirector({
+    route: {
+      base: 'jenkins/tests',
+      pattern: ':protocol(http|https)/:host/:job+',
+    },
+    dateAdded: new Date('2019-11-29'),
+    ...commonProps,
   }),
 ]
diff --git a/services/jenkins/jenkins-tests-redirector.tester.js b/services/jenkins/jenkins-tests-redirector.tester.js
index 51c629aa3b..e0466c47ee 100644
--- a/services/jenkins/jenkins-tests-redirector.tester.js
+++ b/services/jenkins/jenkins-tests-redirector.tester.js
@@ -8,7 +8,7 @@ const t = (module.exports = new ServiceTester({
   pathPrefix: '/jenkins',
 }))
 
-t.create('Tests')
+t.create('old tests prefix + job url in path')
   .get(
     '/t/https/jenkins.qa.ubuntu.com/view/Trusty/view/Smoke Testing/job/trusty-touch-flo-smoke-daily.svg',
     {
@@ -18,5 +18,22 @@ t.create('Tests')
   .expectStatus(301)
   .expectHeader(
     'Location',
-    '/jenkins/tests/https/jenkins.qa.ubuntu.com/view/Trusty/view/Smoke Testing/job/trusty-touch-flo-smoke-daily.svg'
+    `/jenkins/tests.svg?jobUrl=${encodeURIComponent(
+      'https://jenkins.qa.ubuntu.com/view/Trusty/view/Smoke Testing/job/trusty-touch-flo-smoke-daily'
+    )}`
+  )
+
+t.create('new tests prefix + job url in path')
+  .get(
+    '/tests/https/jenkins.qa.ubuntu.com/view/Trusty/view/Smoke Testing/job/trusty-touch-flo-smoke-daily.svg',
+    {
+      followRedirect: false,
+    }
+  )
+  .expectStatus(301)
+  .expectHeader(
+    'Location',
+    `/jenkins/tests.svg?jobUrl=${encodeURIComponent(
+      'https://jenkins.qa.ubuntu.com/view/Trusty/view/Smoke Testing/job/trusty-touch-flo-smoke-daily'
+    )}`
   )
diff --git a/services/jenkins/jenkins-tests.service.js b/services/jenkins/jenkins-tests.service.js
index b18c0e7c49..47d5fc4f1f 100644
--- a/services/jenkins/jenkins-tests.service.js
+++ b/services/jenkins/jenkins-tests.service.js
@@ -42,8 +42,8 @@ module.exports = class JenkinsTests extends JenkinsBase {
 
   static get route() {
     return {
-      base: 'jenkins/tests',
-      pattern: ':protocol(http|https)/:host/:job+',
+      base: 'jenkins',
+      pattern: 'tests',
       queryParamSchema: queryParamSchema.concat(testResultQueryParamSchema),
     }
   }
@@ -52,16 +52,13 @@ module.exports = class JenkinsTests extends JenkinsBase {
     return [
       {
         title: 'Jenkins tests',
-        namedParams: {
-          protocol: 'https',
-          host: 'jenkins.sqlalchemy.org',
-          job: 'alembic_coverage',
-        },
+        namedParams: {},
         queryParams: {
           compact_message: null,
           passed_label: 'passed',
           failed_label: 'failed',
           skipped_label: 'skipped',
+          jobUrl: 'https://jenkins.sqlalchemy.org/job/alembic_coverage',
         },
         staticPreview: this.render({
           passed: 477,
@@ -120,9 +117,10 @@ module.exports = class JenkinsTests extends JenkinsBase {
   }
 
   async handle(
-    { protocol, host, job },
+    namedParams,
     {
       disableStrictSSL,
+      jobUrl,
       compact_message: compactMessage,
       passed_label: passedLabel,
       failed_label: failedLabel,
@@ -130,7 +128,7 @@ module.exports = class JenkinsTests extends JenkinsBase {
     }
   ) {
     const json = await this.fetch({
-      url: buildUrl({ protocol, host, job }),
+      url: buildUrl({ jobUrl }),
       schema,
       qs: buildTreeParamQueryString('actions[failCount,skipCount,totalCount]'),
       disableStrictSSL,
diff --git a/services/jenkins/jenkins-tests.tester.js b/services/jenkins/jenkins-tests.tester.js
index 1f44b9f9af..7b4f490ffb 100644
--- a/services/jenkins/jenkins-tests.tester.js
+++ b/services/jenkins/jenkins-tests.tester.js
@@ -14,43 +14,54 @@ const t = (module.exports = require('../tester').createServiceTester())
 // https://wiki.jenkins.io/pages/viewpage.action?pageId=58001258
 
 t.create('Test status')
-  .get('/https/jenkins.sqlalchemy.org/alembic_coverage.json')
+  .get('/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage')
   .expectBadge({ label: 'tests', message: isDefaultTestTotals })
 
 t.create('Test status with compact message')
-  .get('/https/jenkins.sqlalchemy.org/alembic_coverage.json', {
-    qs: { compact_message: null },
-  })
+  .get(
+    '/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
+    {
+      qs: { compact_message: null },
+    }
+  )
   .expectBadge({ label: 'tests', message: isDefaultCompactTestTotals })
 
 t.create('Test status with custom labels')
-  .get('/https/jenkins.sqlalchemy.org/alembic_coverage.json', {
-    qs: {
-      passed_label: 'good',
-      failed_label: 'bad',
-      skipped_label: 'n/a',
-    },
-  })
+  .get(
+    '/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
+    {
+      qs: {
+        passed_label: 'good',
+        failed_label: 'bad',
+        skipped_label: 'n/a',
+      },
+    }
+  )
   .expectBadge({ label: 'tests', message: isCustomTestTotals })
 
 t.create('Test status with compact message and custom labels')
-  .get('/https/jenkins.sqlalchemy.org/alembic_coverage.json', {
-    qs: {
-      compact_message: null,
-      passed_label: '💃',
-      failed_label: '🤦‍♀️',
-      skipped_label: '🤷',
-    },
-  })
+  .get(
+    '/tests.json?jobUrl=https://jenkins.sqlalchemy.org/job/alembic_coverage',
+    {
+      qs: {
+        compact_message: null,
+        passed_label: '💃',
+        failed_label: '🤦‍♀️',
+        skipped_label: '🤷',
+      },
+    }
+  )
   .expectBadge({
     label: 'tests',
     message: isCustomCompactTestTotals,
   })
 
 t.create('Test status on job with no tests')
-  .get('/https/ci.eclipse.org/openj9/job/Build-Doc-Push_to_ghpages.json')
+  .get(
+    '/tests.json?jobUrl=https://ci.eclipse.org/openj9/job/Build-Doc-Push_to_ghpages'
+  )
   .expectBadge({ label: 'tests', message: 'no tests found' })
 
 t.create('Test status on non-existent job')
-  .get('/https/ci.eclipse.org/openj9/job/does-not-exist.json')
+  .get('/tests.json?jobUrl=https://ci.eclipse.org/openj9/job/does-not-exist')
   .expectBadge({ label: 'tests', message: 'instance or job not found' })