[Packagist] dependency version (#8371)

* happy path is done: packagist depenency version is returned for dependencyVendor and dependencyRepo

* changes in error handling in packagist dependency version service

* Move to packagist-base common parts of packagist-php and packagist-dep-ver

* the label now shows the name of the dependency

* add comments,slightly modify if statement

* fix unit tests in  services/packagist/packagist-php-version.spec.js

* add unit tests for services/packagist/packagist-dependency-version.js

* service tests for packagist-dependency-version.service.js are in progress

* Add additional service tests for packagist-dependency-version.tester.js

* remove toLowerCase()

* resolve from lgtm: 1 for Wrong use of 'this' for static method

* DRY determining the name of the dependency (depVen/depRepo)

* url change in strategy is in progress

* basic examples of packagist/dependency-v work with new url; packagist/php-v redirects to the new url

* service tests should be green

* unit tests for packagist should be fixed

* add toLowerCase()

* part of suggestions from the PR is implemented

* updated tests for packagist dependency version

* fix packagist dependency version spec

* add missing unit tests

* Update services/packagist/packagist-dependency-version.spec.js

Co-authored-by: chris48s <chris48s@users.noreply.github.com>

* Update services/packagist/packagist-dependency-version.spec.js

Co-authored-by: chris48s <chris48s@users.noreply.github.com>

Co-authored-by: chris48s <chris48s@users.noreply.github.com>
This commit is contained in:
Paula Barszcz
2022-09-14 22:12:09 +02:00
committed by GitHub
parent e3c938b4d7
commit 93fa955dde
8 changed files with 387 additions and 290 deletions

View File

@@ -6,9 +6,7 @@ const packageSchema = Joi.array().items(
Joi.object({
version: Joi.string().required(),
require: Joi.alternatives(
Joi.object({
php: Joi.string(),
}).required(),
Joi.object().pattern(Joi.string(), Joi.string()).required(),
Joi.string().valid('__unset')
),
})
@@ -23,7 +21,7 @@ class BasePackagistService extends BaseJsonService {
/**
* Default fetch method.
*
* This method utilize composer metadata API which
* This method utilizes composer metadata API which
* "... is the preferred way to access the data as it is always up to date,
* and dumped to static files so it is very efficient on our end." (comment from official documentation).
* For more information please refer to https://packagist.org/apidoc#get-package-data.
@@ -47,7 +45,7 @@ class BasePackagistService extends BaseJsonService {
/**
* Fetch dev releases method.
*
* This method utilize composer metadata API which
* This method utilizes composer metadata API which
* "... is the preferred way to access the data as it is always up to date,
* and dumped to static files so it is very efficient on our end." (comment from official documentation).
* For more information please refer to https://packagist.org/apidoc#get-package-data.
@@ -166,7 +164,6 @@ class BasePackagistService extends BaseJsonService {
return versions.filter(version => version.version === release)[0]
}
}
const customServerDocumentationFragment = `
<p>
Note that only network-accessible packagist.org and other self-hosted Packagist instances are supported.

View File

@@ -0,0 +1,200 @@
import Joi from 'joi'
import { optionalUrl } from '../validators.js'
import { NotFound } from '../index.js'
import {
allVersionsSchema,
BasePackagistService,
customServerDocumentationFragment,
} from './packagist-base.js'
const queryParamSchema = Joi.object({
server: optionalUrl,
version: Joi.string(),
}).required()
export default class PackagistDependencyVersion extends BasePackagistService {
static category = 'platform-support'
static route = {
base: 'packagist/dependency-v',
pattern: ':user/:repo/:dependency+',
queryParamSchema,
}
static examples = [
{
title: 'Packagist Dependency Version',
namedParams: {
user: 'symfony',
repo: 'symfony',
dependency: 'twig/twig',
},
staticPreview: this.render({
dependency: 'twig/twig',
dependencyVersion: '2.13|^3.0.4',
}),
},
{
title: 'Packagist Dependency Version (specify version)',
namedParams: {
user: 'symfony',
repo: 'symfony',
dependency: 'twig/twig',
},
queryParams: {
version: 'v2.8.0',
},
staticPreview: this.render({
dependency: 'twig/twig',
dependencyVersion: '1.12',
}),
},
{
title: 'Packagist Dependency Version (custom server)',
namedParams: {
user: 'symfony',
repo: 'symfony',
dependency: 'twig/twig',
},
queryParams: {
server: 'https://packagist.org',
},
staticPreview: this.render({
dependency: 'twig/twig',
dependencyVersion: '2.13|^3.0.4',
}),
documentation: customServerDocumentationFragment,
},
{
title: 'Packagist PHP Version',
namedParams: {
user: 'symfony',
repo: 'symfony',
dependency: 'php',
},
staticPreview: this.render({
dependency: 'php',
dependencyVersion: '^7.1.3',
}),
},
]
static defaultBadgeData = {
label: 'dependency version',
color: 'blue',
}
static render({ dependency, dependencyVersion }) {
return {
label: dependency,
message: dependencyVersion,
}
}
async getDependencyVersion({
json,
user,
repo,
dependency,
version = '',
server,
}) {
let packageVersion
const versions = BasePackagistService.expandPackageVersions(
json,
this.getPackageName(user, repo)
)
if (version === '') {
packageVersion = this.findLatestRelease(versions)
} else {
try {
packageVersion = await this.findSpecifiedVersion(
versions,
user,
repo,
version,
server
)
} catch (e) {
packageVersion = null
}
}
if (!packageVersion) {
throw new NotFound({ prettyMessage: 'invalid version' })
}
if (!packageVersion.require) {
throw new NotFound({ prettyMessage: 'version requirement not found' })
}
// All dependencies' names in the 'require' section from the response should be lowercase,
// so that we can compare lowercase name of the dependency given via url by the user.
Object.keys(packageVersion.require).forEach(dependency => {
packageVersion.require[dependency.toLowerCase()] =
packageVersion.require[dependency]
})
const depLowerCase = dependency.toLowerCase()
if (!packageVersion.require[depLowerCase]) {
throw new NotFound({ prettyMessage: 'version requirement not found' })
}
return { dependencyVersion: packageVersion.require[depLowerCase] }
}
async handle({ user, repo, dependency }, { server, version = '' }) {
const allData = await this.fetch({
user,
repo,
schema: allVersionsSchema,
server,
})
const { dependencyVersion } = await this.getDependencyVersion({
json: allData,
user,
repo,
dependency,
version,
server,
})
return this.constructor.render({
dependency,
dependencyVersion,
})
}
findVersionIndex(json, version) {
return json.findIndex(v => v.version === version)
}
async findSpecifiedVersion(json, user, repo, version, server) {
let release
if ((release = json[this.findVersionIndex(json, version)])) {
return release
} else {
try {
const allData = await this.fetchDev({
user,
repo,
schema: allVersionsSchema,
server,
})
const versions = BasePackagistService.expandPackageVersions(
allData,
this.getPackageName(user, repo)
)
return versions[this.findVersionIndex(versions, version)]
} catch (e) {
return release
}
}
}
}

View File

@@ -0,0 +1,93 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import PackagistDependencyVersion from './packagist-dependency-version.service.js'
const { expect } = chai
chai.use(chaiAsPromised)
describe('PackagistDependencyVersion', function () {
const fullPackagistJson = {
packages: {
'frodo/the-one-package': [
{
version: 'v3.0.0',
require: { php: '^7.4 || 8', 'twig/twig': '~1.28|~2.0' },
},
{
version: 'v2.5.0',
require: '__unset',
},
{
version: 'v2.4.0',
},
{
version: 'v2.0.0',
require: { php: '^7.2', 'twig/twig': '~1.20|~1.30' },
},
{
version: 'v1.0.0',
require: { php: '^5.6 || ^7', 'twig/twig': '~1.10|~1.0' },
},
],
},
}
it('should throw NotFound when package version is missing in the response', async function () {
await expect(
PackagistDependencyVersion.prototype.getDependencyVersion({
json: fullPackagistJson,
user: 'frodo',
repo: 'the-one-package',
version: 'v4.0.0',
})
).to.be.rejectedWith('invalid version')
})
it('should throw NotFound when `require` section is missing in the response', async function () {
await expect(
PackagistDependencyVersion.prototype.getDependencyVersion({
json: fullPackagistJson,
user: 'frodo',
repo: 'the-one-package',
version: 'v2.4.0',
})
).to.be.rejectedWith('version requirement not found')
})
it('should throw NotFound when `require` section in the response has the value of __unset (thank you, Packagist API :p)', async function () {
await expect(
PackagistDependencyVersion.prototype.getDependencyVersion({
json: fullPackagistJson,
user: 'frodo',
repo: 'the-one-package',
version: 'v2.5.0',
})
).to.be.rejectedWith('version requirement not found')
})
it('should return dependency version for the default release', async function () {
expect(
await PackagistDependencyVersion.prototype.getDependencyVersion({
json: fullPackagistJson,
user: 'frodo',
repo: 'the-one-package',
dependency: 'twig/twig',
})
)
.to.have.property('dependencyVersion')
.that.equals('~1.28|~2.0')
})
it('should return dependency version for the specified release', async function () {
expect(
await PackagistDependencyVersion.prototype.getDependencyVersion({
json: fullPackagistJson,
user: 'frodo',
repo: 'the-one-package',
version: 'v2.0.0',
dependency: 'twig/twig',
})
)
.to.have.property('dependencyVersion')
.that.equals('~1.20|~1.30')
})
})

View File

@@ -0,0 +1,61 @@
import { isComposerVersion } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('gets the package version')
.get('/symfony/symfony/twig/twig.json')
.expectBadge({ label: 'twig/twig', message: isComposerVersion })
t.create('incorrect dependency name')
.get('/symfony/symfony/twig/twiiiiiiig.json')
.expectBadge({
label: 'dependency version',
message: 'version requirement not found',
})
t.create('missing vendor of dependency')
.get('/symfony/symfony/twig.json')
.expectBadge({
label: 'dependency version',
message: 'version requirement not found',
})
t.create('gets the package version + specified symfony version')
.get('/symfony/symfony/twig/twig.json?version=v3.2.8')
.expectBadge({ label: 'twig/twig', message: isComposerVersion })
t.create('gets the package version + valid custom server')
.get('/symfony/symfony/twig/twig.json?server=https://packagist.org')
.expectBadge({ label: 'twig/twig', message: isComposerVersion })
t.create('invalid custom server')
.get('/symfony/symfony/twig/twig.json?server=https://packagisttttttt.org')
.expectBadge({
label: 'dependency version',
message: 'inaccessible',
})
t.create('incorrect symfony version')
.get('/symfony/symfony/twig/twig.json?version=v3.2.80000')
.expectBadge({
label: 'dependency version',
message: 'invalid version',
})
t.create('gets the package version - dependency does not need the vendor')
.get('/symfony/symfony/ext-xml.json')
.expectBadge({ label: 'ext-xml', message: isComposerVersion })
t.create('package with no requirements')
.get('/bpampuch/pdfmake/twig/twig.json')
.expectBadge({
label: 'dependency version',
message: 'version requirement not found',
})
t.create('package with no twig/twig version requirement')
.get('/raulfraile/ladybug-theme-modern/twig/twig.json')
.expectBadge({
label: 'dependency version',
message: 'version requirement not found',
})

View File

@@ -1,149 +1,21 @@
import Joi from 'joi'
import { redirector } from '../index.js'
import { optionalUrl } from '../validators.js'
import { NotFound } from '../index.js'
import {
allVersionsSchema,
BasePackagistService,
customServerDocumentationFragment,
} from './packagist-base.js'
const queryParamSchema = Joi.object({
server: optionalUrl,
}).required()
export default class PackagistPhpVersion extends BasePackagistService {
static category = 'platform-support'
static route = {
export default redirector({
category: 'platform-support',
route: {
base: 'packagist/php-v',
pattern: ':user/:repo/:version?',
queryParamSchema,
}
static examples = [
{
title: 'Packagist PHP Version Support',
pattern: ':user/:repo',
namedParams: {
user: 'symfony',
repo: 'symfony',
},
staticPreview: this.render({ php: '^7.1.3' }),
},
{
title: 'Packagist PHP Version Support (specify version)',
pattern: ':user/:repo/:version',
namedParams: {
user: 'symfony',
repo: 'symfony',
version: 'v2.8.0',
},
staticPreview: this.render({ php: '>=5.3.9' }),
},
{
title: 'Packagist PHP Version Support (custom server)',
pattern: ':user/:repo',
namedParams: {
user: 'symfony',
repo: 'symfony',
},
queryParams: {
server: 'https://packagist.org',
},
staticPreview: this.render({ php: '^7.1.3' }),
documentation: customServerDocumentationFragment,
},
]
static defaultBadgeData = {
label: 'php',
color: 'blue',
}
static render({ php }) {
return {
message: php,
}
}
findVersionIndex(json, version) {
return json.findIndex(v => v.version === version)
}
async findSpecifiedVersion(json, user, repo, version, server) {
let release
if ((release = json[this.findVersionIndex(json, version)])) {
return release
} else {
try {
const allData = await this.fetchDev({
user,
repo,
schema: allVersionsSchema,
server,
})
const versions = BasePackagistService.expandPackageVersions(
allData,
this.getPackageName(user, repo)
)
return versions[this.findVersionIndex(versions, version)]
} catch (e) {
return release
}
}
}
async getPhpVersion({ json, user, repo, version = '', server }) {
let packageVersion
const versions = BasePackagistService.expandPackageVersions(
json,
this.getPackageName(user, repo)
)
if (version === '') {
packageVersion = this.findLatestRelease(versions)
} else {
try {
packageVersion = await this.findSpecifiedVersion(
versions,
user,
repo,
version,
server
)
} catch (e) {
packageVersion = null
}
}
if (!packageVersion) {
throw new NotFound({ prettyMessage: 'invalid version' })
}
if (!packageVersion.require || !packageVersion.require.php) {
throw new NotFound({ prettyMessage: 'version requirement not found' })
}
return { phpVersion: packageVersion.require.php }
}
async handle({ user, repo, version = '' }, { server }) {
const allData = await this.fetch({
user,
repo,
schema: allVersionsSchema,
server,
})
const { phpVersion } = await this.getPhpVersion({
json: allData,
user,
repo,
version,
server,
})
return this.constructor.render({ php: phpVersion })
}
}
},
transformPath: ({ user, repo }) =>
`/packagist/dependency-v/${user}/${repo}/php`,
transformQueryParams: ({ version, server }) => ({ version, server }),
overrideTransformedQueryParams: true,
dateAdded: new Date('2022-09-07'),
})

View File

@@ -1,115 +0,0 @@
import { expect } from 'chai'
import PackagistPhpVersion from './packagist-php-version.service.js'
describe('PackagistPhpVersion', function () {
const json = {
packages: {
'frodo/the-one-package': [
{
version: '3.0.0',
require: { php: '^7.4 || 8' },
},
{
version: '2.0.0',
require: { php: '^7.2' },
},
{
version: '1.0.0',
require: { php: '^5.6 || ^7' },
},
],
},
}
it('should throw NotFound when package version is missing', async function () {
await expect(
PackagistPhpVersion.prototype.getPhpVersion({
json,
user: 'frodo',
repo: 'the-one-package',
version: '4.0.0',
})
).to.be.rejectedWith('invalid version')
})
it('should throw NotFound when PHP version not found on package when using default release', async function () {
const specJson = {
packages: {
'frodo/the-one-package': [
{
version: '3.0.0',
},
{
version: '2.0.0',
require: { php: '^7.2' },
},
{
version: '1.0.0',
require: { php: '^5.6 || ^7' },
},
],
},
}
await expect(
PackagistPhpVersion.prototype.getPhpVersion({
json: specJson,
user: 'frodo',
repo: 'the-one-package',
})
).to.be.rejectedWith('version requirement not found')
})
it('should throw NotFound when PHP version not found on package when using specified release', async function () {
const specJson = {
packages: {
'frodo/the-one-package': [
{
version: '3.0.0',
require: { php: '^7.4 || 8' },
},
{
version: '2.0.0',
require: { php: '^7.2' },
},
{
version: '1.0.0',
require: '__unset',
},
],
},
}
await expect(
PackagistPhpVersion.prototype.getPhpVersion({
json: specJson,
user: 'frodo',
repo: 'the-one-package',
version: '1.0.0',
})
).to.be.rejectedWith('version requirement not found')
})
it('should return PHP version for the default release', async function () {
expect(
await PackagistPhpVersion.prototype.getPhpVersion({
json,
user: 'frodo',
repo: 'the-one-package',
})
)
.to.have.property('phpVersion')
.that.equals('^7.4 || 8')
})
it('should return PHP version for the specified release', async function () {
expect(
await PackagistPhpVersion.prototype.getPhpVersion({
json,
user: 'frodo',
repo: 'the-one-package',
version: '2.0.0',
})
)
.to.have.property('phpVersion')
.that.equals('^7.2')
})
})

View File

@@ -1,35 +1,24 @@
import { isComposerVersion } from '../test-validators.js'
import { createServiceTester } from '../tester.js'
export const t = await createServiceTester()
t.create('gets the package version of symfony')
t.create(
'redirect getting required php version for the dependency from packagist (valid, package version not specified in request)'
)
.get('/symfony/symfony.json')
.expectBadge({ label: 'php', message: isComposerVersion })
.expectRedirect('/packagist/dependency-v/symfony/symfony/php.json?')
t.create('gets the package version of symfony 5.2.3')
.get('/symfony/symfony/v5.2.3.json')
.expectBadge({ label: 'php', message: isComposerVersion })
t.create(
'redirect getting required php version for the dependency from packagist (valid, package version specified in request)'
)
.get('/symfony/symfony/v3.2.8.json')
.expectRedirect(
'/packagist/dependency-v/symfony/symfony/php.json?version=v3.2.8'
)
t.create('package with no requirements')
.get('/bpampuch/pdfmake.json')
.expectBadge({ label: 'php', message: 'version requirement not found' })
t.create('package with no php version requirement')
.get('/raulfraile/ladybug-theme-modern.json')
.expectBadge({ label: 'php', message: 'version requirement not found' })
t.create('invalid package name')
.get('/frodo/is-not-a-package.json')
.expectBadge({ label: 'php', message: 'not found' })
t.create('invalid version')
.get('/symfony/symfony/invalid.json')
.expectBadge({ label: 'php', message: 'invalid version' })
t.create('custom server')
.get('/symfony/symfony.json?server=https%3A%2F%2Fpackagist.org')
.expectBadge({ label: 'php', message: isComposerVersion })
t.create('invalid custom server')
.get('/symfony/symfony.json?server=https%3A%2F%2Fpackagist.com')
.expectBadge({ label: 'php', message: 'not found' })
t.create(
'redirect getting required php version for the dependency from packagist (valid, package version and server specified in request)'
)
.get('/symfony/symfony/v3.2.8.json?server=https://packagist.org')
.expectRedirect(
'/packagist/dependency-v/symfony/symfony/php.json?server=https%3A%2F%2Fpackagist.org&version=v3.2.8'
)

View File

@@ -43,7 +43,7 @@ const isVPlusDottedVersionNClausesWithOptionalSuffixAndEpoch = withRegex(
// https://getcomposer.org/doc/04-schema.md#package-links
// https://getcomposer.org/doc/04-schema.md#minimum-stability
const isComposerVersion = withRegex(
/^\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?((\s*\|\|)?\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?)*\s*$/
/^\*|(\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?((\s*\|*)?\s*(>=|>|<|<=|!=|\^|~)?\d+(\.(\*|(\d+(\.(\d+|\*))?)))?)*\s*)$/
)
// Regex for validate php-version.versionReduction()