From c6be456d9cbf67f5e242f7cf0017c88febe713ee Mon Sep 17 00:00:00 2001 From: jNullj <15849761+jNullj@users.noreply.github.com> Date: Sun, 10 Mar 2024 12:12:58 +0200 Subject: [PATCH] Add [snapcraft] version badge (#9976) * Add snapstore version badge Fixes badges/shields#9103 * Add basic test for Snapcraft version * Handle snapstore-version - package not found * Add test for invalid snapcraft package * Remove redundent SnapstoreBase class not needed as there is no auth * Remove logo from default The project convention is using namedLogo by default only for social badges. This commit removes the default logo usage as this badge is not in the social category. * Rename snapstore to snapcraft Keep 1 convention for the snapcraft badge, use only snapcraft and ditch snapstore. * Fix SnapcraftVersion schema * Use renderVersionBadge Replace the costume render in SnapcraftVersion with renderVersionBadge * Rename folder from snapstore to snapcraft * Add track & risk path parameters for SnapcraftVersion enhancing control and clarity * Add architecture query parameter to SnapcraftVersion Added architecture query parameter: The snapcraft-version.service.js file now accepts an optional arch query parameter to specify the desired architecture for the Snap package. This defaults to amd64 if not provided. If an unsupported architecture is specified in the query parameter, a NotFound error is thrown with a specific message indicating that the requested architecture is not found. The snapcraft-version.tester.js file is updated to include a new test case that verifies the behavior when using the arch query parameter and also includes a test case for handling an invalid architecture. * move filter logic into a transform function * Fix filter logic The goal here was to filter by all conditions with logic and. Before this fix the only the logic of the last filter is used. * Add tests for SnapcraftVersion.transform --- .../snapcraft/snapcraft-version.service.js | 98 +++++++++++++++++ services/snapcraft/snapcraft-version.spec.js | 103 ++++++++++++++++++ .../snapcraft/snapcraft-version.tester.js | 45 ++++++++ 3 files changed, 246 insertions(+) create mode 100644 services/snapcraft/snapcraft-version.service.js create mode 100644 services/snapcraft/snapcraft-version.spec.js create mode 100644 services/snapcraft/snapcraft-version.tester.js 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', + })