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