1const { join } = require('path') 2const fs = require('fs/promises') 3const ini = require('ini') 4const tspawk = require('../../fixtures/tspawk') 5const t = require('tap') 6 7const spawk = tspawk(t) 8 9const Sandbox = require('../../fixtures/sandbox.js') 10 11t.test('config no args', async t => { 12 const sandbox = new Sandbox(t) 13 14 await t.rejects( 15 sandbox.run('config', []), 16 { 17 code: 'EUSAGE', 18 }, 19 'rejects with usage' 20 ) 21}) 22 23t.test('config ignores workspaces', async t => { 24 const sandbox = new Sandbox(t) 25 26 await t.rejects( 27 sandbox.run('config', ['--workspaces']), 28 { 29 code: 'ENOWORKSPACES', 30 }, 31 'rejects with usage' 32 ) 33}) 34 35t.test('config list', async t => { 36 const temp = t.testdir({ 37 global: { 38 npmrc: 'globalloaded=yes', 39 }, 40 project: { 41 '.npmrc': 'projectloaded=yes', 42 }, 43 home: { 44 '.npmrc': 'userloaded=yes', 45 }, 46 }) 47 const global = join(temp, 'global') 48 const project = join(temp, 'project') 49 const home = join(temp, 'home') 50 51 const sandbox = new Sandbox(t, { global, project, home }) 52 await sandbox.run('config', ['list']) 53 54 t.matchSnapshot(sandbox.output, 'output matches snapshot') 55}) 56 57t.test('config list --long', async t => { 58 const temp = t.testdir({ 59 global: { 60 npmrc: 'globalloaded=yes', 61 }, 62 project: { 63 '.npmrc': 'projectloaded=yes', 64 }, 65 home: { 66 '.npmrc': 'userloaded=yes', 67 }, 68 }) 69 const global = join(temp, 'global') 70 const project = join(temp, 'project') 71 const home = join(temp, 'home') 72 73 const sandbox = new Sandbox(t, { global, project, home }) 74 await sandbox.run('config', ['list', '--long']) 75 76 t.matchSnapshot(sandbox.output, 'output matches snapshot') 77}) 78 79t.test('config list --json', async t => { 80 const temp = t.testdir({ 81 global: { 82 npmrc: 'globalloaded=yes', 83 }, 84 project: { 85 '.npmrc': 'projectloaded=yes', 86 }, 87 home: { 88 '.npmrc': 'userloaded=yes', 89 }, 90 }) 91 const global = join(temp, 'global') 92 const project = join(temp, 'project') 93 const home = join(temp, 'home') 94 95 const sandbox = new Sandbox(t, { global, project, home }) 96 await sandbox.run('config', ['list', '--json']) 97 98 t.matchSnapshot(sandbox.output, 'output matches snapshot') 99}) 100 101t.test('config list with publishConfig', async t => { 102 const temp = t.testdir({ 103 project: { 104 'package.json': JSON.stringify({ 105 publishConfig: { 106 registry: 'https://some.registry', 107 _authToken: 'mytoken', 108 }, 109 }), 110 }, 111 }) 112 const project = join(temp, 'project') 113 114 const sandbox = new Sandbox(t, { project }) 115 await sandbox.run('config', ['list', '']) 116 await sandbox.run('config', ['list', '--global']) 117 118 t.matchSnapshot(sandbox.output, 'output matches snapshot') 119}) 120 121t.test('config delete no args', async t => { 122 const sandbox = new Sandbox(t) 123 124 await t.rejects( 125 sandbox.run('config', ['delete']), 126 { 127 code: 'EUSAGE', 128 }, 129 'rejects with usage' 130 ) 131}) 132 133t.test('config delete single key', async t => { 134 // location defaults to user, so we work with a userconfig 135 const home = t.testdir({ 136 '.npmrc': 'access=public\nall=true', 137 }) 138 139 const sandbox = new Sandbox(t, { home }) 140 await sandbox.run('config', ['delete', 'access']) 141 142 t.equal(sandbox.config.get('access'), null, 'acces should be defaulted') 143 144 const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) 145 const rc = ini.parse(contents) 146 t.not(rc.access, 'access is not set') 147}) 148 149t.test('config delete multiple keys', async t => { 150 const home = t.testdir({ 151 '.npmrc': 'access=public\nall=true\naudit=false', 152 }) 153 154 const sandbox = new Sandbox(t, { home }) 155 await sandbox.run('config', ['delete', 'access', 'all']) 156 157 t.equal(sandbox.config.get('access'), null, 'access should be defaulted') 158 t.equal(sandbox.config.get('all'), false, 'all should be defaulted') 159 160 const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) 161 const rc = ini.parse(contents) 162 t.not(rc.access, 'access is not set') 163 t.not(rc.all, 'all is not set') 164}) 165 166t.test('config delete key --location=global', async t => { 167 const global = t.testdir({ 168 npmrc: 'access=public\nall=true', 169 }) 170 171 const sandbox = new Sandbox(t, { global }) 172 await sandbox.run('config', ['delete', 'access', '--location=global']) 173 174 t.equal(sandbox.config.get('access', 'global'), undefined, 'access should be defaulted') 175 176 const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' }) 177 const rc = ini.parse(contents) 178 t.not(rc.access, 'access is not set') 179}) 180 181t.test('config delete key --global', async t => { 182 const global = t.testdir({ 183 npmrc: 'access=public\nall=true', 184 }) 185 186 const sandbox = new Sandbox(t, { global }) 187 await sandbox.run('config', ['delete', 'access', '--global']) 188 189 t.equal(sandbox.config.get('access', 'global'), undefined, 'access should no longer be set') 190 191 const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' }) 192 const rc = ini.parse(contents) 193 t.not(rc.access, 'access is not set') 194}) 195 196t.test('config set invalid option', async t => { 197 const sandbox = new Sandbox(t) 198 await t.rejects( 199 sandbox.run('config', ['set', 'nonexistantconfigoption', 'something']), 200 /not a valid npm option/ 201 ) 202}) 203 204t.test('config set deprecated option', async t => { 205 const sandbox = new Sandbox(t) 206 await t.rejects( 207 sandbox.run('config', ['set', 'shrinkwrap', 'true']), 208 /deprecated/ 209 ) 210}) 211 212t.test('config set nerf-darted option', async t => { 213 const sandbox = new Sandbox(t) 214 await sandbox.run('config', ['set', '//npm.pkg.github.com/:_authToken', '0xdeadbeef']) 215 t.equal( 216 sandbox.config.get('//npm.pkg.github.com/:_authToken'), 217 '0xdeadbeef', 218 'nerf-darted config is set' 219 ) 220}) 221 222t.test('config set scoped optoin', async t => { 223 const sandbox = new Sandbox(t) 224 await sandbox.run('config', ['set', '@npm:registry', 'https://registry.npmjs.org']) 225 t.equal( 226 sandbox.config.get('@npm:registry'), 227 'https://registry.npmjs.org', 228 'scoped config is set' 229 ) 230}) 231 232t.test('config set no args', async t => { 233 const sandbox = new Sandbox(t) 234 235 await t.rejects( 236 sandbox.run('config', ['set']), 237 { 238 code: 'EUSAGE', 239 }, 240 'rejects with usage' 241 ) 242}) 243 244t.test('config set key', async t => { 245 const home = t.testdir({ 246 '.npmrc': 'access=public', 247 }) 248 249 const sandbox = new Sandbox(t, { home }) 250 251 await sandbox.run('config', ['set', 'access']) 252 253 t.equal(sandbox.config.get('access'), null, 'set the value for access') 254 255 await t.rejects(fs.stat(join(home, '.npmrc'), { encoding: 'utf8' }), 'removed empty config') 256}) 257 258t.test('config set key value', async t => { 259 const home = t.testdir({ 260 '.npmrc': 'access=public', 261 }) 262 263 const sandbox = new Sandbox(t, { home }) 264 265 await sandbox.run('config', ['set', 'access', 'restricted']) 266 267 t.equal(sandbox.config.get('access'), 'restricted', 'set the value for access') 268 269 const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) 270 const rc = ini.parse(contents) 271 t.equal(rc.access, 'restricted', 'access is set to restricted') 272}) 273 274t.test('config set key=value', async t => { 275 const home = t.testdir({ 276 '.npmrc': 'access=public', 277 }) 278 279 const sandbox = new Sandbox(t, { home }) 280 281 await sandbox.run('config', ['set', 'access=restricted']) 282 283 t.equal(sandbox.config.get('access'), 'restricted', 'set the value for access') 284 285 const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) 286 const rc = ini.parse(contents) 287 t.equal(rc.access, 'restricted', 'access is set to restricted') 288}) 289 290t.test('config set key1 value1 key2=value2 key3', async t => { 291 const home = t.testdir({ 292 '.npmrc': 'access=public\nall=true\naudit=true', 293 }) 294 295 const sandbox = new Sandbox(t, { home }) 296 await sandbox.run('config', ['set', 'access', 'restricted', 'all=false', 'audit']) 297 298 t.equal(sandbox.config.get('access'), 'restricted', 'access was set') 299 t.equal(sandbox.config.get('all'), false, 'all was set') 300 t.equal(sandbox.config.get('audit'), true, 'audit was unset and restored to its default') 301 302 const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) 303 const rc = ini.parse(contents) 304 t.equal(rc.access, 'restricted', 'access is set to restricted') 305 t.equal(rc.all, false, 'all is set to false') 306 t.not(contents.includes('audit='), 'config file does not set audit') 307}) 308 309t.test('config set invalid key logs warning', async t => { 310 const sandbox = new Sandbox(t) 311 312 // this doesn't reject, it only logs a warning 313 await sandbox.run('config', ['set', 'access=foo']) 314 t.match( 315 sandbox.logs.warn, 316 [['invalid config', 'access="foo"', `set in ${join(sandbox.home, '.npmrc')}`]], 317 'logged warning' 318 ) 319}) 320 321t.test('config set key=value --location=global', async t => { 322 const global = t.testdir({ 323 npmrc: 'access=public\nall=true', 324 }) 325 326 const sandbox = new Sandbox(t, { global }) 327 await sandbox.run('config', ['set', 'access=restricted', '--location=global']) 328 329 t.equal(sandbox.config.get('access', 'global'), 'restricted', 'foo should be set') 330 331 const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' }) 332 const rc = ini.parse(contents) 333 t.equal(rc.access, 'restricted', 'access is set to restricted') 334}) 335 336t.test('config set key=value --global', async t => { 337 const global = t.testdir({ 338 npmrc: 'access=public\nall=true', 339 }) 340 341 const sandbox = new Sandbox(t, { global }) 342 await sandbox.run('config', ['set', 'access=restricted', '--global']) 343 344 t.equal(sandbox.config.get('access', 'global'), 'restricted', 'access should be set') 345 346 const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' }) 347 const rc = ini.parse(contents) 348 t.equal(rc.access, 'restricted', 'access is set to restricted') 349}) 350 351t.test('config get no args', async t => { 352 const sandbox = new Sandbox(t) 353 354 await sandbox.run('config', ['get']) 355 const getOutput = sandbox.output 356 357 sandbox.reset() 358 359 await sandbox.run('config', ['list']) 360 const listOutput = sandbox.output 361 362 t.equal(listOutput, getOutput, 'get with no args outputs list') 363}) 364 365t.test('config get single key', async t => { 366 const sandbox = new Sandbox(t) 367 368 await sandbox.run('config', ['get', 'all']) 369 t.equal(sandbox.output, `${sandbox.config.get('all')}`, 'should get the value') 370}) 371 372t.test('config get multiple keys', async t => { 373 const sandbox = new Sandbox(t) 374 375 await sandbox.run('config', ['get', 'yes', 'all']) 376 t.ok( 377 sandbox.output.includes(`yes=${sandbox.config.get('yes')}`), 378 'outputs yes' 379 ) 380 t.ok( 381 sandbox.output.includes(`all=${sandbox.config.get('all')}`), 382 'outputs all' 383 ) 384}) 385 386t.test('config get private key', async t => { 387 const sandbox = new Sandbox(t) 388 389 await t.rejects( 390 sandbox.run('config', ['get', '_authToken']), 391 /_authToken option is protected/, 392 'rejects with protected string' 393 ) 394 395 await t.rejects( 396 sandbox.run('config', ['get', '//localhost:8080/:_password']), 397 /_password option is protected/, 398 'rejects with protected string' 399 ) 400}) 401 402t.test('config edit', async t => { 403 const home = t.testdir({ 404 '.npmrc': 'foo=bar\nbar=baz', 405 }) 406 407 const EDITOR = 'vim' 408 const editor = spawk.spawn(EDITOR).exit(0) 409 410 const sandbox = new Sandbox(t, { home, env: { EDITOR } }) 411 await sandbox.run('config', ['edit']) 412 413 t.ok(editor.called, 'editor was spawned') 414 t.same( 415 editor.calledWith.args, 416 [join(sandbox.home, '.npmrc')], 417 'editor opened the user config file' 418 ) 419 420 const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' }) 421 t.ok(contents.includes('foo=bar'), 'kept foo') 422 t.ok(contents.includes('bar=baz'), 'kept bar') 423 t.ok(contents.includes('shown below with default values'), 'appends defaults to file') 424}) 425 426t.test('config edit - editor exits non-0', async t => { 427 const EDITOR = 'vim' 428 const editor = spawk.spawn(EDITOR).exit(1) 429 430 const sandbox = new Sandbox(t) 431 sandbox.process.env.EDITOR = EDITOR 432 await t.rejects( 433 sandbox.run('config', ['edit']), 434 { 435 message: 'editor process exited with code: 1', 436 }, 437 'rejects with error about editor code' 438 ) 439 440 t.ok(editor.called, 'editor was spawned') 441 t.same( 442 editor.calledWith.args, 443 [join(sandbox.home, '.npmrc')], 444 'editor opened the user config file' 445 ) 446}) 447 448t.test('config fix', (t) => { 449 t.test('no problems', async (t) => { 450 const home = t.testdir({ 451 '.npmrc': '', 452 }) 453 454 const sandbox = new Sandbox(t, { home }) 455 await sandbox.run('config', ['fix']) 456 t.equal(sandbox.output, '', 'printed nothing') 457 }) 458 459 t.test('repairs all configs by default', async (t) => { 460 const root = t.testdir({ 461 global: { 462 npmrc: '_authtoken=notatoken\n_authToken=afaketoken', 463 }, 464 home: { 465 '.npmrc': '_authtoken=thisisinvalid\n_auth=beef', 466 }, 467 }) 468 const registry = `//registry.npmjs.org/` 469 470 const sandbox = new Sandbox(t, { 471 global: join(root, 'global'), 472 home: join(root, 'home'), 473 }) 474 await sandbox.run('config', ['fix']) 475 476 // global config fixes 477 t.match(sandbox.output, '`_authtoken` deleted from global config', 478 'output has deleted global _authtoken') 479 t.match(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global config`, 480 'output has renamed global _authToken') 481 t.not(sandbox.config.get('_authtoken', 'global'), '_authtoken is not set globally') 482 t.not(sandbox.config.get('_authToken', 'global'), '_authToken is not set globally') 483 t.equal(sandbox.config.get(`${registry}:_authToken`, 'global'), 'afaketoken', 484 'global _authToken was scoped') 485 const globalConfig = await fs.readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' }) 486 t.equal(globalConfig, `${registry}:_authToken=afaketoken\n`, 'global config was written') 487 488 // user config fixes 489 t.match(sandbox.output, '`_authtoken` deleted from user config', 490 'output has deleted user _authtoken') 491 t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user config`, 492 'output has renamed user _auth') 493 t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config') 494 t.not(sandbox.config.get('_auth'), '_auth is not set in user config') 495 t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped') 496 const userConfig = await fs.readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' }) 497 t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written') 498 }) 499 500 t.test('repairs only the config specified by --location if asked', async (t) => { 501 const root = t.testdir({ 502 global: { 503 npmrc: '_authtoken=notatoken\n_authToken=afaketoken', 504 }, 505 home: { 506 '.npmrc': '_authtoken=thisisinvalid\n_auth=beef', 507 }, 508 }) 509 const registry = `//registry.npmjs.org/` 510 511 const sandbox = new Sandbox(t, { 512 global: join(root, 'global'), 513 home: join(root, 'home'), 514 }) 515 await sandbox.run('config', ['fix', '--location=user']) 516 517 // global config should be untouched 518 t.notMatch(sandbox.output, '`_authtoken` deleted from global', 519 'output has deleted global _authtoken') 520 t.notMatch(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global`, 521 'output has renamed global _authToken') 522 t.equal(sandbox.config.get('_authtoken', 'global'), 'notatoken', 'global _authtoken untouched') 523 t.equal(sandbox.config.get('_authToken', 'global'), 'afaketoken', 'global _authToken untouched') 524 t.not(sandbox.config.get(`${registry}:_authToken`, 'global'), 'global _authToken not scoped') 525 const globalConfig = await fs.readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' }) 526 t.equal(globalConfig, '_authtoken=notatoken\n_authToken=afaketoken', 527 'global config was not written') 528 529 // user config fixes 530 t.match(sandbox.output, '`_authtoken` deleted from user', 531 'output has deleted user _authtoken') 532 t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user`, 533 'output has renamed user _auth') 534 t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config') 535 t.not(sandbox.config.get('_auth', 'user'), '_auth is not set in user config') 536 t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped') 537 const userConfig = await fs.readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' }) 538 t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written') 539 }) 540 541 t.end() 542}) 543 544t.test('completion', async t => { 545 const sandbox = new Sandbox(t) 546 547 let allKeys 548 const testComp = async (argv, expect) => { 549 t.match(await sandbox.complete('config', argv), expect, argv.join(' ')) 550 if (!allKeys) { 551 allKeys = Object.keys(sandbox.config.definitions) 552 } 553 sandbox.reset() 554 } 555 556 await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix', 'list']) 557 await testComp(['set', 'foo'], []) 558 await testComp(['get'], allKeys) 559 await testComp(['set'], allKeys) 560 await testComp(['delete'], allKeys) 561 await testComp(['rm'], allKeys) 562 await testComp(['edit'], []) 563 await testComp(['fix'], []) 564 await testComp(['list'], []) 565 await testComp(['ls'], []) 566 567 const getCommand = await sandbox.complete('get') 568 t.match(getCommand, allKeys, 'also works for just npm get') 569 sandbox.reset() 570 571 const partial = await sandbox.complete('config', 'l') 572 t.match(partial, ['get', 'set', 'delete', 'ls', 'rm', 'edit'], 'and works on partials') 573}) 574