Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Scoop] Added scoop-license badge. #10627

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions services/scoop/scoop-license.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { URL } from 'url'
import Joi from 'joi'
import { NotFound, pathParam, queryParam } from '../index.js'
import { ConditionalGithubAuthV3Service } from '../github/github-auth-service.js'
import { fetchJsonFromRepo } from '../github/github-common-fetch.js'
import { renderLicenseBadge } from '../licenses.js'
import toArray from '../../core/base-service/to-array.js'

const gitHubRepoRegExp =
/https:\/\/github.com\/(?<user>.*?)\/(?<repo>.*?)(\/|$)/

const bucketsSchema = Joi.object()
.pattern(/.+/, Joi.string().pattern(gitHubRepoRegExp).required())
.required()

const scoopSchema = Joi.object({
license: Joi.alternatives()
.try(
Joi.string().required(),
Joi.object({
identifier: Joi.string().required(),
}),
)
.required(),
}).required()

const queryParamSchema = Joi.object({
bucket: Joi.string(),
})

export default class ScoopLicense extends ConditionalGithubAuthV3Service {
// The buckets file (https://github.com/lukesampson/scoop/blob/master/buckets.json) changes very rarely.
// Cache it for the lifetime of the current Node.js process.
buckets = null

static category = 'license'

static route = {
base: 'scoop/l',
pattern: ':app',
queryParamSchema,
}

static openApi = {
'/scoop/l/{app}': {
get: {
summary: 'Scoop License',
description:
'[Scoop](https://scoop.sh/) is a command-line installer for Windows',
parameters: [
pathParam({ name: 'app', example: 'ngrok' }),
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',
}),
],
},
},
}

static defaultBadgeData = { label: 'license' }

static render({ licenses }) {
return renderLicenseBadge({ licenses })
}

async handle({ app }, queryParams) {
if (!this.buckets) {
this.buckets = await fetchJsonFromRepo(this, {
schema: bucketsSchema,
user: 'ScoopInstaller',
repo: 'Scoop',
branch: 'master',
filename: 'buckets.json',
})
}

const bucket = queryParams.bucket || 'main'
let bucketUrl = this.buckets[bucket]

if (!bucketUrl) {
// 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 },
} = gitHubRepoRegExp.exec(bucketUrl)

try {
const { license } = await fetchJsonFromRepo(this, {
schema: scoopSchema,
user,
repo,
branch: 'master',
filename: `bucket/${app}.json`,
})

const licenses = toArray(license).map(license =>
typeof license === 'string' ? license : license.identifier,
)
return this.constructor.render({ licenses })
} catch (error) {
if (error instanceof NotFound) {
throw new NotFound({
prettyMessage: `${app} not found in bucket "${bucket}"`,
})
}
throw error
}
}
}
97 changes: 97 additions & 0 deletions services/scoop/scoop-license.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ServiceTester } from '../tester.js'

export const t = new ServiceTester({
id: 'scoop',
title: 'Scoop License',
})

t.create('License (valid) - with nested response')
.get('/l/ngrok.json')
.expectBadge({
label: 'license',
message: 'Shareware',
})

t.create('License (valid) - with string response')
.get('/l/nvs.json')
.expectBadge({
label: 'license',
message: 'MIT',
})

t.create('License (invalid)').get('/l/not-a-real-app.json').expectBadge({
label: 'license',
message: 'not-a-real-app not found in bucket "main"',
})

t.create('License (valid custom bucket)')
.get('/l/atom.json?bucket=extras')
.expectBadge({
label: 'license',
message: 'MIT',
})

t.create('license (not found in custom bucket)')
.get('/l/not-a-real-app.json?bucket=extras')
.expectBadge({
label: 'license',
message: 'not-a-real-app not found in bucket "extras"',
})

t.create('license (wrong bucket)')
.get('/l/not-a-real-app.json?bucket=not-a-real-bucket')
.expectBadge({
label: 'license',
message: 'bucket "not-a-real-bucket" not found',
})

// version (bucket url)
const validBucketUrl = encodeURIComponent(
'https://github.com/jewlexx/personal-scoop',
)

t.create('license (valid bucket url)')
.get(`/l/sfsu.json?bucket=${validBucketUrl}`)
.expectBadge({
label: 'license',
message: 'Apache-2.0',
})

const validBucketUrlTrailingSlash = encodeURIComponent(
'https://github.com/jewlexx/personal-scoop/',
)

t.create('license (valid bucket url)')
.get(`/l/sfsu.json?bucket=${validBucketUrlTrailingSlash}`)
.expectBadge({
label: 'license',
message: 'Apache-2.0',
})

t.create('license (not found in custom bucket)')
.get(`/l/not-a-real-app.json?bucket=${validBucketUrl}`)
.expectBadge({
label: 'license',
message: `not-a-real-app not found in bucket "${decodeURIComponent(validBucketUrl)}"`,
})

const nonGithubUrl = encodeURIComponent('https://example.com/')

t.create('license (non-github url)')
.get(`/l/not-a-real-app.json?bucket=${nonGithubUrl}`)
.expectBadge({
label: 'license',
message: `bucket "${decodeURIComponent(nonGithubUrl)}" not found`,
})

const nonBucketRepo = encodeURIComponent('https://github.com/jewlexx/sfsu')

t.create('version (non-bucket repo)')
.get(`/l/sfsu.json?bucket=${nonBucketRepo}`)
.expectBadge({
label: 'license',
// !!! 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)}"`,
})
Loading