renovate/lib/modules/manager/npm/post-update/npm.spec.ts

728 lines
23 KiB
TypeScript

import upath from 'upath';
import { envMock, mockExecAll } from '../../../../../test/exec-util';
import { Fixtures } from '../../../../../test/fixtures';
import { env, fs, mockedFunction } from '../../../../../test/util';
import { GlobalConfig } from '../../../../config/global';
import { getNodeToolConstraint } from './node-version';
import * as npmHelper from './npm';
jest.mock('../../../../util/exec/env');
jest.mock('../../../../util/fs');
jest.mock('./node-version');
process.env.CONTAINERBASE = 'true';
describe('modules/manager/npm/post-update/npm', () => {
beforeEach(() => {
env.getChildProcessEnv.mockReturnValue(envMock.basic);
GlobalConfig.set({ localDir: '' });
mockedFunction(getNodeToolConstraint).mockResolvedValueOnce({
toolName: 'node',
constraint: '16.16.0',
});
});
it('generates lock files', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValueOnce('{}');
const packageLockContents = JSON.stringify({
packages: {},
lockfileVersion: 3,
});
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
const skipInstalls = true;
const postUpdateOptions = ['npmDedupe'];
const updates = [
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
];
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ skipInstalls, postUpdateOptions },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe(packageLockContents);
expect(execSnapshots).toMatchSnapshot();
});
it('performs lock file updates', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('package-lock-contents');
const skipInstalls = true;
const updates = [
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: true },
];
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ skipInstalls, constraints: { npm: '^6.0.0' } },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchSnapshot();
});
it('performs lock file updates retaining the package.json counterparts', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce(
Fixtures.get('update-lockfile-massage-1/package-lock.json'),
);
const skipInstalls = true;
const updates = [
{
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {}, // intentional: edge-case test for workspaces
},
];
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ skipInstalls, constraints: { npm: '^6.0.0' } },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.error).toBeFalse();
expect(res.lockFile).toMatchSnapshot();
expect(execSnapshots).toMatchSnapshot();
});
it('performs npm-shrinkwrap.json updates', async () => {
const execSnapshots = mockExecAll();
fs.localPathExists.mockResolvedValueOnce(true);
fs.readLocalFile.mockResolvedValueOnce('package-lock-contents');
const skipInstalls = true;
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'npm-shrinkwrap.json',
{ skipInstalls, constraints: { npm: '^6.0.0' } },
);
expect(fs.renameLocalFile).toHaveBeenCalledTimes(1);
expect(fs.renameLocalFile).toHaveBeenCalledWith(
upath.join('some-dir', 'package-lock.json'),
upath.join('some-dir', 'npm-shrinkwrap.json'),
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(fs.readLocalFile).toHaveBeenCalledWith(
'some-dir/npm-shrinkwrap.json',
'utf8',
);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe('package-lock-contents');
// TODO: is that right?
expect(execSnapshots).toEqual([]);
});
it('performs npm-shrinkwrap.json updates (no package-lock.json)', async () => {
const execSnapshots = mockExecAll();
fs.findLocalSiblingOrParent.mockResolvedValueOnce(null);
fs.readLocalFile.mockResolvedValueOnce('package-lock-contents');
const skipInstalls = true;
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'npm-shrinkwrap.json',
{ skipInstalls, constraints: { npm: '^6.0.0' } },
);
expect(fs.renameLocalFile).toHaveBeenCalledTimes(0);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(fs.readLocalFile).toHaveBeenCalledWith(
'some-dir/npm-shrinkwrap.json',
'utf8',
);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe('package-lock-contents');
// TODO: is that right?
expect(execSnapshots).toEqual([]);
});
it('performs full install', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('package-lock-contents');
const skipInstalls = false;
const binarySource = 'global';
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ skipInstalls, binarySource, constraints: { npm: '^6.0.0' } },
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe('package-lock-contents');
// TODO: is that right?
expect(execSnapshots).toEqual([]);
});
it('deduplicates dependencies on installation with npm >= 7', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValueOnce('{}');
const packageLockContents = JSON.stringify({
dependencies: {},
lockfileVersion: 2,
});
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
const postUpdateOptions = ['npmDedupe'];
const updates = [
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
];
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ postUpdateOptions },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe(packageLockContents);
expect(execSnapshots).toHaveLength(1);
expect(execSnapshots).toMatchObject([
{
cmd: 'npm install --package-lock-only --no-audit --prefer-dedupe --ignore-scripts',
},
]);
});
it('deduplicates package-lock.json dependencies after installation with npm <= 6', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValueOnce('{}');
const packageLockContents = JSON.stringify({
dependencies: {},
lockfileVersion: 1,
});
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
const postUpdateOptions = ['npmDedupe'];
const updates = [
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
];
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ postUpdateOptions },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe(packageLockContents);
expect(execSnapshots).toHaveLength(2);
expect(execSnapshots).toMatchObject([
{
cmd: 'npm install --no-audit --ignore-scripts',
},
{
cmd: 'npm dedupe',
},
]);
});
it('deduplicates npm-shrinkwrap.json dependencies after installation with npm <= 6', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValueOnce('{}');
const packageLockContents = JSON.stringify({
dependencies: {},
lockfileVersion: 1,
});
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
fs.readLocalFile.mockResolvedValueOnce(packageLockContents);
const postUpdateOptions = ['npmDedupe'];
const updates = [
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
];
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'npm-shrinkwrap.json',
{ postUpdateOptions },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(fs.readLocalFile).toHaveBeenCalledWith(
'some-dir/npm-shrinkwrap.json',
'utf8',
);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe(packageLockContents);
expect(execSnapshots).toHaveLength(2);
expect(execSnapshots).toMatchObject([
{
cmd: 'npm install --no-audit --ignore-scripts',
},
{
cmd: 'npm dedupe',
},
]);
});
it('runs twice if remediating', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('package-lock-contents');
const binarySource = 'global';
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ binarySource, constraints: { npm: '^6.0.0' } },
[{ isRemediation: true }],
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.error).toBeFalse();
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toHaveLength(2);
});
it('catches errors', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockImplementation(() => {
throw new Error('not found');
});
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.error).toBeTrue();
expect(res.lockFile).toBeUndefined();
expect(execSnapshots).toEqual([]);
});
it('finds npm globally', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValue('{}');
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(res.lockFile).toBe('package-lock-contents');
// TODO: is that right?
expect(execSnapshots).toEqual([]);
});
it('uses docker npm', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ binarySource: 'docker', constraints: { npm: '^6.0.0' } },
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
// TODO: is that right?
expect(execSnapshots).toEqual([]);
});
it('performs lock file maintenance', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValue('{}');
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{},
[{ isLockFileMaintenance: true }],
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(fs.deleteLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchSnapshot();
});
it('works for docker mode', async () => {
GlobalConfig.set({
localDir: '',
cacheDir: '/tmp',
binarySource: 'docker',
allowScripts: true,
dockerSidecarImage: 'ghcr.io/containerbase/sidecar',
});
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ constraints: { npm: '6.0.0' } },
[{ isLockFileMaintenance: true }],
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchObject([
{ cmd: 'docker pull ghcr.io/containerbase/sidecar' },
{ cmd: 'docker ps --filter name=renovate_sidecar -aq' },
{
cmd:
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
'-v "/tmp":"/tmp" ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "some-dir" ' +
'ghcr.io/containerbase/sidecar ' +
'bash -l -c "' +
'install-tool node 16.16.0 ' +
'&& ' +
'install-tool npm 6.0.0 ' +
'&& ' +
'hash -d npm 2>/dev/null || true ' +
'&& ' +
'npm install --package-lock-only --no-audit' +
'"',
},
]);
});
it('works for install mode', async () => {
GlobalConfig.set({
localDir: '',
cacheDir: '/tmp',
binarySource: 'install',
});
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValue('package-lock-contents');
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ constraints: { npm: '6.0.0' } },
[{ isLockFileMaintenance: true }],
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
expect(res.lockFile).toBe('package-lock-contents');
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool node 16.16.0' },
{ cmd: 'install-tool npm 6.0.0' },
{ cmd: 'hash -d npm 2>/dev/null || true' },
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
},
]);
});
describe('installs workspace only packages separately', () => {
const updates = [
{
packageFile: 'some-dir/docs/a/package.json',
packageName: 'abbrev',
depType: 'dependencies',
newVersion: '1.1.0',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/web/b/package.json',
packageName: 'xmldoc',
depType: 'dependencies',
newVersion: '2.2.0',
newValue: '^2.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/docs/a/package.json',
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/package.json',
packageName: 'chalk',
depType: 'dependencies',
newVersion: '9.4.8',
newValue: '^9.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/web/b/package.json',
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/package.json',
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/web/b/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/docs/a/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-dir/docs/dir.has.period/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'some-missing-dir/docs/a/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
];
it('workspace in sub-folder', async () => {
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValue('{}');
fs.readLocalFile.mockResolvedValueOnce('package-lock content');
const skipInstalls = true;
const res = await npmHelper.generateLockFile(
'some-dir',
{},
'package-lock.json',
{ skipInstalls },
updates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(res.error).toBeFalse();
expect(execSnapshots).toMatchObject([
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=docs/a abbrev@1.1.0 hello@1.1.1',
},
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=web/b xmldoc@2.2.0 hello@1.1.1',
},
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=docs/dir.has.period hello@1.1.1',
},
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts chalk@9.4.8 postcss@8.4.8',
},
]);
});
it('workspace in root folder', async () => {
const modifiedUpdates = updates.map((update) => {
return {
...update,
packageFile: update.packageFile.replace('some-dir/', ''),
};
});
const execSnapshots = mockExecAll();
// package.json
fs.readLocalFile.mockResolvedValue('{}');
fs.readLocalFile.mockResolvedValueOnce('package-lock content');
const skipInstalls = true;
const res = await npmHelper.generateLockFile(
'.',
{},
'package-lock.json',
{ skipInstalls },
modifiedUpdates,
);
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
expect(res.error).toBeFalse();
expect(execSnapshots).toMatchObject([
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=docs/a abbrev@1.1.0 hello@1.1.1',
},
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=web/b xmldoc@2.2.0 hello@1.1.1',
},
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --workspace=docs/dir.has.period hello@1.1.1',
},
{
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts chalk@9.4.8 postcss@8.4.8',
},
]);
expect(
npmHelper.divideWorkspaceAndRootDeps('.', modifiedUpdates),
).toMatchObject({
lockRootUpdates: [
{
packageFile: 'package.json',
packageName: 'chalk',
depType: 'dependencies',
newVersion: '9.4.8',
newValue: '^9.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
{
packageFile: 'package.json',
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
},
],
lockWorkspacesUpdates: [
{
packageFile: 'docs/a/package.json',
packageName: 'abbrev',
depType: 'dependencies',
newVersion: '1.1.0',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'docs/a',
},
{
packageFile: 'web/b/package.json',
packageName: 'xmldoc',
depType: 'dependencies',
newVersion: '2.2.0',
newValue: '^2.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'web/b',
},
{
packageFile: 'docs/a/package.json',
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'docs/a',
},
{
packageFile: 'web/b/package.json',
packageName: 'postcss',
depType: 'dependencies',
newVersion: '8.4.8',
newValue: '^8.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'web/b',
},
{
packageFile: 'web/b/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'web/b',
},
{
packageFile: 'docs/a/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'docs/a',
},
{
packageFile: 'docs/dir.has.period/package.json',
packageName: 'hello',
depType: 'dependencies',
newVersion: '1.1.1',
newValue: '^1.0.0',
isLockfileUpdate: true,
managerData: {
workspacesPackages: ['docs/*', 'web/*'],
},
workspace: 'docs/dir.has.period',
},
],
workspaces: new Set(['docs/a', 'web/b', 'docs/dir.has.period']),
rootDeps: new Set(['chalk@9.4.8', 'postcss@8.4.8']),
});
});
});
});