const t = require('tap') const requireInject = require('require-inject') const { EventEmitter } = require('events') const redactCwd = (path) => { const normalizePath = p => p .replace(/\\+/g, '/') .replace(/\r\n/g, '\n') const replaceCwd = p => p .replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}') const cleanupWinPaths = p => p .replace(normalizePath(process.execPath), '/path/to/node') .replace(normalizePath(process.env.HOME), '~/') return cleanupWinPaths( replaceCwd( normalizePath(path) ) ) } t.cleanSnapshot = (str) => redactCwd(str) let result = '' const types = { 'init-author-name': String, 'init-version': String, 'init.author.name': String, 'init.version': String, } const defaults = { 'init-author-name': '', 'init-version': '1.0.0', 'init.author.name': '', 'init.version': '1.0.0', } const flatOptions = { editor: 'vi', json: false, long: false, global: false, } const npm = { flatOptions, log: { info: () => null, enableProgress: () => null, disableProgress: () => null, }, config: { data: new Map(Object.entries({ default: { data: defaults, source: 'default values' }, global: { data: {}, source: '/etc/npmrc' }, cli: { data: flatOptions, source: 'command line options' }, })), get (key) { return flatOptions[key] }, validate () { return true }, }, } const usageUtil = () => 'usage instructions' const mocks = { '../../lib/utils/config.js': { defaults, types }, '../../lib/utils/output.js': msg => { result = msg }, '../../lib/utils/usage.js': usageUtil, } const Config = requireInject('../../lib/config.js', mocks) const config = new Config(npm) t.test('config no args', t => { config.exec([], (err) => { t.match(err, /usage instructions/, 'should not error out on empty locations') t.end() }) }) t.test('config list', t => { t.plan(2) npm.config.find = () => 'cli' result = '' t.teardown(() => { result = '' delete npm.config.find }) config.exec(['list'], (err) => { t.ifError(err, 'npm config list') t.matchSnapshot(result, 'should list configs') }) }) t.test('config list overrides', t => { t.plan(2) npm.config.data.set('user', { data: { 'init.author.name': 'Foo', '//private-reg.npmjs.org/:_authThoken': 'f00ba1', }, source: '~/.npmrc', }) flatOptions['init.author.name'] = 'Bar' npm.config.find = () => 'cli' result = '' t.teardown(() => { result = '' npm.config.data.delete('user') delete flatOptions['init.author.name'] delete npm.config.find }) config.exec(['list'], (err) => { t.ifError(err, 'npm config list') t.matchSnapshot(result, 'should list overridden configs') }) }) t.test('config list --long', t => { t.plan(2) npm.config.find = key => key in flatOptions ? 'cli' : 'default' flatOptions.long = true result = '' t.teardown(() => { delete npm.config.find flatOptions.long = false result = '' }) config.exec(['list'], (err) => { t.ifError(err, 'npm config list --long') t.matchSnapshot(result, 'should list all configs') }) }) t.test('config list --json', t => { t.plan(2) flatOptions.json = true result = '' npm.config.list = [{ '//private-reg.npmjs.org/:_authThoken': 'f00ba1', ...npm.config.data.get('cli').data, }] const npmConfigGet = npm.config.get npm.config.get = key => npm.config.list[0][key] t.teardown(() => { delete npm.config.list flatOptions.json = false npm.config.get = npmConfigGet result = '' }) config.exec(['list'], (err) => { t.ifError(err, 'npm config list --json') t.deepEqual( JSON.parse(result), { editor: 'vi', json: true, long: false, global: false, }, 'should list configs usin json' ) }) }) t.test('config delete no args', t => { config.exec(['delete'], (err) => { t.equal( err.message, 'usage instructions', 'should throw usage error' ) t.equal(err.code, 'EUSAGE', 'should throw expected error code') t.end() }) }) t.test('config delete key', t => { t.plan(4) npm.config.delete = (key, where) => { t.equal(key, 'foo', 'should delete expected keyword') t.equal(where, 'user', 'should delete key from user config by default') } npm.config.save = where => { t.equal(where, 'user', 'should save user config post-delete') } config.exec(['delete', 'foo'], (err) => { t.ifError(err, 'npm config delete key') }) t.teardown(() => { delete npm.config.delete delete npm.config.save }) }) t.test('config delete multiple key', t => { t.plan(6) const expect = [ 'foo', 'bar', ] npm.config.delete = (key, where) => { t.equal(key, expect.shift(), 'should delete expected keyword') t.equal(where, 'user', 'should delete key from user config by default') } npm.config.save = where => { t.equal(where, 'user', 'should save user config post-delete') } config.exec(['delete', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config delete keys') }) t.teardown(() => { delete npm.config.delete delete npm.config.save }) }) t.test('config delete key --global', t => { t.plan(4) npm.config.delete = (key, where) => { t.equal(key, 'foo', 'should delete expected keyword from global configs') t.equal(where, 'global', 'should delete key from global config by default') } npm.config.save = where => { t.equal(where, 'global', 'should save global config post-delete') } flatOptions.global = true config.exec(['delete', 'foo'], (err) => { t.ifError(err, 'npm config delete key --global') }) t.teardown(() => { flatOptions.global = false delete npm.config.delete delete npm.config.save }) }) t.test('config set no args', t => { config.exec(['set'], (err) => { t.equal( err.message, 'usage instructions', 'should throw usage error' ) t.end() }) }) t.test('config set key', t => { t.plan(5) npm.config.set = (key, val, where) => { t.equal(key, 'foo', 'should set expected key to user config') t.equal(val, 'bar', 'should set expected value to user config') t.equal(where, 'user', 'should set key/val in user config by default') } npm.config.save = where => { t.equal(where, 'user', 'should save user config') } config.exec(['set', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config set key') }) t.teardown(() => { delete npm.config.set delete npm.config.save }) }) t.test('config set key=val', t => { t.plan(5) npm.config.set = (key, val, where) => { t.equal(key, 'foo', 'should set expected key to user config') t.equal(val, 'bar', 'should set expected value to user config') t.equal(where, 'user', 'should set key/val in user config by default') } npm.config.save = where => { t.equal(where, 'user', 'should save user config') } config.exec(['set', 'foo=bar'], (err) => { t.ifError(err, 'npm config set key') }) t.teardown(() => { delete npm.config.set delete npm.config.save }) }) t.test('config set multiple keys', t => { t.plan(11) const expect = [ ['foo', 'bar'], ['bar', 'baz'], ['asdf', ''], ] const args = ['foo', 'bar', 'bar=baz', 'asdf'] npm.config.set = (key, val, where) => { const [expectKey, expectVal] = expect.shift() t.equal(key, expectKey, 'should set expected key to user config') t.equal(val, expectVal, 'should set expected value to user config') t.equal(where, 'user', 'should set key/val in user config by default') } npm.config.save = where => { t.equal(where, 'user', 'should save user config') } config.exec(['set', ...args], (err) => { t.ifError(err, 'npm config set key') }) t.teardown(() => { delete npm.config.set delete npm.config.save }) }) t.test('config set key to empty value', t => { t.plan(5) npm.config.set = (key, val, where) => { t.equal(key, 'foo', 'should set expected key to user config') t.equal(val, '', 'should set "" to user config') t.equal(where, 'user', 'should set key/val in user config by default') } npm.config.save = where => { t.equal(where, 'user', 'should save user config') } config.exec(['set', 'foo'], (err) => { t.ifError(err, 'npm config set key to empty value') }) t.teardown(() => { delete npm.config.set delete npm.config.save }) }) t.test('config set invalid key', t => { t.plan(3) const npmConfigValidate = npm.config.validate npm.config.save = () => null npm.config.set = () => null npm.config.validate = () => false npm.log.warn = (title, msg) => { t.equal(title, 'config', 'should warn with expected title') t.equal(msg, 'omitting invalid config values', 'should use expected msg') } t.teardown(() => { npm.config.validate = npmConfigValidate delete npm.config.save delete npm.config.set delete npm.log.warn }) config.exec(['set', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config set invalid key') }) }) t.test('config set key --global', t => { t.plan(5) npm.config.set = (key, val, where) => { t.equal(key, 'foo', 'should set expected key to global config') t.equal(val, 'bar', 'should set expected value to global config') t.equal(where, 'global', 'should set key/val in global config') } npm.config.save = where => { t.equal(where, 'global', 'should save global config') } flatOptions.global = true config.exec(['set', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config set key --global') }) t.teardown(() => { flatOptions.global = false delete npm.config.set delete npm.config.save }) }) t.test('config get no args', t => { t.plan(2) npm.config.find = () => 'cli' result = '' t.teardown(() => { result = '' delete npm.config.find }) config.exec(['get'], (err) => { t.ifError(err, 'npm config get no args') t.matchSnapshot(result, 'should list configs on config get no args') }) }) t.test('config get key', t => { t.plan(2) const npmConfigGet = npm.config.get npm.config.get = (key) => { t.equal(key, 'foo', 'should use expected key') return 'bar' } npm.config.save = where => { throw new Error('should not save') } config.exec(['get', 'foo'], (err) => { t.ifError(err, 'npm config get key') }) t.teardown(() => { npm.config.get = npmConfigGet delete npm.config.save }) }) t.test('config get multiple keys', t => { t.plan(4) const expect = [ 'foo', 'bar', ] const npmConfigGet = npm.config.get npm.config.get = (key) => { t.equal(key, expect.shift(), 'should use expected key') return 'asdf' } npm.config.save = where => { throw new Error('should not save') } config.exec(['get', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config get multiple keys') t.equal(result, 'foo=asdf\nbar=asdf') }) t.teardown(() => { result = '' npm.config.get = npmConfigGet delete npm.config.save }) }) t.test('config get private key', t => { config.exec(['get', '//private-reg.npmjs.org/:_authThoken'], (err) => { t.match( err, /The \/\/private-reg.npmjs.org\/:_authThoken option is protected, and cannot be retrieved in this way/, 'should throw unable to retrieve error' ) t.end() }) }) t.test('config edit', t => { t.plan(12) const npmrc = `//registry.npmjs.org/:_authToken=0000000 init.author.name=Foo sign-git-commit=true` npm.config.data.set('user', { source: '~/.npmrc', }) npm.config.save = async where => { t.equal(where, 'user', 'should save to user config by default') } const editMocks = { ...mocks, 'mkdirp-infer-owner': async () => null, fs: { readFile (path, encoding, cb) { cb(null, npmrc) }, writeFile (file, data, encoding, cb) { t.equal(file, '~/.npmrc', 'should save to expected file location') t.matchSnapshot(data, 'should write config file') cb() }, }, child_process: { spawn: (bin, args) => { t.equal(bin, 'vi', 'should use default editor') t.strictSame(args, ['~/.npmrc'], 'should match user source data') const ee = new EventEmitter() process.nextTick(() => { ee.emit('exit', 0) }) return ee }, }, } const Config = requireInject('../../lib/config.js', editMocks) const config = new Config(npm) config.exec(['edit'], (err) => { t.ifError(err, 'npm config edit') // test no config file result editMocks.fs.readFile = (p, e, cb) => { cb(new Error('ERR')) } const Config = requireInject('../../lib/config.js', editMocks) const config = new Config(npm) config.exec(['edit'], (err) => { t.ifError(err, 'npm config edit') }) }) t.teardown(() => { npm.config.data.delete('user') delete npm.config.save }) }) t.test('config edit --global', t => { t.plan(6) flatOptions.global = true const npmrc = 'init.author.name=Foo' npm.config.data.set('global', { source: '/etc/npmrc', }) npm.config.save = async where => { t.equal(where, 'global', 'should save to global config') } const editMocks = { ...mocks, 'mkdirp-infer-owner': async () => null, fs: { readFile (path, encoding, cb) { cb(null, npmrc) }, writeFile (file, data, encoding, cb) { t.equal(file, '/etc/npmrc', 'should save to global file location') t.matchSnapshot(data, 'should write global config file') cb() }, }, child_process: { spawn: (bin, args, cb) => { t.equal(bin, 'vi', 'should use default editor') t.strictSame(args, ['/etc/npmrc'], 'should match global source data') const ee = new EventEmitter() process.nextTick(() => { ee.emit('exit', 137) }) return ee }, }, } const Config = requireInject('../../lib/config.js', editMocks) const config = new Config(npm) config.exec(['edit'], (err) => { t.match(err, /exited with code: 137/, 'propagated exit code from editor') }) t.teardown(() => { flatOptions.global = false npm.config.data.delete('user') delete npm.config.save }) }) t.test('completion', t => { const { completion } = config const testComp = (argv, expect) => { t.resolveMatch(completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) } testComp(['npm', 'foo'], []) testComp(['npm', 'config'], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'list']) testComp(['npm', 'config', 'set', 'foo'], []) const possibleConfigKeys = [...Object.keys(types)] testComp(['npm', 'config', 'get'], possibleConfigKeys) testComp(['npm', 'config', 'set'], possibleConfigKeys) testComp(['npm', 'config', 'delete'], possibleConfigKeys) testComp(['npm', 'config', 'rm'], possibleConfigKeys) testComp(['npm', 'config', 'edit'], []) testComp(['npm', 'config', 'list'], []) testComp(['npm', 'config', 'ls'], []) const partial = completion({conf: { argv: { remain: ['npm', 'config'] } }, partialWord: 'l'}) t.resolveMatch(partial, ['get', 'set', 'delete', 'ls', 'rm', 'edit'], 'npm config') t.end() })