1const t = require('tap') 2const mockNpm = require('../../fixtures/mock-npm') 3const reifyOutput = require('../../../lib/utils/reify-output.js') 4 5t.cleanSnapshot = str => str.replace(/in [0-9]+m?s/g, 'in {TIME}') 6 7const mockReify = async (t, reify, { command, ...config } = {}) => { 8 const mock = await mockNpm(t, { 9 command, 10 config, 11 setCmd: true, 12 }) 13 14 reifyOutput(mock.npm, reify) 15 16 return mock.joinedOutput() 17} 18 19t.test('missing info', async t => { 20 const out = await mockReify(t, { 21 actualTree: { 22 children: [], 23 }, 24 diff: { 25 children: [], 26 }, 27 }) 28 29 t.notMatch( 30 out, 31 'looking for funding', 32 'should not print fund message if missing info' 33 ) 34}) 35 36t.test('even more missing info', async t => { 37 const out = await mockReify(t, { 38 actualTree: { 39 children: [], 40 }, 41 }) 42 43 t.notMatch( 44 out, 45 'looking for funding', 46 'should not print fund message if missing info' 47 ) 48}) 49 50t.test('single package', async t => { 51 const out = await mockReify(t, { 52 // a report with an error is the same as no report at all, if 53 // the command is not 'audit' 54 auditReport: { 55 error: { 56 message: 'no audit for youuuuu', 57 }, 58 }, 59 actualTree: { 60 name: 'foo', 61 package: { 62 name: 'foo', 63 version: '1.0.0', 64 }, 65 edgesOut: new Map([ 66 ['bar', { 67 to: { 68 name: 'bar', 69 package: { 70 name: 'bar', 71 version: '1.0.0', 72 funding: { type: 'foo', url: 'http://example.com' }, 73 }, 74 }, 75 }], 76 ]), 77 }, 78 diff: { 79 children: [], 80 }, 81 }) 82 83 t.match( 84 out, 85 '1 package is looking for funding', 86 'should print single package message' 87 ) 88}) 89 90t.test('no message when funding config is false', async t => { 91 const out = await mockReify(t, { 92 actualTree: { 93 name: 'foo', 94 package: { 95 name: 'foo', 96 version: '1.0.0', 97 }, 98 edgesOut: new Map([ 99 ['bar', { 100 to: { 101 name: 'bar', 102 package: { 103 name: 'bar', 104 version: '1.0.0', 105 funding: { type: 'foo', url: 'http://example.com' }, 106 }, 107 }, 108 }], 109 ]), 110 }, 111 diff: { 112 children: [], 113 }, 114 }, { fund: false }) 115 116 t.notMatch(out, 'looking for funding', 'should not print funding info') 117}) 118 119t.test('print appropriate message for many packages', async t => { 120 const out = await mockReify(t, { 121 actualTree: { 122 name: 'foo', 123 package: { 124 name: 'foo', 125 version: '1.0.0', 126 }, 127 edgesOut: new Map([ 128 ['bar', { 129 to: { 130 name: 'bar', 131 package: { 132 name: 'bar', 133 version: '1.0.0', 134 funding: { type: 'foo', url: 'http://example.com' }, 135 }, 136 }, 137 }], 138 ['lorem', { 139 to: { 140 name: 'lorem', 141 package: { 142 name: 'lorem', 143 version: '1.0.0', 144 funding: { type: 'foo', url: 'http://example.com' }, 145 }, 146 }, 147 }], 148 ['ipsum', { 149 to: { 150 name: 'ipsum', 151 package: { 152 name: 'ipsum', 153 version: '1.0.0', 154 funding: { type: 'foo', url: 'http://example.com' }, 155 }, 156 }, 157 }], 158 ]), 159 }, 160 diff: { 161 children: [], 162 }, 163 }) 164 165 t.match( 166 out, 167 '3 packages are looking for funding', 168 'should print single package message' 169 ) 170}) 171 172t.test('showing and not showing audit report', async t => { 173 const auditReport = { 174 toJSON: () => auditReport, 175 auditReportVersion: 2, 176 vulnerabilities: { 177 minimist: { 178 name: 'minimist', 179 severity: 'low', 180 via: [ 181 { 182 id: 1179, 183 url: 'https://npmjs.com/advisories/1179', 184 title: 'Prototype Pollution', 185 severity: 'low', 186 vulnerable_versions: '<0.2.1 || >=1.0.0 <1.2.3', 187 }, 188 ], 189 effects: [], 190 range: '<0.2.1 || >=1.0.0 <1.2.3', 191 nodes: [ 192 'node_modules/minimist', 193 ], 194 fixAvailable: true, 195 }, 196 }, 197 metadata: { 198 vulnerabilities: { 199 info: 0, 200 low: 1, 201 moderate: 0, 202 high: 0, 203 critical: 0, 204 total: 1, 205 }, 206 dependencies: { 207 prod: 1, 208 dev: 0, 209 optional: 0, 210 peer: 0, 211 peerOptional: 0, 212 total: 1, 213 }, 214 }, 215 } 216 217 t.test('no output when silent', async t => { 218 const out = await mockReify(t, { 219 actualTree: { inventory: { size: 999 }, children: [] }, 220 auditReport, 221 diff: { 222 children: [ 223 { action: 'ADD', ideal: { location: 'loc' } }, 224 ], 225 }, 226 }, { silent: true }) 227 t.equal(out, '', 'should not get output when silent') 228 }) 229 230 t.test('output when not silent', async t => { 231 const out = await mockReify(t, { 232 actualTree: { inventory: new Map(), children: [] }, 233 auditReport, 234 diff: { 235 children: [ 236 { action: 'ADD', ideal: { location: 'loc' } }, 237 ], 238 }, 239 }) 240 241 t.match(out, /Run `npm audit` for details\.$/, 'got audit report') 242 }) 243 244 for (const json of [true, false]) { 245 t.test(`json=${json}`, async t => { 246 t.test('set exit code when cmd is audit', async t => { 247 await mockReify(t, { 248 actualTree: { inventory: new Map(), children: [] }, 249 auditReport, 250 diff: { 251 children: [ 252 { action: 'ADD', ideal: { location: 'loc' } }, 253 ], 254 }, 255 }, { command: 'audit', 'audit-level': 'low' }) 256 257 t.equal(process.exitCode, 1, 'set exit code') 258 }) 259 260 t.test('do not set exit code when cmd is install', async t => { 261 await mockReify(t, { 262 actualTree: { inventory: new Map(), children: [] }, 263 auditReport, 264 diff: { 265 children: [ 266 { action: 'ADD', ideal: { location: 'loc' } }, 267 ], 268 }, 269 }, { command: 'install', 'audit-level': 'low' }) 270 271 t.notOk(process.exitCode, 'did not set exit code') 272 }) 273 }) 274 } 275}) 276 277t.test('packages changed message', async t => { 278 // return a test function that builds up the mock and snapshots output 279 const testCase = async (t, added, removed, changed, audited, json, command) => { 280 const mock = { 281 actualTree: { 282 inventory: { size: audited, has: () => true }, 283 children: [], 284 }, 285 auditReport: audited ? { 286 toJSON: () => mock.auditReport, 287 vulnerabilities: {}, 288 metadata: { 289 vulnerabilities: { 290 total: 0, 291 }, 292 }, 293 } : null, 294 diff: { 295 children: [ 296 { action: 'some random unexpected junk' }, 297 ], 298 }, 299 } 300 for (let i = 0; i < added; i++) { 301 mock.diff.children.push({ action: 'ADD', ideal: { location: 'loc' } }) 302 } 303 304 for (let i = 0; i < removed; i++) { 305 mock.diff.children.push({ action: 'REMOVE', actual: { location: 'loc' } }) 306 } 307 308 for (let i = 0; i < changed; i++) { 309 const actual = { location: 'loc' } 310 const ideal = { location: 'loc' } 311 mock.diff.children.push({ action: 'CHANGE', actual, ideal }) 312 } 313 314 const out = await mockReify(t, mock, { json, command }) 315 t.matchSnapshot(out, JSON.stringify({ 316 added, 317 removed, 318 changed, 319 audited, 320 json, 321 })) 322 } 323 324 const cases = [] 325 for (const added of [0, 1, 2]) { 326 for (const removed of [0, 1, 2]) { 327 for (const changed of [0, 1, 2]) { 328 for (const audited of [0, 1, 2]) { 329 for (const json of [true, false]) { 330 cases.push([added, removed, changed, audited, json, 'install']) 331 } 332 } 333 } 334 } 335 } 336 337 // add case for when audit is the command 338 cases.push([0, 0, 0, 2, true, 'audit']) 339 cases.push([0, 0, 0, 2, false, 'audit']) 340 341 for (const c of cases) { 342 await t.test('', t => testCase(t, ...c)) 343 } 344}) 345 346t.test('added packages should be looked up within returned tree', async t => { 347 t.test('has added pkg in inventory', async t => { 348 const out = await mockReify(t, { 349 actualTree: { 350 name: 'foo', 351 inventory: { 352 has: () => true, 353 }, 354 }, 355 diff: { 356 children: [ 357 { action: 'ADD', ideal: { name: 'baz' } }, 358 ], 359 }, 360 }) 361 362 t.matchSnapshot(out) 363 }) 364 365 t.test('missing added pkg in inventory', async t => { 366 const out = await mockReify(t, { 367 actualTree: { 368 name: 'foo', 369 inventory: { 370 has: () => false, 371 }, 372 }, 373 diff: { 374 children: [ 375 { action: 'ADD', ideal: { name: 'baz' } }, 376 ], 377 }, 378 }) 379 380 t.matchSnapshot(out) 381 }) 382}) 383 384t.test('prints dedupe difference on dry-run', async t => { 385 const mock = { 386 actualTree: { 387 name: 'foo', 388 inventory: { 389 has: () => false, 390 }, 391 }, 392 diff: { 393 children: [ 394 { action: 'ADD', ideal: { name: 'foo', package: { version: '1.0.0' } } }, 395 { action: 'REMOVE', actual: { name: 'bar', package: { version: '1.0.0' } } }, 396 { 397 action: 'CHANGE', 398 actual: { name: 'bar', package: { version: '1.0.0' } }, 399 ideal: { package: { version: '2.1.0' } }, 400 }, 401 ], 402 }, 403 } 404 405 const out = await mockReify(t, mock, { 406 'dry-run': true, 407 }) 408 409 t.matchSnapshot(out, 'diff table') 410}) 411 412t.test('prints dedupe difference on long', async t => { 413 const mock = { 414 actualTree: { 415 name: 'foo', 416 inventory: { 417 has: () => false, 418 }, 419 }, 420 diff: { 421 children: [ 422 { action: 'ADD', ideal: { name: 'foo', package: { version: '1.0.0' } } }, 423 { action: 'REMOVE', actual: { name: 'bar', package: { version: '1.0.0' } } }, 424 { 425 action: 'CHANGE', 426 actual: { name: 'bar', package: { version: '1.0.0' } }, 427 ideal: { package: { version: '2.1.0' } }, 428 }, 429 ], 430 }, 431 } 432 433 const out = await mockReify(t, mock, { 434 long: true, 435 }) 436 437 t.matchSnapshot(out, 'diff table') 438}) 439