import type { Timestamp } from '../../../util/timestamp.ts'; import type { ReleaseResult } from '../types.ts'; import { ComposerRelease, ComposerReleases, MinifiedArray, RegistryMeta, parsePackagesResponse, parsePackagesResponses, } from './schema.ts'; describe('modules/datasource/packagist/schema', () => { describe('MinifiedArray', () => { it('parses MinifiedArray', () => { expect(MinifiedArray.parse([])).toEqual([]); // Source: https://github.com/composer/metadata-minifier/blob/1.0.0/tests/MetadataMinifierTest.php expect( MinifiedArray.parse([ { name: 'foo/bar', version: '2.0.0', version_normalized: '2.0.0.0', type: 'library', scripts: { foo: 'bar', }, license: ['MIT'], }, { version: '1.2.0', version_normalized: '1.2.0.0', license: ['GPL'], homepage: 'https://example.org', scripts: '__unset', }, { version: '1.0.0', version_normalized: '1.0.0.0', homepage: '__unset', }, ]), ).toEqual([ { name: 'foo/bar', version: '2.0.0', version_normalized: '2.0.0.0', type: 'library', scripts: { foo: 'bar', }, license: ['MIT'], }, { name: 'foo/bar', version: '1.2.0', version_normalized: '1.2.0.0', type: 'library', license: ['GPL'], homepage: 'https://example.org', }, { name: 'foo/bar', version: '1.0.0', version_normalized: '1.0.0.0', type: 'library', license: ['GPL'], }, ]); }); }); describe('ComposerRelease', () => { it('rejects ComposerRelease', () => { expect(() => ComposerRelease.parse(null)).toThrow( 'Invalid input: expected object, received null', ); expect(() => ComposerRelease.parse(undefined)).toThrow( 'Invalid input: expected object, received undefined', ); expect(() => ComposerRelease.parse('')).toThrow( 'Invalid input: expected object, received string', ); expect(() => ComposerRelease.parse({})).toThrow( 'Invalid input: expected string, received undefined', ); expect(() => ComposerRelease.parse({ version: null })).toThrow( 'Invalid input: expected string, received null', ); expect(() => ComposerRelease.parse({ version: null })).toThrow( 'Invalid input: expected string, received null', ); }); it('parses ComposerRelease', () => { expect(ComposerRelease.parse({ version: '' })).toEqual({ version: '', homepage: null, source: null, time: null, require: null, }); expect(ComposerRelease.parse({ version: 'dev-main' })).toEqual({ version: 'dev-main', homepage: null, source: null, time: null, require: null, }); expect(ComposerRelease.parse({ version: '1.2.3' })).toEqual({ version: '1.2.3', homepage: null, source: null, time: null, require: null, }); expect(ComposerRelease.parse({ version: '1.2.3', homepage: 42 })).toEqual( { version: '1.2.3', homepage: null, source: null, time: null, require: null, }, ); expect( ComposerRelease.parse({ version: '1.2.3', homepage: 'example.com' }), ).toEqual({ version: '1.2.3', homepage: 'example.com', source: null, time: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', source: 'nonsense' }), ).toEqual({ version: '1.2.3', homepage: null, source: null, time: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', source: { url: 'foobar' } }), ).toEqual({ version: '1.2.3', source: { url: 'foobar' }, homepage: null, time: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', time: '2025-01-16T12:00:00.000Z', }), ).toEqual({ version: '1.2.3', time: '2025-01-16T12:00:00.000Z', homepage: null, source: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', time: '2025-01-16T12:00:00.000Z', 'published-time': '2025-02-20T08:30:00.000Z', }), ).toEqual({ version: '1.2.3', time: '2025-01-16T12:00:00.000Z', 'published-time': '2025-02-20T08:30:00.000Z', homepage: null, source: null, require: null, }); expect( ComposerRelease.parse({ version: 123, }), ).toEqual({ version: '123', time: null, homepage: null, source: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', abandoned: true }), ).toEqual({ version: '1.2.3', abandoned: true, homepage: null, source: null, time: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', abandoned: 'scheb/2fa-bundle', }), ).toEqual({ version: '1.2.3', abandoned: 'scheb/2fa-bundle', homepage: null, source: null, time: null, require: null, }); expect( ComposerRelease.parse({ version: '1.2.3', abandoned: 42 }), ).toEqual({ version: '1.2.3', homepage: null, source: null, time: null, require: null, }); }); }); describe('ComposerReleases', () => { it('parses ComposerReleases', () => { expect(ComposerReleases.parse(null)).toBeEmptyArray(); expect(ComposerReleases.parse(undefined)).toBeEmptyArray(); expect(ComposerReleases.parse('')).toBeEmptyArray(); expect(ComposerReleases.parse({})).toBeEmptyArray(); expect(ComposerReleases.parse([])).toBeEmptyArray(); expect(ComposerReleases.parse([null])).toBeEmptyArray(); expect(ComposerReleases.parse([1, 2, 3])).toBeEmptyArray(); expect(ComposerReleases.parse(['foobar'])).toBeEmptyArray(); expect( ComposerReleases.parse([{ version: '1.2.3' }, { version: 'dev-main' }]), ).toEqual([ { version: '1.2.3', homepage: null, source: null, time: null, require: null, }, { version: 'dev-main', homepage: null, source: null, time: null, require: null, }, ]); }); }); describe('parsePackageResponse', () => { it('parses package response', () => { expect(parsePackagesResponse('foo/bar', null)).toEqual([]); expect(parsePackagesResponse('foo/bar', {})).toEqual([]); expect(parsePackagesResponse('foo/bar', { packages: '123' })).toEqual([]); expect(parsePackagesResponse('foo/bar', { packages: {} })).toEqual([]); expect( parsePackagesResponse('foo/bar', { packages: { 'foo/bar': [{ version: '1.2.3' }], 'baz/qux': [{ version: '4.5.6' }], }, }), ).toEqual([ { version: '1.2.3', homepage: null, source: null, time: null, require: null, }, ]); expect( parsePackagesResponse('foo/bar', { packages: { 'foo/bar': { '1.2.3': { version: '1.2.3' } }, 'baz/qux': { '4.5.6': { version: '4.5.6' } }, }, }), ).toEqual([ { version: '1.2.3', homepage: null, source: null, time: null, require: null, }, ]); }); it('expands minified fields', () => { expect( parsePackagesResponse('foo/bar', { packages: { 'foo/bar': [ { version: '3.3.3', require: { php: '^8.0' } }, { version: '2.2.2' }, { version: '1.1.1' }, { version: '0.0.4', require: { php: '^7.0' } }, { version: '0.0.3' }, { version: '0.0.2', require: '__unset' }, { version: '0.0.1' }, ], }, }), ).toEqual([ { version: '3.3.3', require: { php: '^8.0' }, homepage: null, source: null, time: null, }, { version: '2.2.2', require: { php: '^8.0' }, homepage: null, source: null, time: null, }, { version: '1.1.1', require: { php: '^8.0' }, homepage: null, source: null, time: null, }, { version: '0.0.4', require: { php: '^7.0' }, homepage: null, source: null, time: null, }, { version: '0.0.3', require: { php: '^7.0' }, homepage: null, source: null, time: null, }, { version: '0.0.2', homepage: null, source: null, time: null, require: null, }, { version: '0.0.1', homepage: null, source: null, time: null, require: null, }, ]); }); }); describe('parsePackagesResponses', () => { it('parses array of responses', () => { expect(parsePackagesResponses('foo/bar', [null])).toBeNull(); expect(parsePackagesResponses('foo/bar', [{}])).toBeNull(); expect( parsePackagesResponses('foo/bar', [{ packages: '123' }]), ).toBeNull(); expect(parsePackagesResponses('foo/bar', [{ packages: {} }])).toBeNull(); expect( parsePackagesResponses('foo/bar', [ { packages: { 'foo/bar': [ { version: 'v1.1.1', time: '2025-01-16T12:00:00+00:00' as Timestamp, homepage: 'https://example.com/1', source: { url: 'git@example.com:foo/bar-1' }, require: { php: '^8.0' }, }, ], 'baz/qux': [ { version: 'v2.2.2', time: '2025-01-16T12:00:00+00:00' as Timestamp, homepage: 'https://example.com/2', source: { url: 'git@example.com:baz/qux-2' }, require: null, }, ], }, }, { packages: { 'foo/bar': [ { version: 'v3.3.3', time: '2025-01-16T12:00:00+00:00' as Timestamp, homepage: 'https://example.com/3', source: { url: 'git@example.com:foo/bar-3' }, require: { php: '^7.0' }, }, ], 'baz/qux': [ { version: 'v4.4.4', time: '2025-01-16T12:00:00+00:00' as Timestamp, homepage: 'https://example.com/4', source: { url: 'git@example.com:baz/qux-3' }, require: null, }, ], }, }, ] satisfies { packages: Record }[]), ).toEqual({ homepage: 'https://example.com/1', sourceUrl: 'git@example.com:foo/bar-1', releases: [ { version: '1.1.1', gitRef: 'v1.1.1', releaseTimestamp: '2025-01-16T12:00:00.000Z' as Timestamp, constraints: { php: ['^8.0'] }, }, { version: '3.3.3', gitRef: 'v3.3.3', releaseTimestamp: '2025-01-16T12:00:00.000Z' as Timestamp, constraints: { php: ['^7.0'] }, }, ], } satisfies ReleaseResult); }); it('marks abandoned packages as deprecated', () => { expect( parsePackagesResponses('sonata-project/core-bundle', [ { packages: { 'sonata-project/core-bundle': [ { version: '3.20.0', abandoned: true }, { version: '3.19.0', abandoned: true }, ], }, }, ]), ).toEqual({ deprecationMessage: 'This package is abandoned and no longer maintained.', releases: [ { version: '3.20.0', gitRef: '3.20.0', isDeprecated: true }, { version: '3.19.0', gitRef: '3.19.0', isDeprecated: true }, ], } satisfies ReleaseResult); }); it('suggests a replacement for abandoned packages', () => { expect( parsePackagesResponses('scheb/two-factor-bundle', [ { packages: { 'scheb/two-factor-bundle': [ { version: 'v4.18.4', abandoned: 'scheb/2fa-bundle' }, ], }, }, ]), ).toEqual({ deprecationMessage: 'This package is abandoned and no longer maintained. The author suggests using the `scheb/2fa-bundle` package instead.', releases: [ { version: '4.18.4', gitRef: 'v4.18.4', isDeprecated: true }, ], } satisfies ReleaseResult); }); it('prefers published-time over time, falling back to time', () => { expect( parsePackagesResponses('foo/bar', [ { packages: { 'foo/bar': { '1.1.1': { version: 'v1.1.1', time: '2025-01-16T12:00:00+00:00', 'published-time': '2025-02-20T08:30:00+00:00', }, '2.2.2': { version: 'v2.2.2', time: '2025-03-10T09:00:00+00:00', }, }, }, }, ]), ).toEqual({ releases: [ { version: '1.1.1', gitRef: 'v1.1.1', releaseTimestamp: '2025-02-20T08:30:00.000Z' as Timestamp, }, { version: '2.2.2', gitRef: 'v2.2.2', releaseTimestamp: '2025-03-10T09:00:00.000Z' as Timestamp, }, ], } satisfies ReleaseResult); }); }); describe('RegistryMeta', () => { it('falls back to default values', () => { expect(RegistryMeta.parse('nonsense')).toEqual({ files: [], includesFiles: [], packages: {}, providerPackages: {}, includesPackages: {}, providersLazyUrl: null, providersUrl: null, metadataUrl: null, availablePackages: null, }); }); }); });