diff options
author | isaacs <i@izs.me> | 2021-03-16 02:15:27 +0300 |
---|---|---|
committer | isaacs <i@izs.me> | 2021-03-18 21:58:08 +0300 |
commit | 6598bfe8697439e827d84981f8504febca64a55a (patch) | |
tree | 670bb556880a187e8ba82e843f932222af76fbd8 /test | |
parent | 8cce4282f7bef11aeeb73cffd532b477b241985e (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.js | 150 | ||||
-rw-r--r-- | test/lib/utils/config/definitions.js | 697 | ||||
-rw-r--r-- | test/lib/utils/config/describe-all.js | 6 | ||||
-rw-r--r-- | test/lib/utils/config/flatten.js | 34 | ||||
-rw-r--r-- | test/lib/utils/config/index.js | 24 |
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, +}) |