diff --git a/lib/config.js b/lib/config.js index a56dd92ffbde6..2df7bf513437c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -121,7 +121,7 @@ class Config extends BaseCommand { break case 'list': case 'ls': - await (this.npm.config.get('json') ? this.listJson() : this.list()) + await (this.npm.flatOptions.json ? this.listJson() : this.list()) break case 'edit': await this.edit() @@ -138,7 +138,7 @@ class Config extends BaseCommand { if (!args.length) throw this.usageError() - const where = this.npm.config.get('location') + const where = this.npm.flatOptions.location for (const [key, val] of Object.entries(keyValues(args))) { this.npm.log.info('config', 'set %j %j', key, val) this.npm.config.set(key, val || '', where) @@ -168,15 +168,15 @@ class Config extends BaseCommand { if (!keys.length) throw this.usageError() - const where = this.npm.config.get('location') + const where = this.npm.flatOptions.location for (const key of keys) this.npm.config.delete(key, where) await this.npm.config.save(where) } async edit () { - const e = this.npm.config.get('editor') - const where = this.npm.config.get('location') + const e = this.npm.flatOptions.editor + const where = this.npm.flatOptions.location const file = this.npm.config.data.get(where).source // save first, just to make sure it's synced up @@ -232,6 +232,7 @@ ${defData} async list () { const msg = [] + // long does not have a flattener const long = this.npm.config.get('long') for (const [where, { data, source }] of this.npm.config.data.entries()) { if (where === 'default' && !long) diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index c71781627872a..092e0fc435cb4 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -804,7 +804,11 @@ define('global', { * bin files are linked to \`{prefix}/bin\` * man pages are linked to \`{prefix}/share/man\` `, - flatten, + flatten: (key, obj, flatOptions) => { + flatten(key, obj, flatOptions) + if (flatOptions.global) + flatOptions.location = 'global' + }, }) define('global-style', { @@ -1131,14 +1135,10 @@ define('location', { description: ` When passed to \`npm config\` this refers to which config file to use. `, - // NOTE: the flattener here deliberately does not alter the value of global - // for now, this is to avoid inadvertently causing any breakage. the value of - // global, however, does modify this flag. - flatten (key, obj, flatOptions) { - // if global is set, we override ourselves - if (obj.global) - obj.location = 'global' - flatOptions.location = obj.location + flatten: (key, obj, flatOptions) => { + flatten(key, obj, flatOptions) + if (flatOptions.global) + flatOptions.location = 'global' }, }) @@ -1359,7 +1359,11 @@ define('package-lock', { modules will also be disabled. To remove extraneous modules with package-locks disabled use \`npm prune\`. `, - flatten, + flatten: (key, obj, flatOptions) => { + flatten(key, obj, flatOptions) + if (flatOptions.packageLockOnly) + flatOptions.packageLock = true + }, }) define('package-lock-only', { @@ -1375,7 +1379,11 @@ define('package-lock-only', { For \`list\` this means the output will be based on the tree described by the \`package-lock.json\`, rather than the contents of \`node_modules\`. `, - flatten, + flatten: (key, obj, flatOptions) => { + flatten(key, obj, flatOptions) + if (flatOptions.packageLockOnly) + flatOptions.packageLock = true + }, }) define('pack-destination', { diff --git a/tap-snapshots/test/lib/config.js.test.cjs b/tap-snapshots/test/lib/config.js.test.cjs index b5acbb0af94c5..817f3c173f19e 100644 --- a/tap-snapshots/test/lib/config.js.test.cjs +++ b/tap-snapshots/test/lib/config.js.test.cjs @@ -5,158 +5,334 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' -exports[`test/lib/config.js TAP config edit --location=global > should write global config file 1`] = ` -;;;; -; npm globalconfig file: /etc/npmrc -; this is a simple ini-formatted file -; lines that start with semi-colons are comments -; run \`npm help 7 config\` for documentation of the various options -; -; Configs like \`@scope:registry\` map a scope to a given registry url. -; -; Configs like \`///:_authToken\` are auth that is restricted -; to the registry host specified. - -init.author.name=Foo - -;;;; -; all available options shown below with default values -;;;; - - -; init-author-name= -; init-version=1.0.0 -; init.author.name= -; init.version=1.0.0 - -` - -exports[`test/lib/config.js TAP config edit > should write config file 1`] = ` -;;;; -; npm userconfig file: ~/.npmrc -; this is a simple ini-formatted file -; lines that start with semi-colons are comments -; run \`npm help 7 config\` for documentation of the various options -; -; Configs like \`@scope:registry\` map a scope to a given registry url. -; -; Configs like \`///:_authToken\` are auth that is restricted -; to the registry host specified. - -//registry.npmjs.org/:_authToken=0000000 -init.author.name=Foo -sign-git-commit=true - -;;;; -; all available options shown below with default values -;;;; - - -; init-author-name= -; init-version=1.0.0 -; init.author.name= -; init.version=1.0.0 - -` - -exports[`test/lib/config.js TAP config edit > should write config file 2`] = ` -;;;; -; npm userconfig file: ~/.npmrc -; this is a simple ini-formatted file -; lines that start with semi-colons are comments -; run \`npm help 7 config\` for documentation of the various options -; -; Configs like \`@scope:registry\` map a scope to a given registry url. -; -; Configs like \`///:_authToken\` are auth that is restricted -; to the registry host specified. - - - -;;;; -; all available options shown below with default values -;;;; - - -; init-author-name= -; init-version=1.0.0 -; init.author.name= -; init.version=1.0.0 - +exports[`test/lib/config.js TAP config list --json > output matches snapshot 1`] = ` +{ + "prefix": "{LOCALPREFIX}", + "userconfig": "{HOME}/.npmrc", + "json": true, + "projectloaded": "yes", + "userloaded": "yes", + "globalloaded": "yes", + "access": null, + "all": false, + "allow-same-version": false, + "also": null, + "audit": true, + "audit-level": null, + "auth-type": "legacy", + "before": null, + "bin-links": true, + "browser": null, + "ca": null, + "cache": "{CACHE}", + "cache-max": null, + "cache-min": 0, + "cafile": null, + "call": "", + "cert": null, + "ci-name": null, + "cidr": null, + "color": true, + "commit-hooks": true, + "depth": null, + "description": true, + "dev": false, + "diff": [], + "diff-ignore-all-space": false, + "diff-name-only": false, + "diff-no-prefix": false, + "diff-dst-prefix": "b/", + "diff-src-prefix": "a/", + "diff-text": false, + "diff-unified": 3, + "dry-run": false, + "editor": "{EDITOR}", + "engine-strict": false, + "fetch-retries": 2, + "fetch-retry-factor": 10, + "fetch-retry-maxtimeout": 60000, + "fetch-retry-mintimeout": 10000, + "fetch-timeout": 300000, + "force": false, + "foreground-scripts": false, + "format-package-lock": true, + "fund": true, + "git": "git", + "git-tag-version": true, + "global": false, + "global-style": false, + "globalconfig": "{GLOBALPREFIX}/npmrc", + "heading": "npm", + "https-proxy": null, + "if-present": false, + "ignore-scripts": false, + "include": [], + "include-staged": false, + "init-author-email": "", + "init-author-name": "", + "init-author-url": "", + "init-license": "ISC", + "init-module": "{HOME}/.npm-init.js", + "init-version": "1.0.0", + "init.author.email": "", + "init.author.name": "", + "init.author.url": "", + "init.license": "ISC", + "init.module": "{HOME}/.npm-init.js", + "init.version": "1.0.0", + "key": null, + "legacy-bundling": false, + "legacy-peer-deps": false, + "link": false, + "local-address": null, + "location": "user", + "loglevel": "notice", + "logs-max": 10, + "long": false, + "maxsockets": 15, + "message": "%s", + "node-options": null, + "node-version": "{NODE-VERSION}", + "noproxy": [ + "" + ], + "npm-version": "{NPM-VERSION}", + "offline": false, + "omit": [], + "only": null, + "optional": null, + "otp": null, + "package": [], + "package-lock": true, + "package-lock-only": false, + "pack-destination": ".", + "parseable": false, + "prefer-offline": false, + "prefer-online": false, + "preid": "", + "production": null, + "progress": true, + "proxy": null, + "read-only": false, + "rebuild-bundle": true, + "registry": "https://registry.npmjs.org/", + "save": true, + "save-bundle": false, + "save-dev": false, + "save-exact": false, + "save-optional": false, + "save-peer": false, + "save-prefix": "^", + "save-prod": false, + "scope": "", + "script-shell": null, + "searchexclude": "", + "searchlimit": 20, + "searchopts": "", + "searchstaleness": 900, + "shell": "{SHELL}", + "shrinkwrap": true, + "sign-git-commit": false, + "sign-git-tag": false, + "sso-poll-frequency": 500, + "sso-type": "oauth", + "strict-peer-deps": false, + "strict-ssl": true, + "tag": "latest", + "tag-version-prefix": "v", + "timing": false, + "tmp": "{TMP}", + "umask": 0, + "unicode": false, + "update-notifier": true, + "usage": false, + "user-agent": "npm/{NPM-VERSION} node/{NODE-VERSION} {PLATFORM} {ARCH} workspaces/false", + "version": false, + "versions": false, + "viewer": "{VIEWER}", + "which": null, + "workspace": [], + "workspaces": false, + "yes": null, + "metrics-registry": "https://registry.npmjs.org/" +} ` -exports[`test/lib/config.js TAP config get no args > should list configs on config get no args 1`] = ` -; "cli" config from command line options - -cat = true -chai = true -dog = true -editor = "vi" -json = false -location = "user" -long = false - -; node bin location = /path/to/node -; cwd = {CWD} -; HOME = ~/ -; Run \`npm config ls -l\` to show all defaults. -` - -exports[`test/lib/config.js TAP config list --long > should list all configs 1`] = ` +exports[`test/lib/config.js TAP config list --long > output matches snapshot 1`] = ` ; "default" config from default values +_auth = (protected) +access = null +all = false +allow-same-version = false +also = null +audit = true +audit-level = null +auth-type = "legacy" +before = null +bin-links = true +browser = null +ca = null +cache = "{CACHE}" +cache-max = null +cache-min = 0 +cafile = null +call = "" +cert = null +ci-name = null +cidr = null +color = true +commit-hooks = true +depth = null +description = true +dev = false +diff = [] +diff-dst-prefix = "b/" +diff-ignore-all-space = false +diff-name-only = false +diff-no-prefix = false +diff-src-prefix = "a/" +diff-text = false +diff-unified = 3 +dry-run = false +editor = "{EDITOR}" +engine-strict = false +fetch-retries = 2 +fetch-retry-factor = 10 +fetch-retry-maxtimeout = 60000 +fetch-retry-mintimeout = 10000 +fetch-timeout = 300000 +force = false +foreground-scripts = false +format-package-lock = true +fund = true +git = "git" +git-tag-version = true +global = false +global-style = false +globalconfig = "{GLOBALPREFIX}/npmrc" +heading = "npm" +https-proxy = null +if-present = false +ignore-scripts = false +include = [] +include-staged = false +init-author-email = "" init-author-name = "" +init-author-url = "" +init-license = "ISC" +init-module = "{HOME}/.npm-init.js" init-version = "1.0.0" +init.author.email = "" init.author.name = "" +init.author.url = "" +init.license = "ISC" +init.module = "{HOME}/.npm-init.js" init.version = "1.0.0" - -; "cli" config from command line options - -cat = true -chai = true -dog = true -editor = "vi" json = false +key = null +legacy-bundling = false +legacy-peer-deps = false +link = false +local-address = null location = "user" -long = true -` +loglevel = "notice" +logs-max = 10 +; long = false ; overridden by cli +maxsockets = 15 +message = "%s" +metrics-registry = "https://registry.npmjs.org/" +node-options = null +node-version = "{NODE-VERSION}" +noproxy = [""] +npm-version = "{NPM-VERSION}" +offline = false +omit = [] +only = null +optional = null +otp = null +pack-destination = "." +package = [] +package-lock = true +package-lock-only = false +parseable = false +prefer-offline = false +prefer-online = false +; prefix = "{REALGLOBALREFIX}" ; overridden by cli +preid = "" +production = null +progress = true +proxy = null +read-only = false +rebuild-bundle = true +registry = "https://registry.npmjs.org/" +save = true +save-bundle = false +save-dev = false +save-exact = false +save-optional = false +save-peer = false +save-prefix = "^" +save-prod = false +scope = "" +script-shell = null +searchexclude = "" +searchlimit = 20 +searchopts = "" +searchstaleness = 900 +shell = "{SHELL}" +shrinkwrap = true +sign-git-commit = false +sign-git-tag = false +sso-poll-frequency = 500 +sso-type = "oauth" +strict-peer-deps = false +strict-ssl = true +tag = "latest" +tag-version-prefix = "v" +timing = false +tmp = "{TMP}" +umask = 0 +unicode = false +update-notifier = true +usage = false +user-agent = "npm/{NPM-VERSION} node/{NODE-VERSION} {PLATFORM} {ARCH} workspaces/false" +; userconfig = "{HOME}/.npmrc" ; overridden by cli +version = false +versions = false +viewer = "{VIEWER}" +which = null +workspace = [] +workspaces = false +yes = null + +; "global" config from {GLOBALPREFIX}/npmrc + +globalloaded = "yes" + +; "user" config from {HOME}/.npmrc + +userloaded = "yes" + +; "project" config from {LOCALPREFIX}/.npmrc + +projectloaded = "yes" -exports[`test/lib/config.js TAP config list > should list configs 1`] = ` ; "cli" config from command line options -cat = true -chai = true -dog = true -editor = "vi" -json = false -location = "user" -long = false - -; node bin location = /path/to/node -; cwd = {CWD} -; HOME = ~/ -; Run \`npm config ls -l\` to show all defaults. +long = true +prefix = "{LOCALPREFIX}" +userconfig = "{HOME}/.npmrc" ` -exports[`test/lib/config.js TAP config list overrides > should list overridden configs 1`] = ` +exports[`test/lib/config.js TAP config list > output matches snapshot 1`] = ` ; "cli" config from command line options -cat = true -chai = true -dog = true -editor = "vi" -init.author.name = "Bar" -json = false -location = "user" -long = false - -; "user" config from ~/.npmrc - -; //private-reg.npmjs.org/:_authThoken = (protected) ; overridden by cli -; init.author.name = "Foo" ; overridden by cli +prefix = "{LOCALPREFIX}" +userconfig = "{HOME}/.npmrc" -; node bin location = /path/to/node -; cwd = {CWD} -; HOME = ~/ +; node bin location = {EXECPATH} +; cwd = {NPMDIR} +; HOME = {HOME} ; Run \`npm config ls -l\` to show all defaults. ` diff --git a/test/fixtures/sandbox.js b/test/fixtures/sandbox.js new file mode 100644 index 0000000000000..45af373d2753a --- /dev/null +++ b/test/fixtures/sandbox.js @@ -0,0 +1,361 @@ +const { createHook, executionAsyncId } = require('async_hooks') +const { EventEmitter } = require('events') +const { homedir, tmpdir } = require('os') +const { dirname, join } = require('path') +const { promisify } = require('util') +const mkdirp = require('mkdirp-infer-owner') +const npmlog = require('npmlog') +const rimraf = promisify(require('rimraf')) +const t = require('tap') + +let active = null +const chain = new Map() +const sandboxes = new Map() + +// keep a reference to the real process +const _process = process + +const processHook = createHook({ + init: (asyncId, type, triggerAsyncId, resource) => { + // track parentage of asyncIds + chain.set(asyncId, triggerAsyncId) + }, + before: (asyncId) => { + // find the nearest parent id that has a sandbox + let parent = asyncId + while (chain.has(parent) && !sandboxes.has(parent)) { + parent = chain.get(parent) + } + + process = sandboxes.has(parent) + ? sandboxes.get(parent) + : _process + }, +}).enable() + +for (const level in npmlog.levels) { + npmlog[`_${level}`] = npmlog[level] + npmlog[level] = (...args) => { + process._logs = process._logs || {} + process._logs[level] = process._logs[level] || [] + process._logs[level].push(args) + const _level = npmlog.level + npmlog.level = 'silent' + npmlog[`_${level}`](...args) + npmlog.level = _level + } +} + +const _data = Symbol('sandbox.data') +const _dirs = Symbol('sandbox.dirs') +const _test = Symbol('sandbox.test') +const _mocks = Symbol('sandbox.mocks') +const _npm = Symbol('sandbox.npm') +const _parent = Symbol('sandbox.parent') +const _output = Symbol('sandbox.output') +const _proxy = Symbol('sandbox.proxy') +const _get = Symbol('sandbox.proxy.get') +const _set = Symbol('sandbox.proxy.set') + +// these config keys can be redacted widely +const redactedDefaults = [ + 'node-version', + 'npm-version', + 'tmp', +] + +// we can't just replace these values everywhere because they're known to be +// very short strings that could be present all over the place, so we only +// replace them if they're located within quotes for now +const vagueRedactedDefaults = [ + 'editor', + 'shell', +] + +const normalize = (str) => str + .replace(/\r\n/g, '\n') // normalize line endings (for ini) + .replace(/[A-z]:\\/g, '\\') // turn windows roots to posix ones + .replace(/\\+/g, '/') // replace \ with / + +class Sandbox extends EventEmitter { + constructor (test, options = {}) { + super() + + this[_test] = test + this[_mocks] = options.mocks || {} + this[_data] = new Map() + this[_output] = [] + const tempDir = `${test.testdirName}-sandbox` + this[_dirs] = { + temp: tempDir, + global: options.global || join(tempDir, 'global'), + home: options.home || join(tempDir, 'home'), + project: options.project || join(tempDir, 'project'), + } + + this[_proxy] = new Proxy(_process, { + get: this[_get].bind(this), + set: this[_set].bind(this), + }) + this[_proxy].env = {} + this[_proxy].argv = [] + + test.cleanSnapshot = this.cleanSnapshot.bind(this) + test.afterEach(() => this.reset()) + test.teardown(() => this.teardown()) + } + + get config () { + return this[_npm] && this[_npm].config + } + + get logs () { + return this[_proxy]._logs + } + + get global () { + return this[_dirs].global + } + + get home () { + return this[_dirs].home + } + + get project () { + return this[_dirs].project + } + + get process () { + return this[_proxy] + } + + get output () { + return this[_output].map((line) => line.join(' ')).join('\n') + } + + cleanSnapshot (snapshot) { + let clean = normalize(snapshot) + + if (this[_npm]) { + // replace default config values with placeholders + for (const name of redactedDefaults) { + let value = this[_npm].config.defaults[name] + clean = clean.split(value).join(`{${name.toUpperCase()}}`) + } + + // replace vague default config values that are present within quotes + // with placeholders + for (const name of vagueRedactedDefaults) { + const value = this[_npm].config.defaults[name] + clean = clean.split(`"${value}"`).join(`"{${name.toUpperCase()}}"`) + } + } + + const viewer = _process.platform === 'win32' + ? /"browser"([^:]+|$)/g + : /"man"([^:]+|$)/g + + // the global prefix is platform dependent + const realGlobalPrefix = _process.platform === 'win32' + ? dirname(_process.execPath) + : dirname(dirname(_process.execPath)) + + const cache = _process.platform === 'win32' + ? /\{HOME\}\/npm-cache(\r?\n|"|\/|$)/g + : /\{HOME\}\/\.npm(\n|"|\/|$)/g + + // and finally replace some paths we know could be present + clean = clean + .replace(viewer, '"{VIEWER}"$1') + .split(normalize(this[_proxy].execPath)).join('{EXECPATH}') + .split(normalize(_process.execPath)).join('{REALEXECPATH}') + .split(normalize(this.global)).join('{GLOBALPREFIX}') + .split(normalize(realGlobalPrefix)).join('{REALGLOBALREFIX}') + .split(normalize(this.project)).join('{LOCALPREFIX}') + .split(normalize(this.home)).join('{HOME}') + .replace(cache, '{CACHE}$1') + .split(normalize(dirname(dirname(__dirname)))).join('{NPMDIR}') + .split(normalize(tmpdir())).join('{TMP}') + .split(normalize(homedir())).join('{REALHOME}') + .split(this[_proxy].platform).join('{PLATFORM}') + .split(this[_proxy].arch).join('{ARCH}') + + return clean + } + + // test.afterEach hook + reset () { + this.removeAllListeners() + this[_parent] = undefined + this[_output] = [] + this[_data].clear() + this[_proxy].env = {} + this[_proxy].argv = [] + this[_npm] = undefined + } + + // test.teardown hook + teardown () { + if (this[_parent]) { + sandboxes.delete(this[_parent]) + } + return rimraf(this[_dirs].temp).catch(() => null) + } + + // proxy get handler + [_get] (target, prop, receiver) { + if (this[_data].has(prop)) { + return this[_data].get(prop) + } + + if (this[prop] !== undefined) { + return Reflect.get(this, prop, this) + } + + const actual = Reflect.get(target, prop, receiver) + if (typeof actual === 'function') { + // in node 10.1 there's an interesting bug where if a function on process + // is called without explicitly forcing the 'this' arg to something, we + // get 'Illegal invocation' errors. wrapping function properties in their + // own proxy so that we can make sure the context is right fixes it + return new Proxy(actual, { + apply: (target, context, args) => { + return Reflect.apply(target, _process, args) + }, + }) + } + + return actual + } + + // proxy set handler + [_set] (target, prop, value) { + if (prop === 'env') { + value = { + ...value, + HOME: this.home, + } + } + + if (prop === 'argv') { + value = [ + process.execPath, + join(dirname(process.execPath), 'npm'), + ...value, + ] + } + + return this[_data].set(prop, value) + } + + async run (command, argv = []) { + await Promise.all([ + mkdirp(this.project), + mkdirp(this.home), + mkdirp(this.global), + ]) + + // attach the sandbox process now, doing it after the promise above is + // necessary to make sure that only async calls spawned as part of this + // call to run will receive the sandbox. if we attach it too early, we + // end up interfering with tap + this[_parent] = executionAsyncId() + this[_data].set('_asyncId', this[_parent]) + sandboxes.set(this[_parent], this[_proxy]) + process = this[_proxy] + + this[_proxy].argv = [ + '--prefix', this.project, + '--userconfig', join(this.home, '.npmrc'), + '--globalconfig', join(this.global, 'npmrc'), + command, + ...argv, + ] + + this[_npm] = this[_test].mock('../../lib/npm.js', this[_mocks]) + this[_npm].output = (...args) => this[_output].push(args) + await this[_npm].load() + // in some node versions (later 10.x) our executionAsyncId at this point + // will for some reason appear to have been triggered by a different parent + // so immediately after load, if we can see that we lost our ancestry, we + // fix it here with a hammer + if (chain.get(executionAsyncId()) !== this[_parent]) { + chain.set(executionAsyncId(), this[_parent]) + process = this[_proxy] + } + + const cmd = this[_npm].argv.shift() + const impl = this[_npm].commands[cmd] + if (!impl) { + throw new Error(`Unknown command: ${cmd}`) + } + + return new Promise((resolve, reject) => { + impl(this[_npm].argv, (err) => { + if (err) { + return reject(err) + } + + return resolve() + }) + }) + } + + async complete (command, argv, partial) { + if (!Array.isArray(argv)) { + partial = argv + argv = [] + } + + await Promise.all([ + mkdirp(this.project), + mkdirp(this.home), + mkdirp(this.global), + ]) + + // attach the sandbox process now, doing it after the promise above is + // necessary to make sure that only async calls spawned as part of this + // call to run will receive the sandbox. if we attach it too early, we + // end up interfering with tap + this[_parent] = executionAsyncId() + this[_data].set('_asyncId', this[_parent]) + sandboxes.set(this[_parent], this[_proxy]) + process = this[_proxy] + + this[_proxy].argv = [ + '--prefix', this.project, + '--userconfig', join(this.home, '.npmrc'), + '--globalconfig', join(this.global, 'npmrc'), + command, + ...argv, + ] + + this[_npm] = this[_test].mock('../../lib/npm.js', this[_mocks]) + this[_npm].output = (...args) => this[_output].push(args) + await this[_npm].load() + // in some node versions (later 10.x) our executionAsyncId at this point + // will for some reason appear to have been triggered by a different parent + // so immediately after load, if we can see that we lost our ancestry, we + // fix it here with a hammer + if (chain.get(executionAsyncId()) !== this[_parent]) { + chain.set(executionAsyncId(), this[_parent]) + process = this[_proxy] + } + + const impl = this[_npm].commands[command] + if (!impl) { + throw new Error(`Unknown command: ${cmd}`) + } + + return impl.completion({ + partialWord: partial, + conf: { + argv: { + remain: ['npm', command, ...argv], + }, + }, + }) + } +} + +module.exports = Sandbox diff --git a/test/lib/config.js b/test/lib/config.js index 8a1e7d85e09aa..ba47fa11d0bbc 100644 --- a/test/lib/config.js +++ b/test/lib/config.js @@ -1,659 +1,379 @@ +const { join } = require('path') +const { promisify } = require('util') +const fs = require('fs') +const spawk = require('spawk') const t = require('tap') -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 configDefs = require('../../lib/utils/config') -const definitions = Object.entries(configDefs.definitions) - .filter(([key, def]) => { - return [ - 'init-author-name', - 'init.author.name', - 'init-version', - 'init.version', - ].includes(key) - }).reduce((defs, [key, def]) => { - defs[key] = def - return defs - }, {}) - -const defaults = { - 'init-author-name': '', - 'init-version': '1.0.0', - 'init.author.name': '', - 'init.version': '1.0.0', -} - -const cliConfig = { - editor: 'vi', - json: false, - location: 'user', - long: false, - cat: true, - chai: true, - dog: true, -} - -const npm = { - log: { - warn: () => null, - 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: cliConfig, source: 'command line options' }, - })), - get (key) { - return cliConfig[key] - }, - validate () { - return true - }, - }, - output: msg => { - result = msg - }, -} - -const usageUtil = () => 'usage instructions' - -const mocks = { - '../../lib/utils/config/index.js': { defaults, definitions }, - '../../lib/utils/usage.js': usageUtil, -} - -const Config = t.mock('../../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() - }) -}) +spawk.preventUnmatched() -t.test('config ignores workspaces', t => { - npm.log.warn = (title, msg) => { - t.equal(title, 'config', 'should warn with expected title') - t.equal( - msg, - 'This command does not support workspaces.', - 'should warn with unsupported option msg' - ) - } - config.execWorkspaces([], [], (err) => { - t.match(err, /usage instructions/, 'should not error out when workspaces are defined') - npm.log.warn = () => null - t.end() - }) +const readFile = promisify(fs.readFile) + +const Sandbox = require('../fixtures/sandbox.js') + +t.test('config no args', async (t) => { + const sandbox = new Sandbox(t) + + await t.rejects(sandbox.run('config', []), { + code: 'EUSAGE', + }, 'rejects with usage') }) -t.test('config list', t => { - t.plan(2) +t.test('config ignores workspaces', async (t) => { + const sandbox = new Sandbox(t) - npm.config.find = () => 'cli' - result = '' - t.teardown(() => { - result = '' - delete npm.config.find - }) + await t.rejects(sandbox.run('config', ['--workspaces']), { + code: 'EUSAGE', + }, 'rejects with usage') - config.exec(['list'], (err) => { - t.error(err, 'npm config list') - t.matchSnapshot(result, 'should list configs') - }) + t.match(sandbox.logs.warn, [['config', 'This command does not support workspaces.']], 'logged the warning') }) -t.test('config list overrides', t => { - t.plan(2) +t.test('config list', async (t) => { + const sandbox = new Sandbox(t) - npm.config.data.set('user', { - data: { - 'init.author.name': 'Foo', - '//private-reg.npmjs.org/:_authThoken': 'f00ba1', + const temp = t.testdir({ + global: { + npmrc: 'globalloaded=yes', + }, + project: { + '.npmrc': 'projectloaded=yes', + }, + home: { + '.npmrc': 'userloaded=yes', }, - source: '~/.npmrc', - }) - cliConfig['init.author.name'] = 'Bar' - npm.config.find = () => 'cli' - result = '' - t.teardown(() => { - result = '' - npm.config.data.delete('user') - delete cliConfig['init.author.name'] - delete npm.config.find }) + const global = join(temp, 'global') + const project = join(temp, 'project') + const home = join(temp, 'home') - config.exec(['list'], (err) => { - t.error(err, 'npm config list') - t.matchSnapshot(result, 'should list overridden configs') - }) -}) + await sandbox.run('config', ['list'], { global, project, home }) -t.test('config list --long', t => { - t.plan(2) + t.matchSnapshot(sandbox.output, 'output matches snapshot') +}) - npm.config.find = key => key in cliConfig ? 'cli' : 'default' - cliConfig.long = true - result = '' - t.teardown(() => { - delete npm.config.find - cliConfig.long = false - result = '' +t.test('config list --long', async (t) => { + const temp = t.testdir({ + global: { + npmrc: 'globalloaded=yes', + }, + project: { + '.npmrc': 'projectloaded=yes', + }, + home: { + '.npmrc': 'userloaded=yes', + }, }) + const global = join(temp, 'global') + const project = join(temp, 'project') + const home = join(temp, 'home') - config.exec(['list'], (err) => { - t.error(err, 'npm config list --long') - t.matchSnapshot(result, 'should list all configs') - }) + const sandbox = new Sandbox(t, { global, project, home }) + await sandbox.run('config', ['list', '--long']) + + t.matchSnapshot(sandbox.output, 'output matches snapshot') }) -t.test('config list --json', t => { - t.plan(2) +t.test('config list --json', async (t) => { + const temp = t.testdir({ + global: { + npmrc: 'globalloaded=yes', + }, + project: { + '.npmrc': 'projectloaded=yes', + }, + home: { + '.npmrc': 'userloaded=yes', + }, + }) + const global = join(temp, 'global') + const project = join(temp, 'project') + const home = join(temp, 'home') - cliConfig.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] + const sandbox = new Sandbox(t, { global, project, home }) + await sandbox.run('config', ['list', '--json']) - t.teardown(() => { - delete npm.config.list - cliConfig.json = false - npm.config.get = npmConfigGet - result = '' - }) + t.matchSnapshot(sandbox.output, 'output matches snapshot') +}) - config.exec(['list'], (err) => { - t.error(err, 'npm config list --json') - t.same( - JSON.parse(result), - { - editor: 'vi', - json: true, - location: 'user', - long: false, - cat: true, - chai: true, - dog: true, - }, - 'should list configs usin json' - ) - }) +t.test('config delete no args', async (t) => { + const sandbox = new Sandbox(t) + + await t.rejects(sandbox.run('config', ['delete']), { + code: 'EUSAGE', + }, 'rejects with usage') }) -t.test('config delete no args', t => { - config.exec(['delete'], (err) => { - t.match(err, { message: '\nUsage: usage instructions' }) - t.end() +t.test('config delete single key', async (t) => { + // location defaults to user, so we work with a userconfig + const home = t.testdir({ + '.npmrc': 'foo=bar\nbar=baz', }) -}) -t.test('config delete key', t => { - t.plan(4) + const sandbox = new Sandbox(t) + await sandbox.run('config', ['delete', 'foo'], { home }) - 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') - } + t.equal(sandbox.config.get('foo'), undefined, 'foo should no longer be set') - npm.config.save = where => { - t.equal(where, 'user', 'should save user config post-delete') - } + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.not(contents.includes('foo='), 'foo was removed on disk') +}) - config.exec(['delete', 'foo'], (err) => { - t.error(err, 'npm config delete key') +t.test('config delete multiple keys', async (t) => { + const home = t.testdir({ + '.npmrc': 'foo=bar\nbar=baz\nbaz=buz', }) - t.teardown(() => { - delete npm.config.delete - delete npm.config.save - }) -}) + const sandbox = new Sandbox(t) + await sandbox.run('config', ['delete', 'foo', 'bar'], { home }) -t.test('config delete multiple key', t => { - t.plan(6) + t.equal(sandbox.config.get('foo'), undefined, 'foo should no longer be set') + t.equal(sandbox.config.get('bar'), undefined, 'bar should no longer be set') - const expect = [ - 'foo', - 'bar', - ] + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.not(contents.includes('foo='), 'foo was removed on disk') + t.not(contents.includes('bar='), 'bar was removed on disk') +}) - 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') - } +t.test('config delete key --location=global', async (t) => { + const global = t.testdir({ + npmrc: 'foo=bar\nbar=baz', + }) - npm.config.save = where => { - t.equal(where, 'user', 'should save user config post-delete') - } + const sandbox = new Sandbox(t) + await sandbox.run('config', ['delete', 'foo', '--location=global'], { global }) - config.exec(['delete', 'foo', 'bar'], (err) => { - t.error(err, 'npm config delete keys') - }) + t.equal(sandbox.config.get('foo', 'global'), undefined, 'foo should no longer be set') - t.teardown(() => { - delete npm.config.delete - delete npm.config.save - }) + const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' }) + t.not(contents.includes('foo='), 'foo was removed on disk') }) -t.test('config delete key --location=global', t => { - t.plan(4) +t.test('config delete key --global', async (t) => { + const global = t.testdir({ + npmrc: 'foo=bar\nbar=baz', + }) + + const sandbox = new Sandbox(t) + await sandbox.run('config', ['delete', 'foo', '--global'], { global }) - 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') - } + t.equal(sandbox.config.get('foo', 'global'), undefined, 'foo should no longer be set') - npm.config.save = where => { - t.equal(where, 'global', 'should save global config post-delete') - } + const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' }) + t.not(contents.includes('foo='), 'foo was removed on disk') +}) - cliConfig.location = 'global' - config.exec(['delete', 'foo'], (err) => { - t.error(err, 'npm config delete key --location=global') - }) +t.test('config set no args', async (t) => { + const sandbox = new Sandbox(t) - t.teardown(() => { - cliConfig.location = 'user' - delete npm.config.delete - delete npm.config.save - }) + await t.rejects(sandbox.run('config', ['set']), { + code: 'EUSAGE', + }, 'rejects with usage') }) -t.test('config set no args', t => { - config.exec(['set'], (err) => { - t.match(err, { message: '\nUsage: usage instructions' }) - t.end() +t.test('config set key', async (t) => { + const home = t.testdir({ + '.npmrc': 'foo=bar', }) -}) -t.test('config set key', t => { - t.plan(5) + const sandbox = new Sandbox(t, { home }) - 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') - } + await sandbox.run('config', ['set', 'foo']) - npm.config.save = where => { - t.equal(where, 'user', 'should save user config') - } + t.equal(sandbox.config.get('foo'), '', 'set the value for foo') - config.exec(['set', 'foo', 'bar'], (err) => { - t.error(err, 'npm config set key') - }) + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.ok(contents.includes('foo='), 'wrote foo to disk') +}) - t.teardown(() => { - delete npm.config.set - delete npm.config.save +t.test('config set key value', async (t) => { + const home = t.testdir({ + '.npmrc': 'foo=bar', }) -}) -t.test('config set key=val', t => { - t.plan(5) + const sandbox = new Sandbox(t, { home }) - 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') - } + await sandbox.run('config', ['set', 'foo', 'baz']) - npm.config.save = where => { - t.equal(where, 'user', 'should save user config') - } + t.equal(sandbox.config.get('foo'), 'baz', 'set the value for foo') - config.exec(['set', 'foo=bar'], (err) => { - t.error(err, 'npm config set key') - }) + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.ok(contents.includes('foo=baz'), 'wrote foo to disk') +}) - t.teardown(() => { - delete npm.config.set - delete npm.config.save +t.test('config set key=value', async (t) => { + const home = t.testdir({ + '.npmrc': 'foo=bar', }) -}) -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') - } + const sandbox = new Sandbox(t, { home }) - npm.config.save = where => { - t.equal(where, 'user', 'should save user config') - } + await sandbox.run('config', ['set', 'foo=baz']) - config.exec(['set', ...args], (err) => { - t.error(err, 'npm config set key') - }) + t.equal(sandbox.config.get('foo'), 'baz', 'set the value for foo') - t.teardown(() => { - delete npm.config.set - delete npm.config.save - }) + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.ok(contents.includes('foo=baz'), 'wrote foo to disk') }) -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') - } +t.test('config set key1 value1 key2=value2 key3', async (t) => { + const home = t.testdir({ + '.npmrc': 'foo=bar\nbar=baz\nbaz=foo', + }) - npm.config.save = where => { - t.equal(where, 'user', 'should save user config') - } + const sandbox = new Sandbox(t, { home }) + await sandbox.run('config', ['set', 'foo', 'oof', 'bar=rab', 'baz']) - config.exec(['set', 'foo'], (err) => { - t.error(err, 'npm config set key to empty value') - }) + t.equal(sandbox.config.get('foo'), 'oof', 'foo was set') + t.equal(sandbox.config.get('bar'), 'rab', 'bar was set') + t.equal(sandbox.config.get('baz'), '', 'baz was set') - t.teardown(() => { - delete npm.config.set - delete npm.config.save - }) + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.ok(contents.includes('foo=oof'), 'foo was written to disk') + t.ok(contents.includes('bar=rab'), 'bar was written to disk') + t.ok(contents.includes('baz='), 'baz was written to disk') }) -t.test('config set invalid key', t => { - t.plan(3) +t.test('config set invalid key logs warning', async (t) => { + const sandbox = new Sandbox(t) - 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 - npm.log.warn = () => null - }) - - config.exec(['set', 'foo', 'bar'], (err) => { - t.error(err, 'npm config set invalid key') - }) + // this doesn't reject, it only logs a warning + await sandbox.run('config', ['set', 'access=foo']) + t.match(sandbox.logs.warn, [ + ['invalid config', 'access="foo"', `set in ${join(sandbox.home, '.npmrc')}`], + ], 'logged warning') }) -t.test('config set key --location=global', t => { - t.plan(5) +t.test('config set key=value --location=global', async (t) => { + const global = t.testdir({ + npmrc: 'foo=bar\nbar=baz', + }) - 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') - } + const sandbox = new Sandbox(t, { global }) + await sandbox.run('config', ['set', 'foo=buzz', '--location=global']) - npm.config.save = where => { - t.equal(where, 'global', 'should save global config') - } + t.equal(sandbox.config.get('foo', 'global'), 'buzz', 'foo should be set') - cliConfig.location = 'global' - config.exec(['set', 'foo', 'bar'], (err) => { - t.error(err, 'npm config set key --location=global') - }) + const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' }) + t.not(contents.includes('foo=buzz'), 'foo was saved on disk') +}) - t.teardown(() => { - cliConfig.location = 'user' - delete npm.config.set - delete npm.config.save +t.test('config set key=value --global', async (t) => { + const global = t.testdir({ + npmrc: 'foo=bar\nbar=baz', }) -}) -t.test('config get no args', t => { - t.plan(2) + const sandbox = new Sandbox(t, { global }) + await sandbox.run('config', ['set', 'foo=buzz', '--global']) - npm.config.find = () => 'cli' - result = '' - t.teardown(() => { - result = '' - delete npm.config.find - }) + t.equal(sandbox.config.get('foo', 'global'), 'buzz', 'foo should be set') - config.exec(['get'], (err) => { - t.error(err, 'npm config get no args') - t.matchSnapshot(result, 'should list configs on config get no args') - }) + const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' }) + t.not(contents.includes('foo=buzz'), 'foo was saved on disk') }) -t.test('config get key', t => { - t.plan(2) +t.test('config get no args', async (t) => { + const sandbox = new Sandbox(t) - const npmConfigGet = npm.config.get - npm.config.get = (key) => { - t.equal(key, 'foo', 'should use expected key') - return 'bar' - } + await sandbox.run('config', ['get']) + const getOutput = sandbox.output - npm.config.save = where => { - throw new Error('should not save') - } + sandbox.reset() - config.exec(['get', 'foo'], (err) => { - t.error(err, 'npm config get key') - }) + await sandbox.run('config', ['list']) + const listOutput = sandbox.output - t.teardown(() => { - npm.config.get = npmConfigGet - delete npm.config.save - }) + t.equal(listOutput, getOutput, 'get with no args outputs list') }) -t.test('config get multiple keys', t => { - t.plan(4) - - const expect = [ - 'foo', - 'bar', - ] +t.test('config get single key', async (t) => { + const sandbox = new Sandbox(t) - 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') - } + await sandbox.run('config', ['get', 'node-version']) + t.equal(sandbox.output, sandbox.config.get('node-version'), 'should get the value') +}) - config.exec(['get', 'foo', 'bar'], (err) => { - t.error(err, 'npm config get multiple keys') - t.equal(result, 'foo=asdf\nbar=asdf') - }) +t.test('config get multiple keys', async (t) => { + const sandbox = new Sandbox(t) - t.teardown(() => { - result = '' - npm.config.get = npmConfigGet - delete npm.config.save - }) + await sandbox.run('config', ['get', 'node-version', 'npm-version']) + t.ok(sandbox.output.includes(`node-version=${sandbox.config.get('node-version')}`), 'outputs node-version') + t.ok(sandbox.output.includes(`npm-version=${sandbox.config.get('npm-version')}`), 'outputs npm-version') }) -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 get private key', async (t) => { + const sandbox = new Sandbox(t) + + await t.rejects(sandbox.run('config', ['get', '_authToken']), '_authToken is protected', 'rejects with protected string') }) -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 = t.mock('../../lib/config.js', editMocks) - const config = new Config(npm) - - config.exec(['edit'], (err) => { - t.error(err, 'npm config edit') - - // test no config file result - editMocks.fs.readFile = (p, e, cb) => { - cb(new Error('ERR')) - } - const Config = t.mock('../../lib/config.js', editMocks) - const config = new Config(npm) - config.exec(['edit'], (err) => { - t.error(err, 'npm config edit') - }) +t.test('config edit', async (t) => { + const home = t.testdir({ + '.npmrc': 'foo=bar\nbar=baz', }) t.teardown(() => { - npm.config.data.delete('user') - delete npm.config.save + spawk.clean() }) -}) -t.test('config edit --location=global', t => { - t.plan(6) + const EDITOR = 'vim' + const editor = spawk.spawn(EDITOR).exit(0) - cliConfig.location = 'global' - 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 = t.mock('../../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') - }) + const sandbox = new Sandbox(t, { home }) + sandbox.process.env.EDITOR = EDITOR + await sandbox.run('config', ['edit']) + + t.ok(editor.called, 'editor was spawned') + t.same(editor.calledWith.args, [join(sandbox.home, '.npmrc')], 'editor opened the user config file') + + const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' }) + t.ok(contents.includes('foo=bar'), 'kept foo') + t.ok(contents.includes('bar=baz'), 'kept bar') + t.ok(contents.includes('shown below with default values'), 'appends defaults to file') +}) +t.test('config edit - editor exits non-0', async (t) => { t.teardown(() => { - cliConfig.location = 'user' - npm.config.data.delete('user') - delete npm.config.save + spawk.clean() }) -}) -t.test('completion', t => { - const { completion } = config + const EDITOR = 'vim' + const editor = spawk.spawn(EDITOR).exit(1) - const testComp = (argv, expect) => { - t.resolveMatch(completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) - } + const sandbox = new Sandbox(t) + sandbox.process.env.EDITOR = EDITOR + await t.rejects(sandbox.run('config', ['edit']), { + message: 'editor process exited with code: 1', + }, 'rejects with error about editor code') - testComp(['npm', 'foo'], []) - testComp(['npm', 'config'], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'list']) - testComp(['npm', 'config', 'set', 'foo'], []) + t.ok(editor.called, 'editor was spawned') + t.same(editor.calledWith.args, [join(sandbox.home, '.npmrc')], 'editor opened the user config file') +}) - const possibleConfigKeys = [...Object.keys(definitions)] - 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'], []) +t.test('completion', async (t) => { + const sandbox = new Sandbox(t) - const partial = completion({conf: { argv: { remain: ['npm', 'config'] } }, partialWord: 'l'}) - t.resolveMatch(partial, ['get', 'set', 'delete', 'ls', 'rm', 'edit'], 'npm config') + let allKeys + const testComp = async (argv, expect) => { + t.match(await sandbox.complete('config', argv), expect, argv.join(' ')) + if (!allKeys) + allKeys = Object.keys(sandbox.config.definitions) + sandbox.reset() + } - t.end() + await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'list']) + await testComp(['set', 'foo'], []) + await testComp(['get'], allKeys) + await testComp(['set'], allKeys) + await testComp(['delete'], allKeys) + await testComp(['rm'], allKeys) + await testComp(['edit'], []) + await testComp(['list'], []) + await testComp(['ls'], []) + + const getCommand = await sandbox.complete('get') + t.match(getCommand, allKeys, 'also works for just npm get') + sandbox.reset() + + const partial = await sandbox.complete('config', 'l') + t.match(partial, ['get', 'set', 'delete', 'ls', 'rm', 'edit'], 'and works on partials') }) diff --git a/test/lib/utils/config/definitions.js b/test/lib/utils/config/definitions.js index 63d9bbd195ab2..65193020d050c 100644 --- a/test/lib/utils/config/definitions.js +++ b/test/lib/utils/config/definitions.js @@ -812,19 +812,44 @@ t.test('location', t => { location: 'user', } const flat = {} + // the global flattener is what sets location, so run that + definitions.global.flatten('global', obj, flat) definitions.location.flatten('location', obj, flat) // global = true sets location in both places to global - t.strictSame(flat, { location: 'global' }) - t.strictSame(obj, { global: true, location: 'global' }) + t.strictSame(flat, { global: true, location: 'global' }) + // location here is still 'user' because flattening doesn't modify the object + t.strictSame(obj, { global: true, location: 'user' }) obj.global = false obj.location = 'user' delete flat.global delete flat.location + definitions.global.flatten('global', obj, flat) definitions.location.flatten('location', obj, flat) // global = false leaves location unaltered - t.strictSame(flat, { location: 'user' }) + t.strictSame(flat, { global: false, location: 'user' }) t.strictSame(obj, { global: false, location: 'user' }) t.end() }) + +t.test('package-lock-only', t => { + const obj = { + 'package-lock': false, + 'package-lock-only': true, + } + const flat = {} + + definitions['package-lock-only'].flatten('package-lock-only', obj, flat) + definitions['package-lock'].flatten('package-lock', obj, flat) + t.strictSame(flat, { packageLock: true, packageLockOnly: true }) + + obj['package-lock-only'] = false + delete flat.packageLock + delete flat.packageLockOnly + + definitions['package-lock-only'].flatten('package-lock-only', obj, flat) + definitions['package-lock'].flatten('package-lock', obj, flat) + t.strictSame(flat, { packageLock: false, packageLockOnly: false }) + t.end() +})