1const t = require('tap')
2const { resolve } = require('path')
3const fs = require('fs/promises')
4const { load: _loadMockNpm } = require('../../fixtures/mock-npm.js')
5const mockGlobals = require('@npmcli/mock-globals')
6const tmock = require('../../fixtures/tmock')
7const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot.js')
8
9t.formatSnapshot = (p) => {
10  if (Array.isArray(p.files) && !p.files.length) {
11    delete p.files
12  }
13  if (p?.json === undefined) {
14    delete p.json
15  }
16  return p
17}
18t.cleanSnapshot = p => cleanDate(cleanCwd(p))
19
20mockGlobals(t, {
21  process: {
22    getuid: () => 867,
23    getgid: () => 5309,
24    arch: 'x64',
25    version: '123.456.789-node',
26    platform: 'posix',
27  },
28})
29
30const loadMockNpm = async (t, { errorMocks, ...opts } = {}) => {
31  const mockError = tmock(t, '{LIB}/utils/error-message.js', errorMocks)
32  const res = await _loadMockNpm(t, {
33    ...opts,
34    mocks: {
35      ...opts.mocks,
36      '{ROOT}/package.json': {
37        version: '123.456.789-npm',
38      },
39    },
40  })
41  return {
42    ...res,
43    errorMessage: (er) => mockError(er, res.npm),
44  }
45}
46
47t.test('just simple messages', async t => {
48  const { errorMessage } = await loadMockNpm(t, {
49    prefixDir: { 'package-lock.json': '{}' },
50    command: 'audit',
51    exec: true,
52  })
53  const codes = [
54    'ENOAUDIT',
55    'ENOLOCK',
56    'ECONNREFUSED',
57    'ENOGIT',
58    'EPUBLISHCONFLICT',
59    'EISGIT',
60    'EEXIST',
61    'ENEEDAUTH',
62    'ECONNRESET',
63    'ENOTFOUND',
64    'ETIMEDOUT',
65    'EAI_FAIL',
66    'EBADENGINE',
67    'ENOSPC',
68    'EROFS',
69    'ENOENT',
70    'EMISSINGARG',
71    'EUNKNOWNTYPE',
72    'EINVALIDTYPE',
73    'ETOOMANYARGS',
74    'ETARGET',
75    'E403',
76    'ERR_SOCKET_TIMEOUT',
77  ]
78  for (const code of codes) {
79    const path = '/some/path'
80    const pkgid = 'some@package'
81    const file = '/some/file'
82    const stack = 'dummy stack trace'
83    const er = Object.assign(new Error('foo'), {
84      code,
85      path,
86      pkgid,
87      file,
88      stack,
89    })
90    t.matchSnapshot(errorMessage(er))
91  }
92})
93
94t.test('replace message/stack sensistive info', async t => {
95  const { errorMessage } = await loadMockNpm(t, { command: 'audit' })
96  const path = '/some/path'
97  const pkgid = 'some@package'
98  const file = '/some/file'
99  const stack = 'dummy stack trace at https://user:pass@registry.npmjs.org/'
100  const message = 'Error at registry: https://user:pass@registry.npmjs.org/'
101  const er = Object.assign(new Error(message), {
102    code: 'ENOAUDIT',
103    path,
104    pkgid,
105    file,
106    stack,
107  })
108  t.matchSnapshot(errorMessage(er))
109})
110
111t.test('bad engine without config loaded', async t => {
112  const { errorMessage } = await loadMockNpm(t, { load: false })
113  const path = '/some/path'
114  const pkgid = 'some@package'
115  const file = '/some/file'
116  const stack = 'dummy stack trace'
117  const er = Object.assign(new Error('foo'), {
118    code: 'EBADENGINE',
119    path,
120    pkgid,
121    file,
122    stack,
123  })
124  t.matchSnapshot(errorMessage(er))
125})
126
127t.test('enoent without a file', async t => {
128  const { errorMessage } = await loadMockNpm(t)
129  const path = '/some/path'
130  const pkgid = 'some@package'
131  const stack = 'dummy stack trace'
132  const er = Object.assign(new Error('foo'), {
133    code: 'ENOENT',
134    path,
135    pkgid,
136    stack,
137  })
138  t.matchSnapshot(errorMessage(er))
139})
140
141t.test('enolock without a command', async t => {
142  const { errorMessage } = await loadMockNpm(t, { command: null })
143  const path = '/some/path'
144  const pkgid = 'some@package'
145  const file = '/some/file'
146  const stack = 'dummy stack trace'
147  const er = Object.assign(new Error('foo'), {
148    code: 'ENOLOCK',
149    path,
150    pkgid,
151    file,
152    stack,
153  })
154  t.matchSnapshot(errorMessage(er))
155})
156
157t.test('default message', async t => {
158  const { errorMessage } = await loadMockNpm(t)
159  t.matchSnapshot(errorMessage(new Error('error object')))
160  t.matchSnapshot(errorMessage('error string'))
161  t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), {
162    cmd: 'some command',
163    signal: 'SIGYOLO',
164    args: ['a', 'r', 'g', 's'],
165    stdout: 'stdout',
166    stderr: 'stderr',
167  })))
168})
169
170t.test('args are cleaned', async t => {
171  const { errorMessage } = await loadMockNpm(t)
172  t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), {
173    cmd: 'some command',
174    signal: 'SIGYOLO',
175    args: ['a', 'r', 'g', 's', 'https://evil:password@npmjs.org'],
176    stdout: 'stdout',
177    stderr: 'stderr',
178  })))
179})
180
181t.test('eacces/eperm', async t => {
182  const runTest = (windows, loaded, cachePath, cacheDest) => async t => {
183    const { errorMessage, logs, cache } = await loadMockNpm(t, {
184      windows,
185      load: loaded,
186      globals: windows ? { 'process.platform': 'win32' } : [],
187    })
188
189    const path = `${cachePath ? cache : '/not/cache/dir'}/path`
190    const dest = `${cacheDest ? cache : '/not/cache/dir'}/dest`
191    const er = Object.assign(new Error('whoopsie'), {
192      code: 'EACCES',
193      path,
194      dest,
195      stack: 'dummy stack trace',
196    })
197
198    t.matchSnapshot(errorMessage(er))
199    t.matchSnapshot(logs.verbose)
200  }
201
202  for (const windows of [true, false]) {
203    for (const loaded of [true, false]) {
204      for (const cachePath of [true, false]) {
205        for (const cacheDest of [true, false]) {
206          const m = JSON.stringify({ windows, loaded, cachePath, cacheDest })
207          t.test(m, runTest(windows, loaded, cachePath, cacheDest))
208        }
209      }
210    }
211  }
212})
213
214t.test('json parse', t => {
215  mockGlobals(t, { 'process.argv': ['arg', 'v'] })
216
217  t.test('merge conflict in package.json', async t => {
218    const prefixDir = {
219      'package.json': await fs.readFile(
220        resolve(__dirname, '../../fixtures/merge-conflict.json'), 'utf-8'),
221    }
222    const { errorMessage, npm } = await loadMockNpm(t, { prefixDir })
223    t.matchSnapshot(errorMessage(Object.assign(new Error('conflicted'), {
224      code: 'EJSONPARSE',
225      path: resolve(npm.prefix, 'package.json'),
226    })))
227    t.end()
228  })
229
230  t.test('just regular bad json in package.json', async t => {
231    const prefixDir = {
232      'package.json': 'not even slightly json',
233    }
234    const { errorMessage, npm } = await loadMockNpm(t, { prefixDir })
235    t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), {
236      code: 'EJSONPARSE',
237      path: resolve(npm.prefix, 'package.json'),
238    })))
239    t.end()
240  })
241
242  t.test('json somewhere else', async t => {
243    const prefixDir = {
244      'blerg.json': 'not even slightly json',
245    }
246    const { npm, errorMessage } = await loadMockNpm(t, { prefixDir })
247    t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), {
248      code: 'EJSONPARSE',
249      path: resolve(npm.prefix, 'blerg.json'),
250    })))
251    t.end()
252  })
253
254  t.end()
255})
256
257t.test('eotp/e401', async t => {
258  const { errorMessage } = await loadMockNpm(t)
259
260  t.test('401, no auth headers', t => {
261    t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), {
262      code: 'E401',
263    })))
264    t.end()
265  })
266
267  t.test('401, no message', t => {
268    t.matchSnapshot(errorMessage({
269      code: 'E401',
270    }))
271    t.end()
272  })
273
274  t.test('one-time pass challenge code', t => {
275    t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), {
276      code: 'EOTP',
277    })))
278    t.end()
279  })
280
281  t.test('one-time pass challenge message', t => {
282    const message = 'one-time pass'
283    t.matchSnapshot(errorMessage(Object.assign(new Error(message), {
284      code: 'E401',
285    })))
286    t.end()
287  })
288
289  t.test('www-authenticate challenges', t => {
290    const auths = [
291      'Bearer realm=do, charset="UTF-8", challenge="yourself"',
292      'Basic realm=by, charset="UTF-8", challenge="your friends"',
293      'PickACardAnyCard realm=friday, charset="UTF-8"',
294      'WashYourHands, charset="UTF-8"',
295    ]
296    t.plan(auths.length)
297    for (const auth of auths) {
298      t.test(auth, t => {
299        const er = Object.assign(new Error('challenge!'), {
300          headers: {
301            'www-authenticate': [auth],
302          },
303          code: 'E401',
304        })
305        t.matchSnapshot(errorMessage(er))
306        t.end()
307      })
308    }
309  })
310})
311
312t.test('404', async t => {
313  const { errorMessage } = await loadMockNpm(t)
314
315  t.test('no package id', t => {
316    const er = Object.assign(new Error('404 not found'), { code: 'E404' })
317    t.matchSnapshot(errorMessage(er))
318    t.end()
319  })
320  t.test('you should publish it', t => {
321    const er = Object.assign(new Error('404 not found'), {
322      pkgid: 'yolo',
323      code: 'E404',
324    })
325    t.matchSnapshot(errorMessage(er))
326    t.end()
327  })
328  t.test('name with warning', t => {
329    const er = Object.assign(new Error('404 not found'), {
330      pkgid: new Array(215).fill('x').join(''),
331      code: 'E404',
332    })
333    t.matchSnapshot(errorMessage(er))
334    t.end()
335  })
336  t.test('name with error', t => {
337    const er = Object.assign(new Error('404 not found'), {
338      pkgid: 'node_modules',
339      code: 'E404',
340    })
341    t.matchSnapshot(errorMessage(er))
342    t.end()
343  })
344  t.test('cleans sensitive info from package id', t => {
345    const er = Object.assign(new Error('404 not found'), {
346      pkgid: 'http://evil:password@npmjs.org/not-found',
347      code: 'E404',
348    })
349    t.matchSnapshot(errorMessage(er))
350    t.end()
351  })
352})
353
354t.test('bad platform', async t => {
355  const { errorMessage } = await loadMockNpm(t)
356
357  t.test('string os/arch', t => {
358    const er = Object.assign(new Error('a bad plat'), {
359      pkgid: 'lodash@1.0.0',
360      current: {
361        os: 'posix',
362        cpu: 'x64',
363      },
364      required: {
365        os: '!yours',
366        cpu: 'x420',
367      },
368      code: 'EBADPLATFORM',
369    })
370    t.matchSnapshot(errorMessage(er))
371    t.end()
372  })
373  t.test('array os/arch', t => {
374    const er = Object.assign(new Error('a bad plat'), {
375      pkgid: 'lodash@1.0.0',
376      current: {
377        os: 'posix',
378        cpu: 'x64',
379      },
380      required: {
381        os: ['!yours', 'mine'],
382        cpu: ['x867', 'x5309'],
383      },
384      code: 'EBADPLATFORM',
385    })
386    t.matchSnapshot(errorMessage(er))
387    t.end()
388  })
389  t.test('omits keys with no required value', t => {
390    const er = Object.assign(new Error('a bad plat'), {
391      pkgid: 'lodash@1.0.0',
392      current: {
393        os: 'posix',
394        cpu: 'x64',
395        libc: 'musl',
396      },
397      required: {
398        os: ['!yours', 'mine'],
399        libc: [], // empty arrays should also lead to a key being removed
400        cpu: undefined, // XXX npm-install-checks sets unused keys to undefined
401      },
402      code: 'EBADPLATFORM',
403    })
404    const msg = errorMessage(er)
405    t.matchSnapshot(msg)
406    t.notMatch(msg, /Valid cpu/, 'omits cpu from message')
407    t.notMatch(msg, /Valid libc/, 'omits libc from message')
408    t.end()
409  })
410})
411
412t.test('explain ERESOLVE errors', async t => {
413  const EXPLAIN_CALLED = []
414
415  const { errorMessage } = await loadMockNpm(t, {
416    errorMocks: {
417      '{LIB}/utils/explain-eresolve.js': {
418        report: (...args) => {
419          EXPLAIN_CALLED.push(...args)
420          return { explanation: 'explanation', file: 'report' }
421        },
422      },
423    },
424    config: {
425      color: 'always',
426    },
427  })
428
429  const er = Object.assign(new Error('could not resolve'), {
430    code: 'ERESOLVE',
431  })
432
433  t.matchSnapshot(errorMessage(er))
434  t.equal(EXPLAIN_CALLED.length, 3)
435  t.match(EXPLAIN_CALLED, [er, Function, Function])
436  t.not(EXPLAIN_CALLED[1].level, 0, 'color chalk level is not 0')
437  t.equal(EXPLAIN_CALLED[2].level, 0, 'colorless chalk level is 0')
438})
439