mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 10:09:50 +00:00
ac22678812
* added support for extracting integrity and tarball from npm datasource * test(datasource/npm): keep integrity and tarball in packument shape assertions The packument-trimming tests on main pinned a cached shape that dropped `dist.tarball`, so they broke once the schema started retaining `integrity` and `tarball`. Update both expectations to the new shape and cover `integrity` alongside `tarball`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(workers/lookup): expect integrity and tarball on npm updates `generateUpdate` copies `downloadUrl` and `newDigest` from the release onto the update, so every npm lookup in these tests now carries the tarball URL and, where the packument fixture has one, the integrity hash. Pin and rollback updates are built without those release fields, so their expectations are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Michael Kriese <michael.kriese@mend.io> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
340 lines
11 KiB
TypeScript
340 lines
11 KiB
TypeScript
import { CachedPackument, NpmResponse } from './schema.ts';
|
|
|
|
describe('modules/datasource/npm/schema', () => {
|
|
it('strips fields outside the cached packument shape', () => {
|
|
expect(
|
|
CachedPackument.parse({
|
|
_id: 'vue',
|
|
name: 'vue',
|
|
repository: {
|
|
type: 'git',
|
|
url: 'https://github.com/vuejs/vue.git',
|
|
directory: 'packages/core',
|
|
},
|
|
homepage: 'https://vuejs.org',
|
|
time: {
|
|
created: '2024-01-01T00:00:00.000Z',
|
|
'1.0.0': '2024-01-02T00:00:00.000Z',
|
|
},
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: {
|
|
'1.0.0': {
|
|
repository: 'vuejs/vue',
|
|
homepage: 'https://v1.vuejs.org',
|
|
deprecated: 'deprecated',
|
|
gitHead: 'abc123',
|
|
dependencies: { foo: '^1.0.0' },
|
|
devDependencies: { bar: '^2.0.0' },
|
|
engines: { node: '>=18', bun: '>=1.0.0' },
|
|
dist: {
|
|
attestations: {
|
|
url: 'https://example.com/attestations',
|
|
issuer: 'ignore me',
|
|
},
|
|
integrity: 'sha512-abc',
|
|
tarball: 'https://example.com/vue.tgz',
|
|
},
|
|
scripts: { test: 'vitest' },
|
|
},
|
|
},
|
|
readme: 'huge',
|
|
}),
|
|
).toEqual({
|
|
repository: {
|
|
url: 'https://github.com/vuejs/vue.git',
|
|
directory: 'packages/core',
|
|
},
|
|
homepage: 'https://vuejs.org',
|
|
time: {
|
|
created: '2024-01-01T00:00:00.000Z',
|
|
'1.0.0': '2024-01-02T00:00:00.000Z',
|
|
},
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: {
|
|
'1.0.0': {
|
|
repository: 'vuejs/vue',
|
|
homepage: 'https://v1.vuejs.org',
|
|
deprecated: 'deprecated',
|
|
gitHead: 'abc123',
|
|
dependencies: { foo: '^1.0.0' },
|
|
devDependencies: { bar: '^2.0.0' },
|
|
engines: { node: '>=18' },
|
|
dist: {
|
|
attestations: {
|
|
url: 'https://example.com/attestations',
|
|
},
|
|
integrity: 'sha512-abc',
|
|
tarball: 'https://example.com/vue.tgz',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('drops non-string `time` entries (e.g. Artifactory `unpublished: null`)', () => {
|
|
const result = NpmResponse.parse({
|
|
name: 'mypackage',
|
|
'dist-tags': { latest: '1.1.0' },
|
|
versions: { '1.0.0': {}, '1.1.0': {} },
|
|
time: {
|
|
unpublished: null,
|
|
created: '2026-01-22T23:58:45.285Z',
|
|
modified: '2026-06-02T00:59:50.138Z',
|
|
'1.0.0': '2026-01-23T01:23:37.982Z',
|
|
'1.1.0': '2026-04-15T18:50:36.431Z',
|
|
},
|
|
});
|
|
expect(result.time).toEqual({
|
|
created: '2026-01-22T23:58:45.285Z',
|
|
modified: '2026-06-02T00:59:50.138Z',
|
|
'1.0.0': '2026-01-23T01:23:37.982Z',
|
|
'1.1.0': '2026-04-15T18:50:36.431Z',
|
|
});
|
|
});
|
|
|
|
it('drops non-string `time` entries for the cached packument', () => {
|
|
const result = CachedPackument.parse({
|
|
time: {
|
|
unpublished: null,
|
|
'1.0.0': '2026-01-23T01:23:37.982Z',
|
|
},
|
|
versions: { '1.0.0': {} },
|
|
});
|
|
expect(result.time).toEqual({ '1.0.0': '2026-01-23T01:23:37.982Z' });
|
|
});
|
|
|
|
it('drops non-string dependencies/devDependencies entries (e.g. old-style nested dependency trees)', () => {
|
|
const result = CachedPackument.parse({
|
|
name: 'deep-diff',
|
|
versions: {
|
|
'0.1.0': {
|
|
dependencies: { foo: '^1.0.0', bar: { version: '0.6.4' } },
|
|
devDependencies: { vows: { version: '0.6.4' } },
|
|
},
|
|
},
|
|
});
|
|
expect(result.versions).toEqual({
|
|
'0.1.0': {
|
|
dependencies: { foo: '^1.0.0' },
|
|
devDependencies: {},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('drops an invalid package-level `homepage` (e.g. `null`)', () => {
|
|
const result = CachedPackument.parse({
|
|
homepage: null,
|
|
versions: { '1.0.0': {} },
|
|
});
|
|
expect(result.homepage).toBeUndefined();
|
|
});
|
|
|
|
// https://registry.npmjs.org/jsonfile — homepage: [""]
|
|
it('drops an invalid package-level `homepage` (e.g. an array)', () => {
|
|
const result = CachedPackument.parse({
|
|
homepage: [''],
|
|
versions: { '1.0.0': {} },
|
|
});
|
|
expect(result.homepage).toBeUndefined();
|
|
});
|
|
|
|
// https://registry.npmjs.org/jsonfile — versions 0.0.1 and 1.0.0 have `homepage: [""]`
|
|
it('drops a non-string `homepage` in a version entry instead of invalidating the whole cached packument', () => {
|
|
const result = CachedPackument.parse({
|
|
versions: {
|
|
'0.0.1': { homepage: [''] },
|
|
'6.2.1': {
|
|
homepage: 'https://github.com/jprichardson/node-jsonfile#readme',
|
|
},
|
|
},
|
|
});
|
|
expect(result.versions?.['0.0.1']?.homepage).toBeUndefined();
|
|
expect(result.versions?.['6.2.1']?.homepage).toBe(
|
|
'https://github.com/jprichardson/node-jsonfile#readme',
|
|
);
|
|
});
|
|
|
|
describe('NpmResponseSchema', () => {
|
|
it('parses a full npm registry response and preserves extra version fields', () => {
|
|
const input = {
|
|
_id: 'mypackage',
|
|
name: 'mypackage',
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: {
|
|
'1.0.0': {
|
|
homepage: 'https://example.com',
|
|
deprecated: false,
|
|
gitHead: 'abc123',
|
|
dependencies: { lodash: '^4.0.0' },
|
|
devDependencies: { jest: '^29.0.0' },
|
|
engines: { node: '>=16' },
|
|
dist: { attestations: { url: 'https://example.com/att' } },
|
|
'renovate-config': { default: { rangeStrategy: 'pin' } },
|
|
scripts: { test: 'jest' }, // extra field
|
|
},
|
|
},
|
|
time: { '1.0.0': '2024-01-01T00:00:00.000Z' },
|
|
repository: { url: 'https://github.com/org/mypackage' },
|
|
homepage: 'https://example.com',
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.name).toBe('mypackage');
|
|
expect(result['dist-tags']?.latest).toBe('1.0.0');
|
|
// renovate-config must be preserved in version objects (passthrough)
|
|
expect(result.versions?.['1.0.0']?.['renovate-config']).toEqual({
|
|
default: { rangeStrategy: 'pin' },
|
|
});
|
|
});
|
|
|
|
it('parses a response with string repository shorthand', () => {
|
|
const input = {
|
|
name: 'mypackage',
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: { '1.0.0': { repository: 'myorg/mypackage' } },
|
|
repository: 'myorg/mypackage',
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.repository).toBe('myorg/mypackage');
|
|
expect(result.versions?.['1.0.0']?.repository).toBe('myorg/mypackage');
|
|
});
|
|
|
|
it('accepts repository.url: null and preserves directory', () => {
|
|
const input = {
|
|
name: 'mypackage',
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: {
|
|
'1.0.0': {
|
|
repository: { url: null, directory: 'test' },
|
|
},
|
|
},
|
|
repository: { url: null, directory: 'packages/core' },
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.repository).toEqual({
|
|
url: undefined,
|
|
directory: 'packages/core',
|
|
});
|
|
expect(result.versions?.['1.0.0']?.repository).toEqual({
|
|
url: undefined,
|
|
directory: 'test',
|
|
});
|
|
});
|
|
|
|
it('drops an invalid package-level `homepage` (e.g. `null`)', () => {
|
|
const input = {
|
|
name: 'mypackage',
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: { '1.0.0': {} },
|
|
homepage: null,
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.homepage).toBeUndefined();
|
|
});
|
|
|
|
// https://registry.npmjs.org/jsonfile — homepage: [""]
|
|
it('drops an invalid package-level `homepage` (e.g. an array)', () => {
|
|
const input = {
|
|
name: 'mypackage',
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: { '1.0.0': {} },
|
|
homepage: [''],
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.homepage).toBeUndefined();
|
|
});
|
|
|
|
describe('drops a non-string `homepage` in a version entry instead of invalidating the whole packument', () => {
|
|
// https://registry.npmjs.org/jsonfile — versions 0.0.1 and 1.0.0 have `homepage: [""]`
|
|
it('handles `jsonfile`, where affected versions have `homepage: [""]`', () => {
|
|
const input = {
|
|
name: 'jsonfile',
|
|
'dist-tags': { latest: '6.2.1' },
|
|
homepage: 'https://github.com/jprichardson/node-jsonfile#readme',
|
|
versions: {
|
|
'0.0.1': { homepage: [''] },
|
|
'1.0.0': { homepage: [''] },
|
|
'6.2.1': {
|
|
homepage: 'https://github.com/jprichardson/node-jsonfile#readme',
|
|
},
|
|
},
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.versions?.['0.0.1']?.homepage).toBeUndefined();
|
|
expect(result.versions?.['1.0.0']?.homepage).toBeUndefined();
|
|
expect(result.versions?.['6.2.1']?.homepage).toBe(
|
|
'https://github.com/jprichardson/node-jsonfile#readme',
|
|
);
|
|
});
|
|
|
|
// https://registry.npmjs.org/fs-extra — version 0.0.1 has `homepage: ["<url>"]`
|
|
it('handles `fs-extra`, where affected versions have `homepage: ["<url>"]`', () => {
|
|
const input = {
|
|
name: 'fs-extra',
|
|
'dist-tags': { latest: '11.4.0' },
|
|
versions: {
|
|
'0.0.1': {
|
|
homepage: ['https://github.com/jprichardson/node-fs-extra'],
|
|
},
|
|
'11.4.0': {
|
|
homepage: 'https://github.com/jprichardson/node-fs-extra',
|
|
},
|
|
},
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.versions?.['0.0.1']?.homepage).toBeUndefined();
|
|
expect(result.versions?.['11.4.0']?.homepage).toBe(
|
|
'https://github.com/jprichardson/node-fs-extra',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('parses a response with an array of objects for the `repository`, and returns the first element', () => {
|
|
// https://registry.npmjs.org/tmp
|
|
it('as a package response', () => {
|
|
const input = {
|
|
name: 'tmp',
|
|
versions: {
|
|
'0.0.4': {
|
|
repository: [
|
|
{
|
|
url: 'git://github.com/raszi/tmp.git',
|
|
type: 'git',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
repository: {
|
|
url: 'git://github.com/raszi/tmp.git',
|
|
type: 'git',
|
|
},
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.repository).toEqual({
|
|
url: 'git://github.com/raszi/tmp.git',
|
|
});
|
|
expect(result.versions?.['0.0.4'].repository).toEqual({
|
|
url: 'git://github.com/raszi/tmp.git',
|
|
});
|
|
});
|
|
|
|
// https://registry.npmjs.org/tmp/0.0.4
|
|
it('as a version response', () => {
|
|
const input = {
|
|
name: 'tmp',
|
|
version: '0.0.4',
|
|
repository: [
|
|
{
|
|
url: 'git://github.com/raszi/tmp.git',
|
|
type: 'git',
|
|
},
|
|
],
|
|
};
|
|
const result = NpmResponse.parse(input);
|
|
expect(result.repository).toEqual({
|
|
url: 'git://github.com/raszi/tmp.git',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|