diff --git a/services/scoop/scoop-version.service.js b/services/scoop/scoop-version.service.js index 72fbf46714..7b4d806750 100644 --- a/services/scoop/scoop-version.service.js +++ b/services/scoop/scoop-version.service.js @@ -1,3 +1,4 @@ +import { URL } from 'url' import Joi from 'joi' import { NotFound, pathParam, queryParam } from '../index.js' import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js' @@ -37,7 +38,12 @@ export default class ScoopVersion extends ConditionalGithubAuthV3Service { '[Scoop](https://scoop.sh/) is a command-line installer for Windows', parameters: [ pathParam({ name: 'app', example: 'ngrok' }), - queryParam({ name: 'bucket', example: 'extras' }), + queryParam({ + name: 'bucket', + description: + "App's containing bucket. Can either be a name (e.g `extras`) or a URL to a GitHub Repo (e.g `https://github.com/jewlexx/personal-scoop`)", + example: 'extras', + }), ], }, }, @@ -60,9 +66,30 @@ export default class ScoopVersion extends ConditionalGithubAuthV3Service { }) } const bucket = queryParams.bucket || 'main' - const bucketUrl = this.buckets[bucket] + let bucketUrl = this.buckets[bucket] if (!bucketUrl) { - throw new NotFound({ prettyMessage: `bucket "${bucket}" not found` }) + // Parsing URL here will throw an error if the url is invalid + try { + const url = new URL(decodeURIComponent(bucket)) + + // Throw errors to go to jump to catch statement + // The error messages here are purely for code readability, and will never reach the user. + if (url.hostname !== 'github.com') { + throw new Error('Not a GitHub URL') + } + const path = url.pathname.split('/').filter(value => value !== '') + + if (path.length !== 2) { + throw new Error('Not a valid GitHub Repo') + } + + const [user, repo] = path + + // Reconstructing the url here ensures that the url will match the regex + bucketUrl = `https://github.com/${user}/${repo}` + } catch (e) { + throw new NotFound({ prettyMessage: `bucket "${bucket}" not found` }) + } } const { groups: { user, repo }, diff --git a/services/scoop/scoop-version.tester.js b/services/scoop/scoop-version.tester.js index 6e3950e8a5..8c25e03ccf 100644 --- a/services/scoop/scoop-version.tester.js +++ b/services/scoop/scoop-version.tester.js @@ -40,3 +40,54 @@ t.create('version (wrong bucket)') label: 'scoop', message: 'bucket "not-a-real-bucket" not found', }) + +// version (bucket url) +const validBucketUrl = encodeURIComponent( + 'https://github.com/jewlexx/personal-scoop', +) + +t.create('version (valid bucket url)') + .get(`/v/sfsu.json?bucket=${validBucketUrl}`) + .expectBadge({ + label: 'scoop', + message: isVPlusDottedVersionNClauses, + }) + +const validBucketUrlTrailingSlash = encodeURIComponent( + 'https://github.com/jewlexx/personal-scoop/', +) + +t.create('version (valid bucket url)') + .get(`/v/sfsu.json?bucket=${validBucketUrlTrailingSlash}`) + .expectBadge({ + label: 'scoop', + message: isVPlusDottedVersionNClauses, + }) + +t.create('version (not found in custom bucket)') + .get(`/v/not-a-real-app.json?bucket=${validBucketUrl}`) + .expectBadge({ + label: 'scoop', + message: `not-a-real-app not found in bucket "${decodeURIComponent(validBucketUrl)}"`, + }) + +const nonGithubUrl = encodeURIComponent('https://example.com/') + +t.create('version (non-github url)') + .get(`/v/not-a-real-app.json?bucket=${nonGithubUrl}`) + .expectBadge({ + label: 'scoop', + message: `bucket "${decodeURIComponent(nonGithubUrl)}" not found`, + }) + +const nonBucketRepo = encodeURIComponent('https://github.com/jewlexx/sfsu') + +t.create('version (non-bucket repo)') + .get(`/v/sfsu.json?bucket=${nonBucketRepo}`) + .expectBadge({ + label: 'scoop', + // !!! Important note here + // It is hard to tell if a repo is actually a scoop bucket, without getting the contents + // As such, a helpful error message here, which would require testing if the url is a valid scoop bucket, is difficult. + message: `sfsu not found in bucket "${decodeURIComponent(nonBucketRepo)}"`, + })