diff --git a/services/gerrit/gerrit.service.js b/services/gerrit/gerrit.service.js new file mode 100644 index 0000000000..69ceec4035 --- /dev/null +++ b/services/gerrit/gerrit.service.js @@ -0,0 +1,96 @@ +'use strict' + +const Joi = require('@hapi/joi') +const { optionalUrl } = require('../validators') +const { BaseJsonService } = require('..') + +const queryParamSchema = Joi.object({ + baseUrl: optionalUrl.required(), +}).required() + +const schema = Joi.object({ + status: Joi.equal('NEW', 'MERGED', 'ABANDONED').required(), +}).required() + +module.exports = class Gerrit extends BaseJsonService { + static get category() { + return 'issue-tracking' + } + + static get route() { + return { + base: 'gerrit', + pattern: ':changeId', + queryParamSchema, + } + } + + static get examples() { + return [ + { + title: 'Gerrit change status', + namedParams: { + changeId: '1011478', + }, + queryParams: { baseUrl: 'https://android-review.googlesource.com' }, + staticPreview: this.render({ + changeId: 1011478, + status: 'MERGED', + }), + }, + ] + } + + static get defaultBadgeData() { + return { label: 'gerrit' } + } + + static getColor({ displayStatus }) { + if (displayStatus === 'new') { + return '2cbe4e' + } else if (displayStatus === 'merged') { + return 'blueviolet' + } else if (displayStatus === 'abandoned') { + return 'red' + } + } + + static render({ changeId, status }) { + const displayStatus = status.toLowerCase() + const color = this.getColor({ displayStatus }) + return { + label: `change ${changeId}`, + message: displayStatus, + color, + } + } + + /** + * To prevent against Cross Site Script Inclusion (XSSI) attacks, Gerrit's + * JSON response body starts with a magic prefix line that must be stripped + * before feeding the rest of the response body to a JSON parser. + * See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output + */ + _parseJson(buffer) { + const bufferWithoutPrefixLine = buffer.substring(buffer.indexOf('\n') + 1) + return super._parseJson(bufferWithoutPrefixLine) + } + + async fetch({ changeId, baseUrl }) { + return this._requestJson({ + schema, + url: `${baseUrl}/changes/${changeId}`, + errorMessages: { + 404: 'change not found', + }, + }) + } + + async handle({ changeId }, { baseUrl }) { + const data = await this.fetch({ changeId, baseUrl }) + return this.constructor.render({ + changeId, + status: data.status, + }) + } +} diff --git a/services/gerrit/gerrit.tester.js b/services/gerrit/gerrit.tester.js new file mode 100644 index 0000000000..b4468af421 --- /dev/null +++ b/services/gerrit/gerrit.tester.js @@ -0,0 +1,42 @@ +'use strict' + +const t = (module.exports = require('../tester').createServiceTester()) + +// Change open since December 2010, hopefully won't get merged or abandoned anytime soon. +t.create('Gerrit new change') + .get('/2013.json?baseUrl=https://git.eclipse.org/r') + .expectBadge({ + label: 'change 2013', + message: 'new', + color: '#2cbe4e', + }) + +t.create('Gerrit merged change') + .get('/1011478.json?baseUrl=https://android-review.googlesource.com') + .expectBadge({ + label: 'change 1011478', + message: 'merged', + color: 'blueviolet', + }) + +t.create('Gerrit abandoned change') + .get('/69361.json?baseUrl=https://gerrit.libreoffice.org') + .expectBadge({ + label: 'change 69361', + message: 'abandoned', + color: 'red', + }) + +t.create('Gerrit change not found') + .get('/1000000000.json?baseUrl=https://chromium-review.googlesource.com') + .expectBadge({ + label: 'gerrit', + message: 'change not found', + }) + +t.create('Gerrit invalid baseUrl') + .get('/123.json?baseUrl=something') + .expectBadge({ + label: 'gerrit', + message: 'invalid query parameter: baseUrl', + })