PR-URL: https://github.com/nodejs/node/pull/63857 Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Jordan Harband <ljharb@gmail.com>
594 lines
16 KiB
JavaScript
594 lines
16 KiB
JavaScript
const t = require('tap')
|
|
const mockNpm = require('../../fixtures/mock-npm')
|
|
const reifyOutput = require('../../../lib/utils/reify-output.js')
|
|
|
|
t.cleanSnapshot = str => str.replace(/in [0-9]+m?s/g, 'in {TIME}')
|
|
|
|
const mockReify = async (t, reify, { command, ...config } = {}) => {
|
|
const mock = await mockNpm(t, {
|
|
command,
|
|
config,
|
|
})
|
|
|
|
// Hack to adapt existing fake test. Make npm.command
|
|
// return whatever was passed in to this function.
|
|
// What it should be doing is npm.exec(command) but that
|
|
// breaks most of these tests because they don't expect
|
|
// a command to actually run.
|
|
Object.defineProperty(mock.npm, 'command', {
|
|
get () {
|
|
return command
|
|
},
|
|
enumerable: true,
|
|
})
|
|
|
|
reifyOutput(mock.npm, reify)
|
|
mock.npm.finish()
|
|
|
|
return mock.joinedOutput()
|
|
}
|
|
|
|
t.test('missing info', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: {
|
|
children: [],
|
|
},
|
|
diff: {
|
|
children: [],
|
|
},
|
|
})
|
|
|
|
t.notMatch(
|
|
out,
|
|
'looking for funding',
|
|
'should not print fund message if missing info'
|
|
)
|
|
})
|
|
|
|
t.test('even more missing info', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: {
|
|
children: [],
|
|
},
|
|
})
|
|
|
|
t.notMatch(
|
|
out,
|
|
'looking for funding',
|
|
'should not print fund message if missing info'
|
|
)
|
|
})
|
|
|
|
t.test('single package', async t => {
|
|
const out = await mockReify(t, {
|
|
// a report with an error is the same as no report at all, if
|
|
// the command is not 'audit'
|
|
auditReport: {
|
|
error: {
|
|
message: 'no audit for youuuuu',
|
|
},
|
|
},
|
|
actualTree: {
|
|
name: 'foo',
|
|
package: {
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
},
|
|
edgesOut: new Map([
|
|
['bar', {
|
|
to: {
|
|
name: 'bar',
|
|
package: {
|
|
name: 'bar',
|
|
version: '1.0.0',
|
|
funding: { type: 'foo', url: 'http://example.com' },
|
|
},
|
|
},
|
|
}],
|
|
]),
|
|
},
|
|
diff: {
|
|
children: [],
|
|
},
|
|
})
|
|
|
|
t.match(
|
|
out,
|
|
'1 package is looking for funding',
|
|
'should print single package message'
|
|
)
|
|
})
|
|
|
|
t.test('no message when funding config is false', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: {
|
|
name: 'foo',
|
|
package: {
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
},
|
|
edgesOut: new Map([
|
|
['bar', {
|
|
to: {
|
|
name: 'bar',
|
|
package: {
|
|
name: 'bar',
|
|
version: '1.0.0',
|
|
funding: { type: 'foo', url: 'http://example.com' },
|
|
},
|
|
},
|
|
}],
|
|
]),
|
|
},
|
|
diff: {
|
|
children: [],
|
|
},
|
|
}, { fund: false })
|
|
|
|
t.notMatch(out, 'looking for funding', 'should not print funding info')
|
|
})
|
|
|
|
t.test('print appropriate message for many packages', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: {
|
|
name: 'foo',
|
|
package: {
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
},
|
|
edgesOut: new Map([
|
|
['bar', {
|
|
to: {
|
|
name: 'bar',
|
|
package: {
|
|
name: 'bar',
|
|
version: '1.0.0',
|
|
funding: { type: 'foo', url: 'http://example.com' },
|
|
},
|
|
},
|
|
}],
|
|
['lorem', {
|
|
to: {
|
|
name: 'lorem',
|
|
package: {
|
|
name: 'lorem',
|
|
version: '1.0.0',
|
|
funding: { type: 'foo', url: 'http://example.com' },
|
|
},
|
|
},
|
|
}],
|
|
['ipsum', {
|
|
to: {
|
|
name: 'ipsum',
|
|
package: {
|
|
name: 'ipsum',
|
|
version: '1.0.0',
|
|
funding: { type: 'foo', url: 'http://example.com' },
|
|
},
|
|
},
|
|
}],
|
|
]),
|
|
},
|
|
diff: {
|
|
children: [],
|
|
},
|
|
})
|
|
|
|
t.match(
|
|
out,
|
|
'3 packages are looking for funding',
|
|
'should print single package message'
|
|
)
|
|
})
|
|
|
|
t.test('showing and not showing audit report', async t => {
|
|
const auditReport = {
|
|
toJSON: () => auditReport,
|
|
auditReportVersion: 2,
|
|
vulnerabilities: {
|
|
minimist: {
|
|
name: 'minimist',
|
|
severity: 'low',
|
|
via: [
|
|
{
|
|
id: 1179,
|
|
url: 'https://npmjs.com/advisories/1179',
|
|
title: 'Prototype Pollution',
|
|
severity: 'low',
|
|
vulnerable_versions: '<0.2.1 || >=1.0.0 <1.2.3',
|
|
},
|
|
],
|
|
effects: [],
|
|
range: '<0.2.1 || >=1.0.0 <1.2.3',
|
|
nodes: [
|
|
'node_modules/minimist',
|
|
],
|
|
fixAvailable: true,
|
|
},
|
|
},
|
|
metadata: {
|
|
vulnerabilities: {
|
|
info: 0,
|
|
low: 1,
|
|
moderate: 0,
|
|
high: 0,
|
|
critical: 0,
|
|
total: 1,
|
|
},
|
|
dependencies: {
|
|
prod: 1,
|
|
dev: 0,
|
|
optional: 0,
|
|
peer: 0,
|
|
peerOptional: 0,
|
|
total: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
t.test('no output when silent', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: { inventory: { size: 999 }, children: [] },
|
|
auditReport,
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { location: 'loc' } },
|
|
],
|
|
},
|
|
}, { silent: true })
|
|
t.equal(out, '', 'should not get output when silent')
|
|
})
|
|
|
|
t.test('output when not silent', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: { inventory: new Map(), children: [] },
|
|
auditReport,
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { location: 'loc' } },
|
|
],
|
|
},
|
|
})
|
|
|
|
t.match(out, /Run `npm audit` for details\.$/, 'got audit report')
|
|
})
|
|
|
|
for (const json of [true, false]) {
|
|
t.test(`json=${json}`, async t => {
|
|
t.test('set exit code when cmd is audit', async t => {
|
|
await mockReify(t, {
|
|
actualTree: { inventory: new Map(), children: [] },
|
|
auditReport,
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { location: 'loc' } },
|
|
],
|
|
},
|
|
}, { command: 'audit', 'audit-level': 'low' })
|
|
|
|
t.equal(process.exitCode, 1, 'set exit code')
|
|
})
|
|
|
|
t.test('do not set exit code when cmd is install', async t => {
|
|
await mockReify(t, {
|
|
actualTree: { inventory: new Map(), children: [] },
|
|
auditReport,
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { location: 'loc' } },
|
|
],
|
|
},
|
|
}, { command: 'install', 'audit-level': 'low' })
|
|
|
|
t.notOk(process.exitCode, 'did not set exit code')
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
t.test('packages changed message', async t => {
|
|
// return a test function that builds up the mock and snapshots output
|
|
const testCase = async (t, added, removed, changed, audited, json, command) => {
|
|
const mock = {
|
|
actualTree: {
|
|
inventory: { size: audited, has: () => true },
|
|
children: [],
|
|
},
|
|
auditReport: audited ? {
|
|
toJSON: () => mock.auditReport,
|
|
vulnerabilities: {},
|
|
metadata: {
|
|
vulnerabilities: {
|
|
total: 0,
|
|
},
|
|
},
|
|
} : null,
|
|
diff: {
|
|
children: [
|
|
{ action: 'some random unexpected junk' },
|
|
],
|
|
},
|
|
}
|
|
for (let i = 0; i < added; i++) {
|
|
mock.diff.children.push({ action: 'ADD', ideal: { path: `test/${i}`, name: `@npmcli/pkg${i}`, location: 'loc', package: { version: `1.0.${i}` } } })
|
|
}
|
|
|
|
for (let i = 0; i < removed; i++) {
|
|
mock.diff.children.push({ action: 'REMOVE', actual: { path: `test/${i}`, name: `@npmcli/pkg${i}`, location: 'loc', package: { version: `1.0.${i}` } } })
|
|
}
|
|
|
|
for (let i = 0; i < changed; i++) {
|
|
const actual = { path: `test/a/${i}`, name: `@npmcli/pkg${i}`, location: 'loc', package: { version: `1.0.${i}` } }
|
|
const ideal = { path: `test/i/${i}`, name: `@npmcli/pkg${i}`, location: 'loc', package: { version: `1.1.${i}` } }
|
|
mock.diff.children.push({ action: 'CHANGE', actual, ideal })
|
|
}
|
|
|
|
const out = await mockReify(t, mock, { json, command })
|
|
t.matchSnapshot(out, JSON.stringify({
|
|
added,
|
|
removed,
|
|
changed,
|
|
audited,
|
|
json,
|
|
}))
|
|
}
|
|
|
|
const cases = []
|
|
for (const added of [0, 1, 2]) {
|
|
for (const removed of [0, 1, 2]) {
|
|
for (const changed of [0, 1, 2]) {
|
|
for (const audited of [0, 1, 2]) {
|
|
for (const json of [true, false]) {
|
|
cases.push([added, removed, changed, audited, json, 'install'])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// add case for when audit is the command
|
|
cases.push([0, 0, 0, 2, true, 'audit'])
|
|
cases.push([0, 0, 0, 2, false, 'audit'])
|
|
|
|
for (const c of cases) {
|
|
await t.test('', t => testCase(t, ...c))
|
|
}
|
|
})
|
|
|
|
t.test('added packages should be looked up within returned tree', async t => {
|
|
t.test('has added pkg in inventory', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: {
|
|
name: 'foo',
|
|
inventory: {
|
|
has: () => true,
|
|
},
|
|
},
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { path: 'test/baz', name: 'baz', package: { version: '1.0.0' } } },
|
|
],
|
|
},
|
|
})
|
|
|
|
t.matchSnapshot(out)
|
|
})
|
|
|
|
t.test('missing added pkg in inventory', async t => {
|
|
const out = await mockReify(t, {
|
|
actualTree: {
|
|
name: 'foo',
|
|
inventory: {
|
|
has: () => false,
|
|
},
|
|
},
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { path: 'test/baz', name: 'baz', package: { version: '1.0.0' } } },
|
|
],
|
|
},
|
|
})
|
|
|
|
t.matchSnapshot(out)
|
|
})
|
|
})
|
|
|
|
t.test('prints dedupe difference on dry-run', async t => {
|
|
const mock = {
|
|
actualTree: {
|
|
name: 'foo',
|
|
inventory: {
|
|
has: () => false,
|
|
},
|
|
},
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { path: 'test/foo', name: 'foo', package: { version: '1.0.0' } } },
|
|
{ action: 'REMOVE', actual: { path: 'test/foo', name: 'bar', package: { version: '1.0.0' } } },
|
|
{
|
|
action: 'CHANGE',
|
|
actual: { path: 'test/a/bar', name: 'bar', package: { version: '1.0.0' } },
|
|
ideal: { path: 'test/i/bar', name: 'bar', package: { version: '2.1.0' } },
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
const out = await mockReify(t, mock, {
|
|
'dry-run': true,
|
|
})
|
|
|
|
t.matchSnapshot(out, 'diff table')
|
|
})
|
|
|
|
t.test('prints dedupe difference on long', async t => {
|
|
const mock = {
|
|
actualTree: {
|
|
name: 'foo',
|
|
inventory: {
|
|
has: () => false,
|
|
},
|
|
},
|
|
diff: {
|
|
children: [
|
|
{ action: 'ADD', ideal: { path: 'test/foo', name: 'foo', package: { version: '1.0.0' } } },
|
|
{ action: 'REMOVE', actual: { path: 'test/bar', name: 'bar', package: { version: '1.0.0' } } },
|
|
{
|
|
action: 'CHANGE',
|
|
actual: { path: 'test/a/bar', name: 'bar', package: { version: '1.0.0' } },
|
|
ideal: { path: 'test/i/bar', name: 'bar', package: { version: '2.1.0' } },
|
|
},
|
|
],
|
|
},
|
|
}
|
|
|
|
const out = await mockReify(t, mock, {
|
|
long: true,
|
|
})
|
|
|
|
t.matchSnapshot(out, 'diff table')
|
|
})
|
|
|
|
t.test('prints unreviewed install scripts summary', async t => {
|
|
const mockReifyWithExtras = async (t, reify, extras, { command, ...config } = {}) => {
|
|
const mock = await mockNpm(t, { command, config })
|
|
Object.defineProperty(mock.npm, 'command', {
|
|
get () {
|
|
return command
|
|
},
|
|
enumerable: true,
|
|
})
|
|
reifyOutput(mock.npm, reify, extras)
|
|
mock.npm.finish()
|
|
return mock
|
|
}
|
|
|
|
const baseReify = {
|
|
actualTree: { name: 'host', inventory: { has: () => false } },
|
|
diff: { children: [] },
|
|
}
|
|
|
|
const unreviewedScripts = [
|
|
{
|
|
node: { packageName: 'canvas', name: 'canvas', version: '2.11.0', path: '/x/canvas' },
|
|
scripts: { install: 'node-gyp rebuild' },
|
|
},
|
|
{
|
|
node: { packageName: 'sharp', name: 'sharp', version: '0.33.2', path: '/x/sharp' },
|
|
scripts: { preinstall: 'pre', postinstall: 'post' },
|
|
},
|
|
]
|
|
|
|
const mock = await mockReifyWithExtras(t, baseReify, { unreviewedScripts })
|
|
const warn = mock.logs.warn.byTitle('allow-scripts').join('\n')
|
|
t.match(warn, /2 packages have install scripts not yet covered/)
|
|
t.match(warn, /canvas@2\.11\.0 \(install: node-gyp rebuild\)/)
|
|
t.match(warn, /sharp@0\.33\.2 \(preinstall: pre; postinstall: post\)/)
|
|
t.match(warn, /npm approve-scripts --allow-scripts-pending/)
|
|
})
|
|
|
|
t.test('global install suggests --allow-scripts, not approve-scripts', async t => {
|
|
const mockReifyWithExtras = async (t, reify, extras, config = {}) => {
|
|
const mock = await mockNpm(t, { config })
|
|
reifyOutput(mock.npm, reify, extras)
|
|
mock.npm.finish()
|
|
return mock
|
|
}
|
|
|
|
const baseReify = {
|
|
actualTree: { name: 'host', inventory: { has: () => false } },
|
|
diff: { children: [] },
|
|
}
|
|
|
|
const unreviewedScripts = [
|
|
{
|
|
node: { packageName: 'canvas', name: 'canvas', version: '2.11.0', path: '/x/canvas' },
|
|
scripts: { install: 'node-gyp rebuild' },
|
|
},
|
|
{
|
|
node: { packageName: 'sharp', name: 'sharp', version: '0.33.2', path: '/x/sharp' },
|
|
scripts: { preinstall: 'pre', postinstall: 'post' },
|
|
},
|
|
]
|
|
|
|
const mock = await mockReifyWithExtras(t, baseReify, { unreviewedScripts }, { global: true })
|
|
const warn = mock.logs.warn.byTitle('allow-scripts').join('\n')
|
|
t.match(warn, /2 packages have install scripts not yet covered/)
|
|
t.match(warn, /canvas@2\.11\.0 \(install: node-gyp rebuild\)/)
|
|
t.match(warn, /npm install -g --allow-scripts=canvas,sharp/)
|
|
t.match(warn, /npm config set allow-scripts=canvas,sharp/)
|
|
t.notMatch(warn, /approve-scripts/)
|
|
})
|
|
|
|
t.test('single unreviewed script uses singular wording', async t => {
|
|
const mockReifyWithExtras = async (t, reify, extras) => {
|
|
const mock = await mockNpm(t, {})
|
|
reifyOutput(mock.npm, reify, extras)
|
|
mock.npm.finish()
|
|
return mock
|
|
}
|
|
|
|
const mock = await mockReifyWithExtras(
|
|
t,
|
|
{ actualTree: { inventory: { has: () => false } }, diff: { children: [] } },
|
|
{
|
|
unreviewedScripts: [{
|
|
node: { packageName: 'one', name: 'one', version: '1.0.0', path: '/x' },
|
|
scripts: { install: 'do' },
|
|
}],
|
|
}
|
|
)
|
|
t.match(mock.logs.warn.byTitle('allow-scripts').join('\n'), /1 package has install scripts/)
|
|
})
|
|
|
|
t.test('json output includes unreviewedScripts', async t => {
|
|
const mock = await mockNpm(t, { config: { json: true } })
|
|
reifyOutput(mock.npm, {
|
|
actualTree: { inventory: { size: 0 } },
|
|
diff: null,
|
|
}, {
|
|
unreviewedScripts: [{
|
|
node: { packageName: 'pkg', name: 'pkg', version: '1.0.0', path: '/x' },
|
|
scripts: { install: 'cmd' },
|
|
}],
|
|
})
|
|
mock.npm.finish()
|
|
const parsed = JSON.parse(mock.joinedOutput())
|
|
t.match(parsed.unreviewedScripts, [{
|
|
name: 'pkg',
|
|
version: '1.0.0',
|
|
path: '/x',
|
|
scripts: { install: 'cmd' },
|
|
}])
|
|
})
|
|
|
|
t.test('unreviewed script with node.name only (no packageName) still renders', async t => {
|
|
const mock = await mockNpm(t, {})
|
|
reifyOutput(mock.npm, {
|
|
actualTree: { inventory: { has: () => false } },
|
|
diff: { children: [] },
|
|
}, {
|
|
unreviewedScripts: [{
|
|
node: { name: 'fallback', path: '/x' }, // no packageName, no version
|
|
scripts: { install: 'cmd' },
|
|
}],
|
|
})
|
|
mock.npm.finish()
|
|
t.match(mock.logs.warn.byTitle('allow-scripts').join('\n'), / fallback \(install: cmd\)/)
|
|
})
|
|
|
|
t.test('json output includes node.name when packageName is missing', async t => {
|
|
const mock = await mockNpm(t, { config: { json: true } })
|
|
reifyOutput(mock.npm, {
|
|
actualTree: { inventory: { size: 0 } },
|
|
diff: null,
|
|
}, {
|
|
unreviewedScripts: [{
|
|
node: { name: 'fallback', path: '/x' },
|
|
scripts: { install: 'cmd' },
|
|
}],
|
|
})
|
|
mock.npm.finish()
|
|
const parsed = JSON.parse(mock.joinedOutput())
|
|
t.equal(parsed.unreviewedScripts[0].name, 'fallback')
|
|
})
|