Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorisaacs <i@izs.me>2021-03-16 02:15:27 +0300
committerisaacs <i@izs.me>2021-03-18 21:58:08 +0300
commit6598bfe8697439e827d84981f8504febca64a55a (patch)
tree670bb556880a187e8ba82e843f932222af76fbd8 /test
parent8cce4282f7bef11aeeb73cffd532b477b241985e (diff)
New consolidated config definitions
This replaces the multiple separate sets of objects and documentation, some of which had defaults and/or types, some of which didn't, and cleans up a lot of configs that are no longer used. Deprecated configs are now marked, and the approach used to create these config definitions ensures that it is impossible to create a new config option that lacks the appropriate data for it.
Diffstat (limited to 'test')
-rw-r--r--test/lib/utils/config/definition.js150
-rw-r--r--test/lib/utils/config/definitions.js697
-rw-r--r--test/lib/utils/config/describe-all.js6
-rw-r--r--test/lib/utils/config/flatten.js34
-rw-r--r--test/lib/utils/config/index.js24
5 files changed, 911 insertions, 0 deletions
diff --git a/test/lib/utils/config/definition.js b/test/lib/utils/config/definition.js
new file mode 100644
index 000000000..25530f723
--- /dev/null
+++ b/test/lib/utils/config/definition.js
@@ -0,0 +1,150 @@
+const t = require('tap')
+const Definition = require('../../../../lib/utils/config/definition.js')
+const {
+ typeDefs: {
+ semver: { type: semver },
+ Umask: { type: Umask },
+ url: { type: url },
+ path: { type: path },
+ },
+} = require('@npmcli/config')
+
+t.test('basic definition', async t => {
+ const def = new Definition('key', {
+ default: 'some default value',
+ type: [Number, String],
+ description: 'just a test thingie',
+ })
+ t.same(def, {
+ constructor: Definition,
+ key: 'key',
+ default: 'some default value',
+ defaultDescription: '"some default value"',
+ type: [Number, String],
+ typeDescription: 'Number or String',
+ description: 'just a test thingie',
+ })
+ t.matchSnapshot(def.describe(), 'human-readable description')
+
+ const deprecated = new Definition('deprecated', {
+ deprecated: 'do not use this',
+ default: 1234,
+ description: ' it should not be used\n ever\n\n not even once.\n\n',
+ type: Number,
+ defaultDescription: 'A number bigger than 1',
+ typeDescription: 'An expression of a numeric quantity using numerals',
+ })
+ t.matchSnapshot(deprecated.describe(), 'description of deprecated thing')
+
+ const nullOrUmask = new Definition('key', {
+ default: null,
+ type: [null, Umask],
+ description: 'asdf',
+ })
+ t.equal(nullOrUmask.typeDescription, 'null or Octal numeric string in range 0000..0777 (0..511)')
+ const nullDateOrBool = new Definition('key', {
+ default: 7,
+ type: [null, Date, Boolean],
+ description: 'asdf',
+ })
+ t.equal(nullDateOrBool.typeDescription, 'null, Date, or Boolean')
+ const manyPaths = new Definition('key', {
+ default: ['asdf'],
+ type: [path, Array],
+ description: 'asdf',
+ })
+ t.equal(manyPaths.typeDescription, 'Path (can be set multiple times)')
+ const pathOrUrl = new Definition('key', {
+ default: ['https://example.com'],
+ type: [path, url],
+ description: 'asdf',
+ })
+ t.equal(pathOrUrl.typeDescription, 'Path or URL')
+ const multi12 = new Definition('key', {
+ default: [],
+ type: [1, 2, Array],
+ description: 'asdf',
+ })
+ t.equal(multi12.typeDescription, '1 or 2 (can be set multiple times)')
+ const multi123 = new Definition('key', {
+ default: [],
+ type: [1, 2, 3, Array],
+ description: 'asdf',
+ })
+ t.equal(multi123.typeDescription, '1, 2, or 3 (can be set multiple times)')
+ const multi123Semver = new Definition('key', {
+ default: [],
+ type: [1, 2, 3, Array, semver],
+ description: 'asdf',
+ })
+ t.equal(multi123Semver.typeDescription, '1, 2, 3, or SemVer string (can be set multiple times)')
+})
+
+t.test('missing fields', async t => {
+ t.throws(() => new Definition('lacks-default', {
+ description: 'no default',
+ type: String,
+ }), { message: 'config lacks default: lacks-default' })
+ t.throws(() => new Definition('lacks-type', {
+ description: 'no type',
+ default: 1234,
+ }), { message: 'config lacks type: lacks-type' })
+ t.throws(() => new Definition(null, {
+ description: 'falsey key',
+ default: 1234,
+ type: Number,
+ }), { message: 'config lacks key: null' })
+ t.throws(() => new Definition('extra-field', {
+ type: String,
+ default: 'extra',
+ extra: 'more than is wanted',
+ description: 'this is not ok',
+ }), { message: 'config defines unknown field extra: extra-field' })
+})
+
+t.test('long description', async t => {
+ const { stdout: { columns } } = process
+ t.teardown(() => process.stdout.columns = columns)
+
+ const long = new Definition('walden', {
+ description: `
+ WHEN I WROTE the following pages, or rather the bulk of them, I lived
+ alone, in the woods, a mile from any neighbor, in a house which I had
+ built myself, on the shore of Walden Pond, in Concord, Massachusetts, and
+ earned my living by the labor of my hands only. I lived there two years
+ and two months. At present I am a sojourner in civilized life again.
+
+ I should not obtrude my affairs so much on the notice of my readers if
+ very particular inquiries had not been made by my townsmen concerning my
+ mode of life, which some would call impertinent, though they do not
+ appear to me at all impertinent, but, considering the circumstances, very
+ natural and pertinent.
+
+ \`\`\`
+ this.is('a', {
+ code: 'sample',
+ })
+
+ with (multiple) {
+ blocks()
+ }
+ \`\`\`
+ `,
+ default: true,
+ type: Boolean,
+ })
+ process.stdout.columns = 40
+ t.matchSnapshot(long.describe(), 'cols=40')
+
+ process.stdout.columns = 9000
+ t.matchSnapshot(long.describe(), 'cols=9000')
+
+ process.stdout.columns = 0
+ t.matchSnapshot(long.describe(), 'cols=0')
+
+ process.stdout.columns = -1
+ t.matchSnapshot(long.describe(), 'cols=-1')
+
+ process.stdout.columns = NaN
+ t.matchSnapshot(long.describe(), 'cols=NaN')
+})
diff --git a/test/lib/utils/config/definitions.js b/test/lib/utils/config/definitions.js
new file mode 100644
index 000000000..3169feefb
--- /dev/null
+++ b/test/lib/utils/config/definitions.js
@@ -0,0 +1,697 @@
+const t = require('tap')
+
+const requireInject = require('require-inject')
+const { resolve } = require('path')
+
+// have to fake the node version, or else it'll only pass on this one
+Object.defineProperty(process, 'version', {
+ value: 'v14.8.0',
+})
+
+// also fake the npm version, so that it doesn't get reset every time
+const pkg = require('../../../../package.json')
+
+// this is a pain to keep typing
+const defpath = '../../../../lib/utils/config/definitions.js'
+
+// set this in the test when we need it
+delete process.env.NODE_ENV
+const definitions = require(defpath)
+
+const isWin = '../../../../lib/utils/is-windows.js'
+
+// snapshot these just so we note when they change
+t.matchSnapshot(Object.keys(definitions), 'all config keys')
+t.matchSnapshot(Object.keys(definitions).filter(d => d.flatten),
+ 'all config keys that are shared to flatOptions')
+
+t.equal(definitions['npm-version'].default, pkg.version, 'npm-version default')
+t.equal(definitions['node-version'].default, process.version, 'node-version default')
+
+t.test('basic flattening function camelCases from css-case', t => {
+ const flat = {}
+ const obj = { 'always-auth': true }
+ definitions['always-auth'].flatten('always-auth', obj, flat)
+ t.strictSame(flat, { alwaysAuth: true })
+ t.end()
+})
+
+t.test('editor', t => {
+ t.test('has EDITOR and VISUAL, use EDITOR', t => {
+ process.env.EDITOR = 'vim'
+ process.env.VISUAL = 'mate'
+ const defs = requireInject(defpath)
+ t.equal(defs.editor.default, 'vim')
+ t.end()
+ })
+ t.test('has VISUAL but no EDITOR, use VISUAL', t => {
+ delete process.env.EDITOR
+ process.env.VISUAL = 'mate'
+ const defs = requireInject(defpath)
+ t.equal(defs.editor.default, 'mate')
+ t.end()
+ })
+ t.test('has neither EDITOR nor VISUAL, system specific', t => {
+ delete process.env.EDITOR
+ delete process.env.VISUAL
+ const defsWin = requireInject(defpath, {
+ [isWin]: true,
+ })
+ t.equal(defsWin.editor.default, 'notepad.exe')
+ const defsNix = requireInject(defpath, {
+ [isWin]: false,
+ })
+ t.equal(defsNix.editor.default, 'vi')
+ t.end()
+ })
+ t.end()
+})
+
+t.test('shell', t => {
+ t.test('windows, env.ComSpec then cmd.exe', t => {
+ process.env.ComSpec = 'command.com'
+ const defsComSpec = requireInject(defpath, {
+ [isWin]: true,
+ })
+ t.equal(defsComSpec.shell.default, 'command.com')
+ delete process.env.ComSpec
+ const defsNoComSpec = requireInject(defpath, {
+ [isWin]: true,
+ })
+ t.equal(defsNoComSpec.shell.default, 'cmd')
+ t.end()
+ })
+
+ t.test('nix, SHELL then sh', t => {
+ process.env.SHELL = '/usr/local/bin/bash'
+ const defsShell = requireInject(defpath, {
+ [isWin]: false,
+ })
+ t.equal(defsShell.shell.default, '/usr/local/bin/bash')
+ delete process.env.SHELL
+ const defsNoShell = requireInject(defpath, {
+ [isWin]: false,
+ })
+ t.equal(defsNoShell.shell.default, 'sh')
+ t.end()
+ })
+
+ t.end()
+})
+
+t.test('local-address allowed types', t => {
+ t.test('get list from os.networkInterfaces', t => {
+ const os = {
+ tmpdir: () => '/tmp',
+ networkInterfaces: () => ({
+ eth420: [{ address: '127.0.0.1' }],
+ eth69: [{ address: 'no place like home' }],
+ }),
+ }
+ const defs = requireInject(defpath, { os })
+ t.same(defs['local-address'].type, [
+ null,
+ '127.0.0.1',
+ 'no place like home',
+ ])
+ t.end()
+ })
+ t.test('handle os.networkInterfaces throwing', t => {
+ const os = {
+ tmpdir: () => '/tmp',
+ networkInterfaces: () => {
+ throw new Error('no network interfaces for some reason')
+ },
+ }
+ const defs = requireInject(defpath, { os })
+ t.same(defs['local-address'].type, [null])
+ t.end()
+ })
+ t.end()
+})
+
+t.test('unicode allowed?', t => {
+ const { LC_ALL, LC_CTYPE, LANG } = process.env
+ t.teardown(() => Object.assign(process.env, { LC_ALL, LC_CTYPE, LANG }))
+
+ process.env.LC_ALL = 'utf8'
+ process.env.LC_CTYPE = 'UTF-8'
+ process.env.LANG = 'Unicode utf-8'
+
+ const lcAll = requireInject(defpath)
+ t.equal(lcAll.unicode.default, true)
+ process.env.LC_ALL = 'no unicode for youUUUU!'
+ const noLcAll = requireInject(defpath)
+ t.equal(noLcAll.unicode.default, false)
+
+ delete process.env.LC_ALL
+ const lcCtype = requireInject(defpath)
+ t.equal(lcCtype.unicode.default, true)
+ process.env.LC_CTYPE = 'something other than unicode version 8'
+ const noLcCtype = requireInject(defpath)
+ t.equal(noLcCtype.unicode.default, false)
+
+ delete process.env.LC_CTYPE
+ const lang = requireInject(defpath)
+ t.equal(lang.unicode.default, true)
+ process.env.LANG = 'ISO-8859-1'
+ const noLang = requireInject(defpath)
+ t.equal(noLang.unicode.default, false)
+ t.end()
+})
+
+t.test('cache', t => {
+ process.env.LOCALAPPDATA = 'app/data/local'
+ const defsWinLocalAppData = requireInject(defpath, {
+ [isWin]: true,
+ })
+ t.equal(defsWinLocalAppData.cache.default, 'app/data/local/npm-cache')
+
+ delete process.env.LOCALAPPDATA
+ const defsWinNoLocalAppData = requireInject(defpath, {
+ [isWin]: true,
+ })
+ t.equal(defsWinNoLocalAppData.cache.default, '~/npm-cache')
+
+ const defsNix = requireInject(defpath, {
+ [isWin]: false,
+ })
+ t.equal(defsNix.cache.default, '~/.npm')
+
+ const flat = {}
+ defsNix.cache.flatten('cache', { cache: '/some/cache/value' }, flat)
+ const {join} = require('path')
+ t.equal(flat.cache, join('/some/cache/value', '_cacache'))
+
+ t.end()
+})
+
+t.test('flatteners that populate flat.omit array', t => {
+ t.test('also', t => {
+ const flat = {}
+ const obj = {}
+
+ // ignored if setting is not dev or development
+ obj.also = 'ignored'
+ definitions.also.flatten('also', obj, flat)
+ t.strictSame(obj, {also: 'ignored'}, 'nothing done')
+ t.strictSame(flat, {}, 'nothing done')
+
+ obj.also = 'development'
+ definitions.also.flatten('also', obj, flat)
+ t.strictSame(obj, { also: 'development', include: ['dev'] }, 'marked dev as included')
+ t.strictSame(flat, { omit: [] }, 'nothing omitted, so nothing changed')
+
+ obj.omit = ['dev', 'optional']
+ obj.include = []
+ definitions.also.flatten('also', obj, flat)
+ t.strictSame(obj, { also: 'development', omit: ['dev', 'optional'], include: ['dev'] }, 'marked dev as included')
+ t.strictSame(flat, { omit: ['optional'] }, 'removed dev from omit')
+ t.end()
+ })
+
+ t.test('include', t => {
+ const flat = {}
+ const obj = { include: ['dev'] }
+ definitions.include.flatten('include', obj, flat)
+ t.strictSame(flat, {omit: []}, 'not omitting anything')
+ obj.omit = ['optional', 'dev']
+ definitions.include.flatten('include', obj, flat)
+ t.strictSame(flat, {omit: ['optional']}, 'only omitting optional')
+ t.end()
+ })
+
+ t.test('omit', t => {
+ const flat = {}
+ const obj = { include: ['dev'], omit: ['dev', 'optional'] }
+ definitions.omit.flatten('omit', obj, flat)
+ t.strictSame(flat, { omit: ['optional'] }, 'do not omit what is included')
+
+ process.env.NODE_ENV = 'production'
+ const defProdEnv = requireInject(defpath)
+ t.strictSame(defProdEnv.omit.default, ['dev'], 'omit dev in production')
+ t.end()
+ })
+
+ t.test('only', t => {
+ const flat = {}
+ const obj = { only: 'asdf' }
+ definitions.only.flatten('only', obj, flat)
+ t.strictSame(flat, {}, 'ignored if value is not production')
+
+ obj.only = 'prod'
+ definitions.only.flatten('only', obj, flat)
+ t.strictSame(flat, {omit: ['dev']}, 'omit dev when --only=prod')
+
+ obj.include = ['dev']
+ flat.omit = []
+ definitions.only.flatten('only', obj, flat)
+ t.strictSame(flat, {omit: []}, 'do not omit when included')
+
+ t.end()
+ })
+
+ t.test('optional', t => {
+ const flat = {}
+ const obj = { optional: null }
+
+ definitions.optional.flatten('optional', obj, flat)
+ t.strictSame(obj, { optional: null }, 'do nothing by default')
+ t.strictSame(flat, {}, 'do nothing by default')
+
+ obj.optional = true
+ definitions.optional.flatten('optional', obj, flat)
+ t.strictSame(obj, {include: ['optional'], optional: true}, 'include optional when set')
+ t.strictSame(flat, {omit: []}, 'nothing to omit in flatOptions')
+
+ delete obj.include
+ obj.optional = false
+ definitions.optional.flatten('optional', obj, flat)
+ t.strictSame(obj, {omit: ['optional'], optional: false}, 'omit optional when set false')
+ t.strictSame(flat, {omit: ['optional']}, 'omit optional when set false')
+
+ t.end()
+ })
+
+ t.test('production', t => {
+ const flat = {}
+ const obj = {production: true}
+ definitions.production.flatten('production', obj, flat)
+ t.strictSame(obj, {production: true, omit: ['dev']}, '--production sets --omit=dev')
+ t.strictSame(flat, {omit: ['dev']}, '--production sets --omit=dev')
+
+ delete obj.omit
+ obj.production = false
+ delete flat.omit
+ definitions.production.flatten('production', obj, flat)
+ t.strictSame(obj, {production: false}, '--no-production has no effect')
+ t.strictSame(flat, {}, '--no-production has no effect')
+
+ obj.production = true
+ obj.include = ['dev']
+ definitions.production.flatten('production', obj, flat)
+ t.strictSame(obj, {production: true, include: ['dev'], omit: ['dev']}, 'omit and include dev')
+ t.strictSame(flat, {omit: []}, 'do not omit dev when included')
+
+ t.end()
+ })
+
+ t.end()
+})
+
+t.test('cache-max', t => {
+ const flat = {}
+ const obj = { 'cache-max': 10342 }
+ definitions['cache-max'].flatten('cache-max', obj, flat)
+ t.strictSame(flat, {}, 'no effect if not <= 0')
+ obj['cache-max'] = 0
+ definitions['cache-max'].flatten('cache-max', obj, flat)
+ t.strictSame(flat, {preferOnline: true}, 'preferOnline if <= 0')
+ t.end()
+})
+
+t.test('cache-min', t => {
+ const flat = {}
+ const obj = { 'cache-min': 123 }
+ definitions['cache-min'].flatten('cache-min', obj, flat)
+ t.strictSame(flat, {}, 'no effect if not >= 9999')
+ obj['cache-min'] = 9999
+ definitions['cache-min'].flatten('cache-min', obj, flat)
+ t.strictSame(flat, {preferOffline: true}, 'preferOffline if >=9999')
+ t.end()
+})
+
+t.test('color', t => {
+ const { isTTY } = process.stdout
+ t.teardown(() => process.stdout.isTTY = isTTY)
+
+ const flat = {}
+ const obj = { color: 'always' }
+
+ definitions.color.flatten('color', obj, flat)
+ t.strictSame(flat, {color: true}, 'true when --color=always')
+
+ obj.color = false
+ definitions.color.flatten('color', obj, flat)
+ t.strictSame(flat, {color: false}, 'true when --no-color')
+
+ process.stdout.isTTY = false
+ obj.color = true
+ definitions.color.flatten('color', obj, flat)
+ t.strictSame(flat, {color: false}, 'no color when stdout not tty')
+ process.stdout.isTTY = true
+ definitions.color.flatten('color', obj, flat)
+ t.strictSame(flat, {color: true}, '--color turns on color when stdout is tty')
+
+ delete process.env.NO_COLOR
+ const defsAllowColor = requireInject(defpath)
+ t.equal(defsAllowColor.color.default, true, 'default true when no NO_COLOR env')
+
+ process.env.NO_COLOR = '0'
+ const defsNoColor0 = requireInject(defpath)
+ t.equal(defsNoColor0.color.default, true, 'default true when no NO_COLOR=0')
+
+ process.env.NO_COLOR = '1'
+ const defsNoColor1 = requireInject(defpath)
+ t.equal(defsNoColor1.color.default, false, 'default false when no NO_COLOR=1')
+
+ t.end()
+})
+
+t.test('retry options', t => {
+ const obj = {}
+ // <config>: flat.retry[<option>]
+ const mapping = {
+ 'fetch-retries': 'retries',
+ 'fetch-retry-factor': 'factor',
+ 'fetch-retry-maxtimeout': 'maxTimeout',
+ 'fetch-retry-mintimeout': 'minTimeout',
+ }
+ for (const [config, option] of Object.entries(mapping)) {
+ const msg = `${config} -> retry.${option}`
+ const flat = {}
+ obj[config] = 99
+ definitions[config].flatten(config, obj, flat)
+ t.strictSame(flat, {retry: {[option]: 99}}, msg)
+ delete obj[config]
+ }
+ t.end()
+})
+
+t.test('search options', t => {
+ const obj = {}
+ // <config>: flat.search[<option>]
+ const mapping = {
+ description: 'description',
+ searchexclude: 'exclude',
+ searchlimit: 'limit',
+ searchstaleness: 'staleness',
+ }
+
+ for (const [config, option] of Object.entries(mapping)) {
+ const msg = `${config} -> search.${option}`
+ const flat = {}
+ obj[config] = 99
+ definitions[config].flatten(config, obj, flat)
+ t.strictSame(flat, { search: { limit: 20, [option]: 99 }}, msg)
+ delete obj[config]
+ }
+
+ const flat = {}
+ obj.searchopts = 'a=b&b=c'
+ definitions.searchopts.flatten('searchopts', obj, flat)
+ t.strictSame(flat, {
+ search: {
+ limit: 20,
+ opts: Object.assign(Object.create(null), {
+ a: 'b',
+ b: 'c',
+ }),
+ },
+ }, 'searchopts -> querystring.parse() -> search.opts')
+ delete obj.searchopts
+
+ t.end()
+})
+
+t.test('noProxy', t => {
+ const obj = { noproxy: ['1.2.3.4,2.3.4.5', '3.4.5.6'] }
+ const flat = {}
+ definitions.noproxy.flatten('noproxy', obj, flat)
+ t.strictSame(flat, { noProxy: '1.2.3.4,2.3.4.5,3.4.5.6' })
+ t.end()
+})
+
+t.test('maxSockets', t => {
+ const obj = { maxsockets: 123 }
+ const flat = {}
+ definitions.maxsockets.flatten('maxsockets', obj, flat)
+ t.strictSame(flat, { maxSockets: 123 })
+ t.end()
+})
+
+t.test('projectScope', t => {
+ const obj = { scope: 'asdf' }
+ const flat = {}
+ definitions.scope.flatten('scope', obj, flat)
+ t.strictSame(flat, { projectScope: '@asdf' }, 'prepend @ if needed')
+
+ obj.scope = '@asdf'
+ definitions.scope.flatten('scope', obj, flat)
+ t.strictSame(flat, { projectScope: '@asdf' }, 'leave untouched if has @')
+
+ t.end()
+})
+
+t.test('strictSSL', t => {
+ const obj = { 'strict-ssl': false }
+ const flat = {}
+ definitions['strict-ssl'].flatten('strict-ssl', obj, flat)
+ t.strictSame(flat, { strictSSL: false })
+ obj['strict-ssl'] = true
+ definitions['strict-ssl'].flatten('strict-ssl', obj, flat)
+ t.strictSame(flat, { strictSSL: true })
+ t.end()
+})
+
+t.test('shrinkwrap/package-lock', t => {
+ const obj = { shrinkwrap: false }
+ const flat = {}
+ definitions.shrinkwrap.flatten('shrinkwrap', obj, flat)
+ t.strictSame(flat, {packageLock: false})
+ obj.shrinkwrap = true
+ definitions.shrinkwrap.flatten('shrinkwrap', obj, flat)
+ t.strictSame(flat, {packageLock: true})
+
+ delete obj.shrinkwrap
+ obj['package-lock'] = false
+ definitions['package-lock'].flatten('package-lock', obj, flat)
+ t.strictSame(flat, {packageLock: false})
+ obj['package-lock'] = true
+ definitions['package-lock'].flatten('package-lock', obj, flat)
+ t.strictSame(flat, {packageLock: true})
+
+ t.end()
+})
+
+t.test('scriptShell', t => {
+ const obj = { 'script-shell': null }
+ const flat = {}
+ definitions['script-shell'].flatten('script-shell', obj, flat)
+ t.ok(Object.prototype.hasOwnProperty.call(flat, 'scriptShell'),
+ 'should set it to undefined explicitly')
+ t.strictSame(flat, { scriptShell: undefined }, 'no other fields')
+
+ obj['script-shell'] = 'asdf'
+ definitions['script-shell'].flatten('script-shell', obj, flat)
+ t.strictSame(flat, { scriptShell: 'asdf' }, 'sets if not falsey')
+
+ t.end()
+})
+
+t.test('defaultTag', t => {
+ const obj = { tag: 'next' }
+ const flat = {}
+ definitions.tag.flatten('tag', obj, flat)
+ t.strictSame(flat, {defaultTag: 'next'})
+ t.end()
+})
+
+t.test('timeout', t => {
+ const obj = { 'fetch-timeout': 123 }
+ const flat = {}
+ definitions['fetch-timeout'].flatten('fetch-timeout', obj, flat)
+ t.strictSame(flat, {timeout: 123})
+ t.end()
+})
+
+t.test('saveType', t => {
+ t.test('save-prod', t => {
+ const obj = { 'save-prod': false }
+ const flat = {}
+ definitions['save-prod'].flatten('save-prod', obj, flat)
+ t.strictSame(flat, {}, 'no effect if false and missing')
+ flat.saveType = 'prod'
+ definitions['save-prod'].flatten('save-prod', obj, flat)
+ t.strictSame(flat, {}, 'remove if false and set to prod')
+ flat.saveType = 'dev'
+ definitions['save-prod'].flatten('save-prod', obj, flat)
+ t.strictSame(flat, {saveType: 'dev'}, 'ignore if false and not already prod')
+ obj['save-prod'] = true
+ definitions['save-prod'].flatten('save-prod', obj, flat)
+ t.strictSame(flat, {saveType: 'prod'}, 'set to prod if true')
+ t.end()
+ })
+
+ t.test('save-dev', t => {
+ const obj = { 'save-dev': false }
+ const flat = {}
+ definitions['save-dev'].flatten('save-dev', obj, flat)
+ t.strictSame(flat, {}, 'no effect if false and missing')
+ flat.saveType = 'dev'
+ definitions['save-dev'].flatten('save-dev', obj, flat)
+ t.strictSame(flat, {}, 'remove if false and set to dev')
+ flat.saveType = 'prod'
+ obj['save-dev'] = false
+ definitions['save-dev'].flatten('save-dev', obj, flat)
+ t.strictSame(flat, {saveType: 'prod'}, 'ignore if false and not already dev')
+ obj['save-dev'] = true
+ definitions['save-dev'].flatten('save-dev', obj, flat)
+ t.strictSame(flat, {saveType: 'dev'}, 'set to dev if true')
+ t.end()
+ })
+
+ t.test('save-bundle', t => {
+ const obj = { 'save-bundle': true }
+ const flat = {}
+ definitions['save-bundle'].flatten('save-bundle', obj, flat)
+ t.strictSame(flat, {saveBundle: true}, 'set the saveBundle flag')
+
+ obj['save-bundle'] = false
+ definitions['save-bundle'].flatten('save-bundle', obj, flat)
+ t.strictSame(flat, {saveBundle: false}, 'unset the saveBundle flag')
+
+ obj['save-bundle'] = true
+ obj['save-peer'] = true
+ definitions['save-bundle'].flatten('save-bundle', obj, flat)
+ t.strictSame(flat, {saveBundle: false}, 'false if save-peer is set')
+
+ t.end()
+ })
+
+ t.test('save-peer', t => {
+ const obj = { 'save-peer': false}
+ const flat = {}
+ definitions['save-peer'].flatten('save-peer', obj, flat)
+ t.strictSame(flat, {}, 'no effect if false and not yet set')
+
+ obj['save-peer'] = true
+ definitions['save-peer'].flatten('save-peer', obj, flat)
+ t.strictSame(flat, {saveType: 'peer'}, 'set saveType to peer if unset')
+
+ flat.saveType = 'optional'
+ definitions['save-peer'].flatten('save-peer', obj, flat)
+ t.strictSame(flat, {saveType: 'peerOptional'}, 'set to peerOptional if optional already')
+
+ definitions['save-peer'].flatten('save-peer', obj, flat)
+ t.strictSame(flat, {saveType: 'peerOptional'}, 'no effect if already peerOptional')
+
+ obj['save-peer'] = false
+ definitions['save-peer'].flatten('save-peer', obj, flat)
+ t.strictSame(flat, {saveType: 'optional'}, 'switch peerOptional to optional if false')
+
+ obj['save-peer'] = false
+ flat.saveType = 'peer'
+ definitions['save-peer'].flatten('save-peer', obj, flat)
+ t.strictSame(flat, {}, 'remove saveType if peer and setting false')
+
+ t.end()
+ })
+
+ t.test('save-optional', t => {
+ const obj = { 'save-optional': false}
+ const flat = {}
+ definitions['save-optional'].flatten('save-optional', obj, flat)
+ t.strictSame(flat, {}, 'no effect if false and not yet set')
+
+ obj['save-optional'] = true
+ definitions['save-optional'].flatten('save-optional', obj, flat)
+ t.strictSame(flat, {saveType: 'optional'}, 'set saveType to optional if unset')
+
+ flat.saveType = 'peer'
+ definitions['save-optional'].flatten('save-optional', obj, flat)
+ t.strictSame(flat, {saveType: 'peerOptional'}, 'set to peerOptional if peer already')
+
+ definitions['save-optional'].flatten('save-optional', obj, flat)
+ t.strictSame(flat, {saveType: 'peerOptional'}, 'no effect if already peerOptional')
+
+ obj['save-optional'] = false
+ definitions['save-optional'].flatten('save-optional', obj, flat)
+ t.strictSame(flat, {saveType: 'peer'}, 'switch peerOptional to peer if false')
+
+ flat.saveType = 'optional'
+ definitions['save-optional'].flatten('save-optional', obj, flat)
+ t.strictSame(flat, {}, 'remove saveType if optional and setting false')
+
+ t.end()
+ })
+
+ t.end()
+})
+
+t.test('cafile -> flat.ca', t => {
+ const path = t.testdir({
+ cafile: `
+-----BEGIN CERTIFICATE-----
+XXXX
+XXXX
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+YYYY\r
+YYYY\r
+-----END CERTIFICATE-----
+`,
+ })
+ const cafile = resolve(path, 'cafile')
+
+ const obj = {}
+ const flat = {}
+ definitions.cafile.flatten('cafile', obj, flat)
+ t.strictSame(flat, {}, 'no effect if no cafile set')
+ obj.cafile = resolve(path, 'no/cafile/here')
+ definitions.cafile.flatten('cafile', obj, flat)
+ t.strictSame(flat, {}, 'no effect if cafile not found')
+ obj.cafile = cafile
+ definitions.cafile.flatten('cafile', obj, flat)
+ t.strictSame(flat, {
+ ca: [
+ '-----BEGIN CERTIFICATE-----\nXXXX\nXXXX\n-----END CERTIFICATE-----',
+ '-----BEGIN CERTIFICATE-----\nYYYY\nYYYY\n-----END CERTIFICATE-----',
+ ],
+ })
+ t.test('error other than ENOENT gets thrown', t => {
+ const poo = new Error('poo')
+ const defnReadFileThrows = requireInject(defpath, {
+ fs: {
+ ...require('fs'),
+ readFileSync: () => {
+ throw poo
+ },
+ },
+ })
+ t.throws(() => defnReadFileThrows.cafile.flatten('cafile', obj, {}), poo)
+ t.end()
+ })
+
+ t.end()
+})
+
+t.test('detect CI', t => {
+ const defnNoCI = requireInject(defpath, {
+ '@npmcli/ci-detect': () => false,
+ })
+ const defnCIFoo = requireInject(defpath, {
+ '@npmcli/ci-detect': () => 'foo',
+ })
+ t.equal(defnNoCI['ci-name'].default, null, 'null when not in CI')
+ t.equal(defnCIFoo['ci-name'].default, 'foo', 'name of CI when in CI')
+ t.end()
+})
+
+t.test('user-agent', t => {
+ const obj = {
+ 'user-agent': definitions['user-agent'].default,
+ 'npm-version': '1.2.3',
+ 'node-version': '9.8.7',
+ }
+ const flat = {}
+ const expectNoCI = `npm/1.2.3 node/9.8.7 ` +
+ `${process.platform} ${process.arch}`
+ definitions['user-agent'].flatten('user-agent', obj, flat)
+ t.equal(flat.userAgent, expectNoCI)
+ obj['ci-name'] = 'foo'
+ const expectCI = `${expectNoCI} ci/foo`
+ definitions['user-agent'].flatten('user-agent', obj, flat)
+ t.equal(flat.userAgent, expectCI)
+ t.end()
+})
diff --git a/test/lib/utils/config/describe-all.js b/test/lib/utils/config/describe-all.js
new file mode 100644
index 000000000..814d92ac9
--- /dev/null
+++ b/test/lib/utils/config/describe-all.js
@@ -0,0 +1,6 @@
+const t = require('tap')
+const describeAll = require('../../../../lib/utils/config/describe-all.js')
+// this basically ends up being a duplicate of the helpdoc dumped into
+// a snapshot, but it verifies that we get the same help output on every
+// platform where we run CI.
+t.matchSnapshot(describeAll())
diff --git a/test/lib/utils/config/flatten.js b/test/lib/utils/config/flatten.js
new file mode 100644
index 000000000..9fac0820c
--- /dev/null
+++ b/test/lib/utils/config/flatten.js
@@ -0,0 +1,34 @@
+const t = require('tap')
+const flatten = require('../../../../lib/utils/config/flatten.js')
+
+require.main.filename = '/path/to/npm'
+delete process.env.NODE
+process.execPath = '/path/to/node'
+
+const obj = {
+ 'save-dev': true,
+ '@foobar:registry': 'https://foo.bar.com/',
+ '//foo.bar.com:_authToken': 'foobarbazquuxasdf',
+ userconfig: '/path/to/.npmrc',
+}
+
+const flat = flatten(obj)
+t.strictSame(flat, {
+ saveType: 'dev',
+ '@foobar:registry': 'https://foo.bar.com/',
+ '//foo.bar.com:_authToken': 'foobarbazquuxasdf',
+ npmBin: '/path/to/npm',
+ nodeBin: '/path/to/node',
+ hashAlgorithm: 'sha1',
+})
+
+// now flatten something else on top of it.
+process.env.NODE = '/usr/local/bin/node.exe'
+flatten({ 'save-dev': false }, flat)
+t.strictSame(flat, {
+ '@foobar:registry': 'https://foo.bar.com/',
+ '//foo.bar.com:_authToken': 'foobarbazquuxasdf',
+ npmBin: '/path/to/npm',
+ nodeBin: '/usr/local/bin/node.exe',
+ hashAlgorithm: 'sha1',
+})
diff --git a/test/lib/utils/config/index.js b/test/lib/utils/config/index.js
new file mode 100644
index 000000000..75d72e784
--- /dev/null
+++ b/test/lib/utils/config/index.js
@@ -0,0 +1,24 @@
+const t = require('tap')
+const config = require('../../../../lib/utils/config/index.js')
+const flatten = require('../../../../lib/utils/config/flatten.js')
+const definitions = require('../../../../lib/utils/config/definitions.js')
+const describeAll = require('../../../../lib/utils/config/describe-all.js')
+t.matchSnapshot(config.shorthands, 'shorthands')
+
+// just spot check a few of these to show that we got defaults assembled
+t.match(config.defaults, {
+ registry: definitions.registry.default,
+ 'init-module': definitions['init-module'].default,
+})
+
+// is a getter, so changes are reflected
+definitions.registry.default = 'https://example.com'
+t.strictSame(config.defaults.registry, 'https://example.com')
+
+t.strictSame(config, {
+ defaults: config.defaults,
+ shorthands: config.shorthands,
+ flatten,
+ definitions,
+ describeAll,
+})