mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-04 11:33:40 +00:00
f4fa861457
* test(npm): replace file snapshots with explicit assertions * test(npm): replace inline snapshots with explicit assertions
1318 lines
40 KiB
TypeScript
1318 lines
40 KiB
TypeScript
import upath from 'upath';
|
|
import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util.ts';
|
|
import { Fixtures } from '~test/fixtures.ts';
|
|
import { env, fs } from '~test/util.ts';
|
|
import { GlobalConfig } from '../../../../config/global.ts';
|
|
import { ExecError } from '../../../../util/exec/exec-error.ts';
|
|
import { getNodeToolConstraint } from './node-version.ts';
|
|
import * as npmHelper from './npm.ts';
|
|
|
|
vi.mock('../../../../util/exec/env.ts');
|
|
vi.mock('../../../../util/fs/index.ts');
|
|
vi.mock('./node-version.ts');
|
|
|
|
process.env.CONTAINERBASE = 'true';
|
|
|
|
describe('modules/manager/npm/post-update/npm', () => {
|
|
beforeEach(() => {
|
|
env.getChildProcessEnv.mockReturnValue(envMock.basic);
|
|
GlobalConfig.set({ localDir: '', binarySource: 'global' });
|
|
vi.mocked(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).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --prefer-dedupe --ignore-scripts',
|
|
options: { cwd: 'some-dir' },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('runs npm install twice', async () => {
|
|
const execSnapshots = mockExecAll();
|
|
// package.json
|
|
fs.readLocalFile.mockResolvedValueOnce('{}');
|
|
const packageLockContents = JSON.stringify({
|
|
packages: {},
|
|
lockfileVersion: 3,
|
|
});
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(packageLockContents)
|
|
.mockResolvedValueOnce(packageLockContents);
|
|
const skipInstalls = true;
|
|
const postUpdateOptions = ['npmInstallTwice'];
|
|
const updates = [
|
|
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
|
|
];
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls, postUpdateOptions },
|
|
updates,
|
|
);
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
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).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts some-dep@1.0.1',
|
|
options: { cwd: 'some-dir' },
|
|
},
|
|
]);
|
|
});
|
|
|
|
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();
|
|
// the lock file update is applied to the "dependencies" section while the
|
|
// package.json counterpart in the "packages" section is retained
|
|
const lockFile = JSON.parse(res.lockFile!);
|
|
expect(lockFile.packages['']).toEqual({
|
|
name: 'update-lockfile-massage-1',
|
|
version: '1.0.0',
|
|
dependencies: { postcss: '^8.0.0' },
|
|
});
|
|
expect(lockFile.dependencies.postcss.version).toBe('8.4.8');
|
|
expect(lockFile.packages['node_modules/postcss'].version).toBe('8.4.31');
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts postcss@8.4.8',
|
|
options: { cwd: 'some-dir' },
|
|
},
|
|
]);
|
|
});
|
|
|
|
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).toHaveBeenCalledExactlyOnceWith(
|
|
upath.join('some-dir', 'package-lock.json'),
|
|
upath.join('some-dir', 'npm-shrinkwrap.json'),
|
|
);
|
|
expect(fs.readLocalFile).toHaveBeenCalledTimes(1);
|
|
expect(fs.readLocalFile).toHaveBeenCalledExactlyOnceWith(
|
|
'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).toHaveBeenCalledExactlyOnceWith(
|
|
'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();
|
|
const updates = [
|
|
{ packageName: 'some-dep', newVersion: '1.0.1', isLockfileUpdate: false },
|
|
];
|
|
// package.json
|
|
fs.readLocalFile.mockResolvedValue('{}');
|
|
fs.readLocalFile.mockResolvedValue('package-lock-contents');
|
|
const res = await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{},
|
|
updates,
|
|
);
|
|
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
|
|
expect(res.lockFile).toBe('package-lock-contents');
|
|
// since there are no install npm commands, it means we are using the global npm
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
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).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('does not run npm install four times for lock file maintenance with npmInstallTwice', 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',
|
|
{ postUpdateOptions: ['npmInstallTwice'] },
|
|
[{ isLockFileMaintenance: true }],
|
|
);
|
|
expect(fs.deleteLocalFile).toHaveBeenCalledTimes(1);
|
|
expect(res.lockFile).toBe('package-lock-contents');
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
options: { cwd: 'some-dir' },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('works for docker mode', async () => {
|
|
GlobalConfig.set({
|
|
localDir: '',
|
|
cacheDir: '/tmp',
|
|
binarySource: 'docker',
|
|
allowScripts: true,
|
|
dockerSidecarImage: 'ghcr.io/renovatebot/base-image',
|
|
});
|
|
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/renovatebot/base-image' },
|
|
{ cmd: 'docker ps --filter name=renovate_sidecar -aq' },
|
|
{
|
|
cmd:
|
|
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
|
|
'-v "/tmp":"/tmp" ' +
|
|
'-e CI ' +
|
|
'-e CONTAINERBASE_CACHE_DIR ' +
|
|
'-w "some-dir" ' +
|
|
'ghcr.io/renovatebot/base-image ' +
|
|
"bash -l -c '" +
|
|
'install-tool node 16.16.0 ' +
|
|
'&& ' +
|
|
'install-tool npm 6.0.0 ' +
|
|
'&& ' +
|
|
'npm install --package-lock-only --no-audit ' +
|
|
'&& ' +
|
|
'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: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('does not install npm if no constraints specified', 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: {} },
|
|
[{ isLockFileMaintenance: true }],
|
|
);
|
|
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
|
|
expect(res.lockFile).toBe('package-lock-contents');
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: 'install-tool node 16.16.0' },
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe('passes NODE_OPTIONS', () => {
|
|
it('if nodeMaxMemory set on global config', async () => {
|
|
GlobalConfig.set({
|
|
localDir: '',
|
|
toolSettings: {
|
|
nodeMaxMemory: 3456,
|
|
},
|
|
binarySource: 'global',
|
|
});
|
|
|
|
const execSnapshots = mockExecAll();
|
|
// package.json
|
|
fs.readLocalFile.mockResolvedValueOnce('{}');
|
|
const packageLockContents = JSON.stringify({
|
|
packages: {},
|
|
lockfileVersion: 3,
|
|
});
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(packageLockContents)
|
|
.mockResolvedValueOnce(packageLockContents);
|
|
const skipInstalls = true;
|
|
const updates = [
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
];
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls },
|
|
updates,
|
|
);
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
|
|
expect(execSnapshots[0].options?.env?.NODE_OPTIONS).toEqual(
|
|
'--max-old-space-size=3456',
|
|
);
|
|
});
|
|
|
|
it('if nodeMaxMemory set on repo config', async () => {
|
|
const execSnapshots = mockExecAll();
|
|
// package.json
|
|
fs.readLocalFile.mockResolvedValueOnce('{}');
|
|
const packageLockContents = JSON.stringify({
|
|
packages: {},
|
|
lockfileVersion: 3,
|
|
});
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(packageLockContents)
|
|
.mockResolvedValueOnce(packageLockContents);
|
|
const skipInstalls = true;
|
|
const updates = [
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
];
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{
|
|
skipInstalls,
|
|
toolSettings: {
|
|
nodeMaxMemory: 3456,
|
|
},
|
|
},
|
|
updates,
|
|
);
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
|
|
expect(execSnapshots[0].options?.env?.NODE_OPTIONS).toEqual(
|
|
'--max-old-space-size=3456',
|
|
);
|
|
});
|
|
});
|
|
|
|
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']),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('prevents injections', () => {
|
|
it('while performing lockfileUpdate (npm-workspaces)', 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 },
|
|
[
|
|
{
|
|
packageFile: 'some-dir/web/b/package.json',
|
|
packageName: ' && echo "hi";',
|
|
depType: 'dependencies',
|
|
newVersion: '2.2.0',
|
|
newValue: '^2.0.0',
|
|
isLockfileUpdate: true,
|
|
managerData: {
|
|
workspacesPackages: ['docs/*', 'web/*'],
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'some-dir/docs/a || date; /package.json',
|
|
packageName: 'hello',
|
|
depType: 'dependencies',
|
|
newVersion: '1.1.1',
|
|
newValue: '^1.0.0',
|
|
isLockfileUpdate: true,
|
|
managerData: {
|
|
workspacesPackages: ['docs/*', 'web/*'],
|
|
},
|
|
},
|
|
],
|
|
);
|
|
expect(fs.readLocalFile).toHaveBeenCalledTimes(3);
|
|
expect(res.error).toBeFalse();
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: `npm install --package-lock-only --no-audit --ignore-scripts --workspace=web/b ' && echo "hi";@2.2.0'`,
|
|
},
|
|
{
|
|
cmd: `npm install --package-lock-only --no-audit --ignore-scripts --workspace='docs/a || date; ' hello@1.1.1`,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('while performing lockfileUpdate (npm)', async () => {
|
|
const execSnapshots = mockExecAll();
|
|
// package.json
|
|
fs.readLocalFile.mockResolvedValue('{}');
|
|
fs.readLocalFile.mockResolvedValue('package-lock-contents');
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{},
|
|
[
|
|
{
|
|
depName: 'uuid',
|
|
currentVersion: '^11.0.0',
|
|
newVersion: '11.1.0',
|
|
packageName: '; date; echo ',
|
|
isLockfileUpdate: true,
|
|
},
|
|
],
|
|
);
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: "npm install --package-lock-only --no-audit --ignore-scripts '; date; echo @11.1.0'",
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('--before with minimumReleaseAge', () => {
|
|
let execSnapshots: ReturnType<typeof mockExecAll>;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-06-15T12:00:00.000Z'));
|
|
execSnapshots = mockExecAll();
|
|
fs.readLocalFile.mockResolvedValueOnce('{}');
|
|
const packageLockContents = JSON.stringify({
|
|
packages: {},
|
|
lockfileVersion: 3,
|
|
});
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(packageLockContents)
|
|
.mockResolvedValueOnce(packageLockContents);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('sets --before from minimumReleaseAge', async () => {
|
|
const res = await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: '3 days' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(res.error).toBeFalse();
|
|
expect(res.beforeFallback).toBeFalse();
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-12T12:00:00.000Z',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('skips --before on unparseable minimumReleaseAge', async () => {
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: 'invalid garbage' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('uses stricter npmrc before date when older than minimumReleaseAge', async () => {
|
|
// npmrc (June 1) is earlier than minimumReleaseAge (3 days = June 12)
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: '3 days' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
'registry=https://registry.npmjs.org\nbefore=2026-06-01T00:00:00.000Z\n',
|
|
);
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-01T00:00:00.000Z',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('uses minimumReleaseAge date when stricter than npmrc before date', async () => {
|
|
// minimumReleaseAge (3 days = June 12) is earlier than npmrc (June 14)
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: '3 days' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
'before=2026-06-14T00:00:00.000Z\n',
|
|
);
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-12T12:00:00.000Z',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('skips --before when minimumReleaseAge is absent even if npmrc has before', async () => {
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
'before=2026-06-01T00:00:00.000Z\n',
|
|
);
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('skips --before when .npmrc has min-release-age to avoid npm conflict', async () => {
|
|
await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: '3 days' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
'min-release-age=30\n',
|
|
);
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('retries without --before on ETARGET with "with a date before"', async () => {
|
|
const etargetError = new ExecError('npm error code ETARGET', {
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-12T12:00:00.000Z',
|
|
stdout: '',
|
|
stderr:
|
|
'npm error code ETARGET\nnpm error notarget No matching version found for @scope/pkg@1.2.3 with a date before 6/12/2026, 12:00:00 PM.',
|
|
options: {},
|
|
});
|
|
const packageLockContents = JSON.stringify({
|
|
packages: {},
|
|
lockfileVersion: 3,
|
|
});
|
|
execSnapshots = mockExecSequence([
|
|
etargetError,
|
|
{ stdout: '', stderr: '' },
|
|
]);
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(packageLockContents)
|
|
.mockResolvedValueOnce(packageLockContents);
|
|
|
|
const res = await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: '3 days' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(res.error).toBeFalse();
|
|
expect(res.beforeFallback).toBeTrue();
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-12T12:00:00.000Z',
|
|
},
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('does not retry on non-before ETARGET errors', async () => {
|
|
const otherError = new ExecError('npm error code ETARGET', {
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-12T12:00:00.000Z',
|
|
stdout: '',
|
|
stderr:
|
|
'npm error code ETARGET\nnpm error notarget No matching version found for @scope/pkg@999.999.999.',
|
|
options: {},
|
|
});
|
|
execSnapshots = mockExecSequence([otherError]);
|
|
|
|
const res = await npmHelper.generateLockFile(
|
|
'some-dir',
|
|
{},
|
|
'package-lock.json',
|
|
{ skipInstalls: true, minimumReleaseAge: '3 days' },
|
|
[
|
|
{
|
|
packageName: 'some-dep',
|
|
newVersion: '1.0.1',
|
|
isLockfileUpdate: false,
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(res.error).toBeTrue();
|
|
expect(res.beforeFallback).toBeUndefined();
|
|
expect(execSnapshots).toMatchObject([
|
|
{
|
|
cmd: 'npm install --package-lock-only --no-audit --ignore-scripts --before=2026-06-12T12:00:00.000Z',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('parseNpmrcCooldownDate', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-06-15T12:00:00.000Z'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('returns null', () => {
|
|
it.each`
|
|
content
|
|
${null}
|
|
${''}
|
|
${'registry=https://registry.npmjs.org\n'}
|
|
${'before=not-a-date\n'}
|
|
${'before=2026-13-99T00:00:00.000Z\n'}
|
|
${'min-release-age=not-a-number\n'}
|
|
`('for: $content', ({ content }: { content: string | null }) => {
|
|
expect(npmHelper.parseNpmrcCooldownDate(content)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parses before= key', () => {
|
|
it.each`
|
|
input
|
|
${'before=2026-06-01T00:00:00.000Z\n'}
|
|
${'before="2026-06-01T00:00:00.000Z"\n'}
|
|
${'registry=https://registry.npmjs.org\nbefore=2026-06-01T00:00:00.000Z # some comment\naudit=false\n'}
|
|
`('$input', ({ input }: { input: string }) => {
|
|
const result = npmHelper.parseNpmrcCooldownDate(input);
|
|
expect(result?.date.toISO()).toBe('2026-06-01T00:00:00.000Z');
|
|
expect(result?.source).toBe('before');
|
|
});
|
|
});
|
|
|
|
describe('parses min-release-age= key', () => {
|
|
it.each`
|
|
input
|
|
${'min-release-age=30\n'}
|
|
${'min-release-age="30"\n'}
|
|
${'min-release-age=30 # 30 days\n'}
|
|
${'registry=https://registry.npmjs.org\nmin-release-age=30 # 30 days\n'}
|
|
`('$input', ({ input }: { input: string }) => {
|
|
const result = npmHelper.parseNpmrcCooldownDate(input);
|
|
expect(result?.date.toISO()).toBe('2026-05-16T12:00:00.000Z');
|
|
expect(result?.source).toBe('min-release-age');
|
|
});
|
|
});
|
|
});
|
|
});
|