diff --git a/services/snapcraft/snapcraft-version.service.js b/services/snapcraft/snapcraft-version.service.js new file mode 100644 index 0000000000000..1d18f60449bcb --- /dev/null +++ b/services/snapcraft/snapcraft-version.service.js @@ -0,0 +1,98 @@ +import Joi from 'joi' +import { BaseJsonService, NotFound, pathParams, queryParam } from '../index.js' +import { renderVersionBadge } from '../version.js' + +const queryParamSchema = Joi.object({ + arch: Joi.string(), +}) + +const versionSchema = Joi.object({ + 'channel-map': Joi.array() + .items( + Joi.object({ + channel: Joi.object({ + architecture: Joi.string().required(), + risk: Joi.string().required(), + track: Joi.string().required(), + }), + version: Joi.string().required(), + }).required(), + ) + .min(1) + .required(), +}).required() + +export default class SnapcraftVersion extends BaseJsonService { + static category = 'version' + + static route = { + base: 'snapcraft/v', + pattern: ':package/:track/:risk', + queryParamSchema, + } + + static defaultBadgeData = { label: 'snapcraft' } + + static openApi = { + '/snapcraft/v/{package}/{track}/{risk}': { + get: { + summary: 'Snapcraft version', + parameters: [ + ...pathParams( + { name: 'package', example: 'chromium' }, + { name: 'track', example: 'latest' }, + { name: 'risk', example: 'stable' }, + ), + queryParam({ + name: 'arch', + example: 'amd64', + description: + 'Architecture, When not specified, this will default to `amd64`.', + }), + ], + }, + }, + } + + transform(apiData, track, risk, arch) { + const channelMap = apiData['channel-map'] + let filteredChannelMap = channelMap.filter( + ({ channel }) => channel.architecture === arch, + ) + if (filteredChannelMap.length === 0) { + throw new NotFound({ prettyMessage: 'arch not found' }) + } + filteredChannelMap = filteredChannelMap.filter( + ({ channel }) => channel.track === track, + ) + if (filteredChannelMap.length === 0) { + throw new NotFound({ prettyMessage: 'track not found' }) + } + filteredChannelMap = filteredChannelMap.filter( + ({ channel }) => channel.risk === risk, + ) + if (filteredChannelMap.length === 0) { + throw new NotFound({ prettyMessage: 'risk not found' }) + } + + return filteredChannelMap[0] + } + + async handle({ package: packageName, track, risk }, { arch = 'amd64' }) { + const parsedData = await this._requestJson({ + schema: versionSchema, + options: { + headers: { 'Snap-Device-Series': 16 }, + }, + url: `https://api.snapcraft.io/v2/snaps/info/${packageName}`, + httpErrors: { + 404: 'package not found', + }, + }) + + // filter results by track, risk and arch + const { version } = this.transform(parsedData, track, risk, arch) + + return renderVersionBadge({ version }) + } +} diff --git a/services/snapcraft/snapcraft-version.spec.js b/services/snapcraft/snapcraft-version.spec.js new file mode 100644 index 0000000000000..a0ef7a31c7c4a --- /dev/null +++ b/services/snapcraft/snapcraft-version.spec.js @@ -0,0 +1,103 @@ +import { expect } from 'chai' +import { test, given } from 'sazerac' +import _ from 'lodash' +import { NotFound } from '../index.js' +import SnapcraftVersion from './snapcraft-version.service.js' + +describe('SnapcraftVersion', function () { + const exampleChannel = { + channel: { + architecture: 'amd64', + risk: 'stable', + track: 'latest', + }, + version: '1.2.3', + } + const exampleArchChange = _.merge(_.cloneDeep(exampleChannel), { + channel: { architecture: 'arm64' }, + version: '2.3.4', + }) + const exampleTrackChange = _.merge(_.cloneDeep(exampleChannel), { + channel: { track: 'beta' }, + version: '3.4.5', + }) + const exampleRiskChange = _.merge(_.cloneDeep(exampleChannel), { + channel: { risk: 'edge' }, + version: '5.4.6', + }) + const testApiData = { + 'channel-map': [ + exampleChannel, + exampleArchChange, + exampleTrackChange, + exampleRiskChange, + ], + } + + test(SnapcraftVersion.prototype.transform, () => { + given( + testApiData, + exampleChannel.channel.track, + exampleChannel.channel.risk, + exampleChannel.channel.architecture, + ).expect(exampleChannel) + // change arch + given( + testApiData, + exampleChannel.channel.track, + exampleChannel.channel.risk, + exampleArchChange.channel.architecture, + ).expect(exampleArchChange) + // change track + given( + testApiData, + exampleTrackChange.channel.track, + exampleChannel.channel.risk, + exampleChannel.channel.architecture, + ).expect(exampleTrackChange) + // change risk + given( + testApiData, + exampleChannel.channel.track, + exampleRiskChange.channel.risk, + exampleChannel.channel.architecture, + ).expect(exampleRiskChange) + }) + + it('throws NotFound error with missing arch', function () { + expect(() => { + SnapcraftVersion.prototype.transform( + testApiData, + exampleChannel.channel.track, + exampleChannel.channel.risk, + 'missing', + ) + }) + .to.throw(NotFound) + .with.property('prettyMessage', 'arch not found') + }) + it('throws NotFound error with missing track', function () { + expect(() => { + SnapcraftVersion.prototype.transform( + testApiData, + 'missing', + exampleChannel.channel.risk, + exampleChannel.channel.architecture, + ) + }) + .to.throw(NotFound) + .with.property('prettyMessage', 'track not found') + }) + it('throws NotFound error with missing risk', function () { + expect(() => { + SnapcraftVersion.prototype.transform( + testApiData, + exampleChannel.channel.track, + 'missing', + exampleChannel.channel.architecture, + ) + }) + .to.throw(NotFound) + .with.property('prettyMessage', 'risk not found') + }) +}) diff --git a/services/snapcraft/snapcraft-version.tester.js b/services/snapcraft/snapcraft-version.tester.js new file mode 100644 index 0000000000000..dc4a5001b66e4 --- /dev/null +++ b/services/snapcraft/snapcraft-version.tester.js @@ -0,0 +1,45 @@ +import { isSemver } from '../test-validators.js' +import { createServiceTester } from '../tester.js' +export const t = await createServiceTester() + +t.create('Snapcraft Version for redis') + .get('/redis/latest/stable.json') + .expectBadge({ + label: 'snapcraft', + message: isSemver, + }) + +t.create('Snapcraft Version for redis (query param arch=arm64)') + .get('/redis/latest/stable.json?arch=arm64') + .expectBadge({ + label: 'snapcraft', + message: isSemver, + }) + +t.create('Snapcraft Version for redis (invalid package)') + .get('/this_package_doesnt_exist/fake/fake.json') + .expectBadge({ + label: 'snapcraft', + message: 'package not found', + }) + +t.create('Snapcraft Version for redis (invalid track)') + .get('/redis/notfound/stable.json') + .expectBadge({ + label: 'snapcraft', + message: 'track not found', + }) + +t.create('Snapcraft Version for redis (invalid risk)') + .get('/redis/latest/notfound.json') + .expectBadge({ + label: 'snapcraft', + message: 'risk not found', + }) + +t.create('Snapcraft Version for redis (invalid arch)') + .get('/redis/latest/stable.json?arch=fake') + .expectBadge({ + label: 'snapcraft', + message: 'arch not found', + })