diff --git a/services/nexus/nexus.service.js b/services/nexus/nexus.service.js index 955899899b..f232ec8133 100644 --- a/services/nexus/nexus.service.js +++ b/services/nexus/nexus.service.js @@ -16,7 +16,12 @@ const searchApiSchema = Joi.object({ Joi.object({ latestRelease: optionalDottedVersionNClausesWithOptionalSuffix, latestSnapshot: optionalDottedVersionNClausesWithOptionalSuffix, - version: optionalDottedVersionNClausesWithOptionalSuffix, + // `version` will almost always follow the same pattern as optionalDottedVersionNClausesWithOptionalSuffix. + // However, there are a couple exceptions where `version` may be a simple string (like `android-SNAPSHOT`) + // This schema is relaxed accordingly since for snapshot/release badges the schema has to validate + // the entire history of each published version for the artifact. + // Example artifact that includes such a historical version: https://oss.sonatype.org/service/local/lucene/search?g=com.google.guava&a=guava + version: Joi.string(), }) ) .required(), @@ -46,24 +51,23 @@ module.exports = class Nexus extends BaseJsonService { base: 'nexus', // API pattern: // /nexus/(r|s|)/(http|https)/[:port][/]//[:k1=v1[:k2=v2[...]]] - format: - '(r|s|[^/]+)/(https?)/((?:[^/]+)(?:/[^/]+)?)/([^/]+)/([^/:]+)(:.+)?', - capture: ['repo', 'scheme', 'host', 'groupId', 'artifactId', 'queryOpt'], + pattern: + ':repo(r|s|[^/]+)/:scheme(http|https)/:hostAndPath+/:groupId/:artifactId([^/:]+):queryOpt(:.+)?', } } static get defaultBadgeData() { - return { color: 'blue', label: 'nexus' } + return { label: 'nexus' } } static get examples() { return [ { title: 'Sonatype Nexus (Releases)', - pattern: 'r/:scheme/:host/:groupId/:artifactId', + pattern: 'r/:scheme(http|https)/:hostAndPath/:groupId/:artifactId', namedParams: { scheme: 'https', - host: 'oss.sonatype.org', + hostAndPath: 'oss.sonatype.org', groupId: 'com.google.guava', artifactId: 'guava', }, @@ -73,10 +77,10 @@ module.exports = class Nexus extends BaseJsonService { }, { title: 'Sonatype Nexus (Snapshots)', - pattern: 's/:scheme/:host/:groupId/:artifactId', + pattern: 's/:scheme(http|https)/:hostAndPath/:groupId/:artifactId', namedParams: { scheme: 'https', - host: 'oss.sonatype.org', + hostAndPath: 'oss.sonatype.org', groupId: 'com.google.guava', artifactId: 'guava', }, @@ -86,11 +90,11 @@ module.exports = class Nexus extends BaseJsonService { }, { title: 'Sonatype Nexus (Repository)', - pattern: ':repo/:scheme/:host/:groupId/:artifactId', + pattern: ':repo/:scheme(http|https)/:hostAndPath/:groupId/:artifactId', namedParams: { repo: 'developer', scheme: 'https', - host: 'repository.jboss.org/nexus', + hostAndPath: 'repository.jboss.org/nexus', groupId: 'ai.h2o', artifactId: 'h2o-automl', }, @@ -100,11 +104,12 @@ module.exports = class Nexus extends BaseJsonService { }, { title: 'Sonatype Nexus (Query Options)', - pattern: ':repo/:scheme/:host/:groupId/:artifactId/:queryOpt', + pattern: + ':repo/:scheme(http|https)/:hostAndPath/:groupId/:artifactId/:queryOpt', namedParams: { repo: 'fs-public-snapshots', scheme: 'https', - host: 'repository.jboss.org/nexus', + hostAndPath: 'repository.jboss.org/nexus', groupId: 'com.progress.fuse', artifactId: 'fusehq', queryOpt: ':c=agent-apple-osx:p=tar.gz', @@ -114,10 +119,10 @@ module.exports = class Nexus extends BaseJsonService { }), documentation: `

- Note that you can use query options with any Nexus badge type (Releases, Snapshots, or Repository) + Note that you can use query options with any Nexus badge type (Releases, Snapshots, or Repository).

- Query options should be provided as key=value pairs separated by a semicolon + Query options should be provided as key=value pairs separated by a colon.

`, }, @@ -125,8 +130,15 @@ module.exports = class Nexus extends BaseJsonService { } transform({ repo, json }) { + if (json.data.length === 0) { + throw new NotFound({ prettyMessage: 'artifact or version not found' }) + } if (repo === 'r') { - return { version: json.data[0].latestRelease } + const version = json.data[0].latestRelease + if (!version) { + throw new InvalidResponse({ prettyMessage: 'invalid artifact version' }) + } + return { version } } else if (repo === 's') { // only want to match 1.2.3-SNAPSHOT style versions, which may not always be in // 'latestSnapshot' so check 'version' as well before continuing to next entry @@ -140,32 +152,31 @@ module.exports = class Nexus extends BaseJsonService { } throw new InvalidResponse({ prettyMessage: 'no snapshot versions found' }) } else { - return { version: json.data.baseVersion || json.data.version } + const version = json.data.baseVersion || json.data.version + if (!version) { + throw new InvalidResponse({ prettyMessage: 'invalid artifact version' }) + } + return { version } } } - async handle({ repo, scheme, host, groupId, artifactId, queryOpt }) { + async handle({ repo, scheme, hostAndPath, groupId, artifactId, queryOpt }) { const { json } = await this.fetch({ repo, scheme, - host, + hostAndPath, groupId, artifactId, queryOpt, }) - if (json.data.length === 0) { - throw new NotFound({ prettyMessage: 'artifact or version not found' }) - } + const { version } = this.transform({ repo, json }) - if (!version) { - throw new InvalidResponse({ prettyMessage: 'invalid artifact version' }) - } return this.constructor.render({ version }) } addQueryParamsToQueryString({ qs, queryOpt }) { // Users specify query options with 'key=value' pairs, using a - // semicolon delimiter between pairs ([:k1=v1[:k2=v2[...]]]). + // colon delimiter between pairs ([:k1=v1[:k2=v2[...]]]). // queryOpt will be a string containing those key/value pairs, // For example: :c=agent-apple-osx:p=tar.gz const keyValuePairs = queryOpt.split(':') @@ -177,13 +188,13 @@ module.exports = class Nexus extends BaseJsonService { }) } - async fetch({ repo, scheme, host, groupId, artifactId, queryOpt }) { + async fetch({ repo, scheme, hostAndPath, groupId, artifactId, queryOpt }) { const qs = { g: groupId, a: artifactId, } let schema - let url = `${scheme}://${host}/` + let url = `${scheme}://${hostAndPath}/` // API pattern: // for /nexus/[rs]/... pattern, use the search api of the nexus server, and // for /nexus//... pattern, use the resolve api of the nexus server. diff --git a/services/nexus/nexus.spec.js b/services/nexus/nexus.spec.js new file mode 100644 index 0000000000..cfa665cf16 --- /dev/null +++ b/services/nexus/nexus.spec.js @@ -0,0 +1,71 @@ +'use strict' + +const { expect } = require('chai') +const { InvalidResponse, NotFound } = require('..') +const Nexus = require('./nexus.service') + +describe('Nexus', function() { + context('transform()', function() { + it('throws NotFound error when no versions exist', function() { + try { + Nexus.prototype.transform({ json: { data: [] } }) + expect.fail('Expected to throw') + } catch (e) { + expect(e).to.be.an.instanceof(NotFound) + expect(e.prettyMessage).to.equal('artifact or version not found') + } + }) + + it('throws InvalidResponse error when no there is no latestRelease version', function() { + try { + Nexus.prototype.transform({ repo: 'r', json: { data: [{}] } }) + expect.fail('Expected to throw') + } catch (e) { + expect(e).to.be.an.instanceof(InvalidResponse) + expect(e.prettyMessage).to.equal('invalid artifact version') + } + }) + + it('returns latestSnapshot value', function() { + const latestSnapshot = '7.0.1-SNAPSHOT' + const { version } = Nexus.prototype.transform({ + repo: 's', + json: { + data: [{ latestSnapshot }, { version: '1.2.3' }], + }, + }) + expect(version).to.equal(latestSnapshot) + }) + + it('returns version value when it is a snapshot', function() { + const latestSnapshot = '1.2.7-SNAPSHOT' + const { version } = Nexus.prototype.transform({ + repo: 's', + json: { + data: [{ latestSnapshot: '1.2.3' }, { version: latestSnapshot }], + }, + }) + expect(version).to.equal(latestSnapshot) + }) + + it('throws InvalidResponse error when no snapshot versions exist', function() { + try { + Nexus.prototype.transform({ repo: 's', json: { data: [{}] } }) + expect.fail('Expected to throw') + } catch (e) { + expect(e).to.be.an.instanceof(InvalidResponse) + expect(e.prettyMessage).to.equal('no snapshot versions found') + } + }) + + it('throws InvalidResponse error when repository has no version data', function() { + try { + Nexus.prototype.transform({ repo: 'developer', json: { data: {} } }) + expect.fail('Expected to throw') + } catch (e) { + expect(e).to.be.an.instanceof(InvalidResponse) + expect(e.prettyMessage).to.equal('invalid artifact version') + } + }) + }) +}) diff --git a/services/nexus/nexus.tester.js b/services/nexus/nexus.tester.js index 5916cf7c29..3b4ea29a93 100644 --- a/services/nexus/nexus.tester.js +++ b/services/nexus/nexus.tester.js @@ -18,32 +18,36 @@ function mockNexusCreds() { } t.create('live: search release version valid artifact') - .get('/r/https/repository.jboss.org/nexus/jboss/jboss-client.json') + .timeout(15000) + .get('/r/https/oss.sonatype.org/com.google.guava/guava.json') .expectBadge({ label: 'nexus', message: isVersion, }) -t.create('live: search release version of an inexistent artifact') - .get('/r/https/repository.jboss.org/nexus/jboss/inexistent-artifact-id.json') +t.create('live: search release version of an nonexistent artifact') + .timeout(10000) + .get( + '/r/https/oss.sonatype.org/com.google.guava/nonexistent-artifact-id.json' + ) .expectBadge({ label: 'nexus', message: 'artifact or version not found', }) t.create('live: search snapshot version valid snapshot artifact') - .get('/s/https/repository.jboss.org/nexus/com.progress.fuse/fusehq.json') + .timeout(10000) + .get('/s/https/oss.sonatype.org/com.google.guava/guava.json') .expectBadge({ label: 'nexus', message: isVersion, }) -t.create('live: search snapshot version of a release artifact') - .get('/s/https/repository.jboss.org/nexus/jboss/jboss-client.json') - .expectBadge({ label: 'nexus', message: 'no snapshot versions found' }) - -t.create('live: search snapshot version of an inexistent artifact') - .get('/s/https/repository.jboss.org/nexus/jboss/inexistent-artifact-id.json') +t.create('live: search snapshot version of an nonexistent artifact') + .timeout(10000) + .get( + '/s/https/oss.sonatype.org/com.google.guava/nonexistent-artifact-id.json' + ) .expectBadge({ label: 'nexus', message: 'artifact or version not found', @@ -66,9 +70,9 @@ t.create('live: repository version with query') message: isVersion, }) -t.create('live: repository version of an inexistent artifact') +t.create('live: repository version of an nonexistent artifact') .get( - '/developer/https/repository.jboss.org/nexus/jboss/inexistent-artifact-id.json' + '/developer/https/repository.jboss.org/nexus/jboss/nonexistent-artifact-id.json' ) .expectBadge({ label: 'nexus',