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