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