From b9c1caa8e4cc7c900d09657425ea361db5974319 Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Fri, 23 Oct 2020 12:41:17 -0400 Subject: [PATCH] fix: npm owner refactor - Fixed npm owner completion - Refactored lib/owner.js - Added error codes to lib/owner.js errors - Added tests for lib/owner.js PR-URL: https://github.com/npm/cli/pull/2026 Credit: @ruyadorno Close: #2026 Reviewed-by: @isaacs --- lib/owner.js | 384 +++++----- tap-snapshots/test-lib-owner.js-TAP.test.js | 20 + test/lib/owner.js | 790 ++++++++++++++++++++ 3 files changed, 1004 insertions(+), 190 deletions(-) create mode 100644 tap-snapshots/test-lib-owner.js-TAP.test.js create mode 100644 test/lib/owner.js diff --git a/lib/owner.js b/lib/owner.js index 4b306ac6aeb7a..536c4c54c9724 100644 --- a/lib/owner.js +++ b/lib/owner.js @@ -1,241 +1,245 @@ -module.exports = owner +'use strict' const log = require('npmlog') const npa = require('npm-package-arg') -const npm = require('./npm.js') const npmFetch = require('npm-registry-fetch') +const pacote = require('pacote') + +const npm = require('./npm.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') -const { packument } = require('pacote') const readLocalPkg = require('./utils/read-local-package.js') -const usage = require('./utils/usage') -const getIdentity = require('./utils/get-identity') +const usageUtil = require('./utils/usage.js') -owner.usage = usage( +const usage = usageUtil( 'owner', 'npm owner add [<@scope>/]' + '\nnpm owner rm [<@scope>/]' + '\nnpm owner ls [<@scope>/]' ) -owner.completion = function (opts, cb) { +const completion = function (opts, cb) { const argv = opts.conf.argv.remain - if (argv.length > 4) return cb() - if (argv.length <= 2) { - var subs = ['add', 'rm'] - if (opts.partialWord === 'l') subs.push('ls') - else subs.push('ls', 'list') + if (argv.length > 3) { + return cb(null, []) + } + if (argv[1] !== 'owner') { + argv.unshift('owner') + } + if (argv.length === 2) { + var subs = ['add', 'rm', 'ls'] return cb(null, subs) } - Promise.resolve().then(() => { - const opts = npm.flatOptions - return getIdentity(opts).then(username => { - const un = encodeURIComponent(username) - let byUser, theUser - switch (argv[2]) { - case 'ls': - // FIXME: there used to be registry completion here, but it stopped - // making sense somewhere around 50,000 packages on the registry - return - case 'rm': - if (argv.length > 3) { - theUser = encodeURIComponent(argv[3]) - byUser = `/-/by-user/${theUser}|${un}` - return npmFetch.json(byUser, opts).then(d => { - return d[theUser].filter( - // kludge for server adminery. - p => un === 'isaacs' || d[un].indexOf(p) === -1 - ) - }) - } - // else fallthrough - /* eslint no-fallthrough:0 */ - case 'add': - if (argv.length > 3) { - theUser = encodeURIComponent(argv[3]) - byUser = `/-/by-user/${theUser}|${un}` - return npmFetch.json(byUser, opts).then(d => { - var mine = d[un] || [] - var theirs = d[theUser] || [] - return mine.filter(p => theirs.indexOf(p) === -1) - }) - } else { - // just list all users who aren't me. - return npmFetch.json('/-/users', opts).then(list => { - return Object.keys(list).filter(n => n !== un) - }) - } - - default: - return cb() - } - }) - }).then(() => cb(), cb) -} -function UsageError () { - throw Object.assign(new Error(owner.usage), { code: 'EUSAGE' }) + // reaches registry in order to autocomplete rm + if (argv[2] === 'rm') { + const opts = { + ...npm.flatOptions, + fullMetadata: true + } + readLocalPkg() + .then(pkgName => { + if (!pkgName) { + return null + } + const spec = npa(pkgName) + return pacote.packument(spec, opts) + }) + .then(data => { + if (data && data.maintainers && data.maintainers.length) { + return data.maintainers.map(m => m.name) + } + return [] + }) + .then(owners => { + return cb(null, owners) + }) + } else { + cb(null, []) + } } -function owner ([action, ...args], cb) { +const UsageError = () => + Object.assign(new Error(usage), { code: 'EUSAGE' }) + +const cmd = (args, cb) => owner(args).then(() => cb()).catch(cb) + +const owner = async ([action, ...args]) => { const opts = npm.flatOptions - Promise.resolve().then(() => { - switch (action) { - case 'ls': case 'list': return ls(args[0], opts) - case 'add': return add(args[0], args[1], opts) - case 'rm': case 'remove': return rm(args[0], args[1], opts) - default: UsageError() - } - }).then( - data => cb(null, data), - err => err.code === 'EUSAGE' ? cb(err.message) : cb(err) - ) + switch (action) { + case 'ls': + case 'list': + return ls(args[0], opts) + case 'add': + return add(args[0], args[1], opts) + case 'rm': + case 'remove': + return rm(args[0], args[1], opts) + default: + throw UsageError() + } } -function ls (pkg, opts) { +const ls = async (pkg, opts) => { if (!pkg) { - return readLocalPkg().then(pkg => { - if (!pkg) { UsageError() } - return ls(pkg, opts) - }) + const pkgName = await readLocalPkg() + if (!pkgName) { + throw UsageError() + } + pkg = pkgName } const spec = npa(pkg) - return packument(spec, { ...opts, fullMetadata: true }).then( - data => { - var owners = data.maintainers - if (!owners || !owners.length) { - output('admin party!') - } else { - output(owners.map(o => `${o.name} <${o.email}>`).join('\n')) - } - return owners - }, - err => { - log.error('owner ls', "Couldn't get owner data", pkg) - throw err + + try { + const packumentOpts = { ...opts, fullMetadata: true } + const { maintainers } = await pacote.packument(spec, packumentOpts) + if (!maintainers || !maintainers.length) { + output('no admin found') + } else { + output(maintainers.map(o => `${o.name} <${o.email}>`).join('\n')) } - ) + return maintainers + } catch (err) { + log.error('owner ls', "Couldn't get owner data", pkg) + throw err + } } -function add (user, pkg, opts) { - if (!user) { UsageError() } +const validateAddOwner = (newOwner, owners) => { + owners = owners || [] + for (const o of owners) { + if (o.name === newOwner.name) { + log.info( + 'owner add', + 'Already a package owner: ' + o.name + ' <' + o.email + '>' + ) + return false + } + } + return [ + ...owners, + newOwner + ] +} + +const add = async (user, pkg, opts) => { + if (!user) { + throw UsageError() + } if (!pkg) { - return readLocalPkg().then(pkg => { - if (!pkg) { UsageError() } - return add(user, pkg, opts) - }) + const pkgName = await readLocalPkg() + if (!pkgName) { + throw UsageError() + } + pkg = pkgName } log.verbose('owner add', '%s to %s', user, pkg) const spec = npa(pkg) - return withMutation(spec, user, opts, (u, owners) => { - if (!owners) owners = [] - for (var i = 0, l = owners.length; i < l; i++) { - var o = owners[i] - if (o.name === u.name) { - log.info( - 'owner add', - 'Already a package owner: ' + o.name + ' <' + o.email + '>' - ) - return false - } - } - owners.push(u) - return owners + return putOwners(spec, user, opts, validateAddOwner) +} + +const validateRmOwner = (rmOwner, owners) => { + let found = false + const m = owners.filter(function (o) { + var match = (o.name === rmOwner.name) + found = found || match + return !match }) + + if (!found) { + log.info('owner rm', 'Not a package owner: ' + rmOwner.name) + return false + } + + if (!m.length) { + throw Object.assign( + new Error( + 'Cannot remove all owners of a package. Add someone else first.' + ), + { code: 'EOWNERRM' } + ) + } + + return m } -function rm (user, pkg, opts) { - if (!user) { UsageError() } +const rm = async (user, pkg, opts) => { + if (!user) { + throw UsageError() + } if (!pkg) { - return readLocalPkg().then(pkg => { - if (!pkg) { UsageError() } - return rm(user, pkg, opts) - }) + const pkgName = await readLocalPkg() + if (!pkgName) { + throw UsageError() + } + pkg = pkgName } log.verbose('owner rm', '%s from %s', user, pkg) const spec = npa(pkg) - return withMutation(spec, user, opts, function (u, owners) { - let found = false - const m = owners.filter(function (o) { - var match = (o.name === user) - found = found || match - return !match - }) - - if (!found) { - log.info('owner rm', 'Not a package owner: ' + user) - return false - } - - if (!m.length) { - throw new Error( - 'Cannot remove all owners of a package. Add someone else first.' - ) - } - - return m - }) + return putOwners(spec, user, opts, validateRmOwner) } -function withMutation (spec, user, opts, mutation) { - return Promise.resolve().then(() => { - if (user) { - const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}` - return npmFetch.json(uri, opts).then(mutate_, err => { - log.error('owner mutate', 'Error getting user data for %s', user) - throw err - }) - } else { - return mutate_(null) - } - }) +const putOwners = async (spec, user, opts, validation) => { + const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}` + let u = '' - function mutate_ (u) { - if (user && (!u || u.error)) { - throw new Error( + try { + u = await npmFetch.json(uri, opts) + } catch (err) { + log.error('owner mutate', `Error getting user data for ${user}`) + throw err + } + + if (user && (!u || !u.name || u.error)) { + throw Object.assign( + new Error( "Couldn't get user data for " + user + ': ' + JSON.stringify(u) - ) - } + ), + { code: 'EOWNERUSER' } + ) + } + + // normalize user data + u = { name: u.name, email: u.email } + + const data = await pacote.packument(spec, { ...opts, fullMetadata: true }) + + // save the number of maintainers before validation for comparison + const before = data.maintainers ? data.maintainers.length : 0 - if (u) u = { name: u.name, email: u.email } - return packument(spec, { + const m = validation(u, data.maintainers) + if (!m) return // invalid owners + + const body = { + _id: data._id, + _rev: data._rev, + maintainers: m + } + const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}` + const res = await otplease(opts, opts => + npmFetch.json(dataPath, { ...opts, - fullMetadata: true - }).then(data => { - // save the number of maintainers before mutation so that we can figure - // out if maintainers were added or removed - const beforeMutation = data.maintainers.length - - const m = mutation(u, data.maintainers) - if (!m) return // handled - if (m instanceof Error) throw m // error - - data = { - _id: data._id, - _rev: data._rev, - maintainers: m - } - const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}` - return otplease(opts, opts => { - return npmFetch.json(dataPath, { - ...opts, - method: 'PUT', - body: data, - spec - }) - }).then(data => { - if (data.error) { - throw new Error('Failed to update package metadata: ' + JSON.stringify(data)) - } else if (m.length > beforeMutation) { - output('+ %s (%s)', user, spec.name) - } else if (m.length < beforeMutation) { - output('- %s (%s)', user, spec.name) - } - return data - }) - }) + method: 'PUT', + body, + spec + })) + + if (!res.error) { + if (m.length < before) { + output(`- ${user} (${spec.name})`) + } else { + output(`+ ${user} (${spec.name})`) + } + } else { + throw Object.assign( + new Error('Failed to update package: ' + JSON.stringify(res)), + { code: 'EOWNERMUTATE' } + ) } + return res } + +module.exports = Object.assign(cmd, { usage, completion }) diff --git a/tap-snapshots/test-lib-owner.js-TAP.test.js b/tap-snapshots/test-lib-owner.js-TAP.test.js new file mode 100644 index 0000000000000..2d92b0ae5ed6f --- /dev/null +++ b/tap-snapshots/test-lib-owner.js-TAP.test.js @@ -0,0 +1,20 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/owner.js TAP owner ls > should output owners of 1`] = ` +nlf +ruyadorno +darcyclarke +isaacs +` + +exports[`test/lib/owner.js TAP owner ls no args > should output owners of cwd package 1`] = ` +nlf +ruyadorno +darcyclarke +isaacs +` diff --git a/test/lib/owner.js b/test/lib/owner.js new file mode 100644 index 0000000000000..dc179e4662028 --- /dev/null +++ b/test/lib/owner.js @@ -0,0 +1,790 @@ +const requireInject = require('require-inject') +const t = require('tap') + +let result = '' +let readLocalPkgResponse = null + +const noop = () => null + +const npm = { flatOptions: {} } +const npmFetch = { json: noop } +const npmlog = { error: noop, info: noop, verbose: noop } +const pacote = { packument: noop } + +const mocks = { + npmlog, + 'npm-registry-fetch': npmFetch, + pacote, + '../../lib/npm.js': npm, + '../../lib/utils/output.js': (...msg) => { result += msg.join('\n') }, + '../../lib/utils/otplease.js': async (opts, fn) => fn({ otp: '123456', opts }), + '../../lib/utils/read-local-package.js': async () => readLocalPkgResponse, + '../../lib/utils/usage.js': () => 'usage instructions' +} + +const npmcliMaintainers = [ + { email: 'quitlahok@gmail.com', name: 'nlf' }, + { email: 'ruyadorno@hotmail.com', name: 'ruyadorno' }, + { email: 'darcy@darcyclarke.me', name: 'darcyclarke' }, + { email: 'i@izs.me', name: 'isaacs' } +] + +const owner = requireInject('../../lib/owner.js', mocks) + +t.test('owner no args', t => { + result = '' + t.teardown(() => { + result = '' + }) + + owner([], err => { + t.equal( + err.message, + 'usage instructions', + 'should throw usage instructions' + ) + t.end() + }) +}) + +t.test('owner ls no args', t => { + t.plan(4) + + result = '' + + readLocalPkgResponse = '@npmcli/map-workspaces' + pacote.packument = async (spec, opts) => { + t.equal(spec.name, '@npmcli/map-workspaces', 'should use expect pkg name') + t.match( + opts, + { + ...npm.flatOptions, + fullMetadata: true + }, + 'should forward expected options to pacote.packument' + ) + return { maintainers: npmcliMaintainers } + } + t.teardown(() => { + result = '' + pacote.packument = noop + readLocalPkgResponse = null + }) + + owner(['ls'], err => { + t.ifError(err, 'npm owner ls no args') + t.matchSnapshot(result, 'should output owners of cwd package') + }) +}) + +t.test('owner ls no args no cwd package', t => { + result = '' + t.teardown(() => { + result = '' + npmlog.error = noop + }) + + owner(['ls'], err => { + t.equal( + err.message, + 'usage instructions', + 'should throw usage instructions if no cwd package available' + ) + t.end() + }) +}) + +t.test('owner ls fails to retrieve packument', t => { + t.plan(4) + + result = '' + readLocalPkgResponse = '@npmcli/map-workspaces' + pacote.packument = () => { + throw new Error('ERR') + } + npmlog.error = (title, msg, pkgName) => { + t.equal(title, 'owner ls', 'should list npm owner ls title') + t.equal(msg, "Couldn't get owner data", 'should use expected msg') + t.equal(pkgName, '@npmcli/map-workspaces', 'should use pkg name') + } + t.teardown(() => { + result = '' + npmlog.error = noop + pacote.packument = noop + }) + + owner(['ls'], err => { + t.match( + err, + /ERR/, + 'should throw unkown error' + ) + }) +}) + +t.test('owner ls ', t => { + t.plan(4) + + result = '' + pacote.packument = async (spec, opts) => { + t.equal(spec.name, '@npmcli/map-workspaces', 'should use expect pkg name') + t.match( + opts, + { + ...npm.flatOptions, + fullMetadata: true + }, + 'should forward expected options to pacote.packument' + ) + return { maintainers: npmcliMaintainers } + } + t.teardown(() => { + result = '' + pacote.packument = noop + }) + + owner(['ls', '@npmcli/map-workspaces'], err => { + t.ifError(err, 'npm owner ls ') + t.matchSnapshot(result, 'should output owners of ') + }) +}) + +t.test('owner ls no maintainers', t => { + result = '' + pacote.packument = async (spec, opts) => { + return { maintainers: [] } + } + t.teardown(() => { + result = '' + pacote.packument = noop + }) + + owner(['ls', '@npmcli/map-workspaces'], err => { + t.ifError(err, 'npm owner ls no maintainers') + t.equal(result, 'no admin found', 'should output no admint found msg') + t.end() + }) +}) + +t.test('owner add ', t => { + t.plan(9) + + result = '' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + t.ok('should request user info') + t.match(opts, { ...npm.flatOptions }, 'should use expected opts') + return { + _id: 'org.couchdb.user:foo', + email: 'foo@github.com', + name: 'foo' + } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + t.ok('should put changed owner') + t.match(opts, { + ...npm.flatOptions, + method: 'PUT', + body: { + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + }, + otp: '123456', + spec: { + name: '@npmcli/map-workspaces' + } + }, 'should use expected opts') + t.deepEqual( + opts.body.maintainers, + [ + ...npmcliMaintainers, + { + name: 'foo', + email: 'foo@github.com' + } + ], + 'should contain expected new owners, adding requested user' + ) + return {} + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => { + t.equal(spec.name, '@npmcli/map-workspaces', 'should use expect pkg name') + t.match( + opts, + { + ...npm.flatOptions, + fullMetadata: true + }, + 'should forward expected options to pacote.packument' + ) + return { + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + } + } + t.teardown(() => { + result = '' + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + t.ifError(err, 'npm owner add ') + t.equal(result, '+ foo (@npmcli/map-workspaces)', 'should output add result') + }) +}) + +t.test('owner add cwd package', t => { + result = '' + readLocalPkgResponse = '@npmcli/map-workspaces' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + return { + _id: 'org.couchdb.user:foo', + email: 'foo@github.com', + name: 'foo' + } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + return {} + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => ({ + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + }) + t.teardown(() => { + result = '' + readLocalPkgResponse = null + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'foo'], err => { + t.ifError(err, 'npm owner add cwd package') + t.equal(result, '+ foo (@npmcli/map-workspaces)', 'should output add result') + t.end() + }) +}) + +t.test('owner add already an owner', t => { + t.plan(3) + + result = '' + npmlog.info = (title, msg) => { + t.equal(title, 'owner add', 'should use expected title') + t.equal( + msg, + 'Already a package owner: ruyadorno ', + 'should log already package owner info message' + ) + } + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:ruyadorno') { + return { + _id: 'org.couchdb.user:ruyadorno', + email: 'ruyadorno@hotmail.com', + name: 'ruyadorno' + } + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => { + return { + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + } + } + t.teardown(() => { + result = '' + npmlog.info = noop + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'ruyadorno', '@npmcli/map-workspaces'], err => { + t.ifError(err, 'npm owner add already an owner') + }) +}) + +t.test('owner add fails to retrieve user', t => { + result = '' + readLocalPkgResponse = + npmFetch.json = async (uri, opts) => { + // retrieve borked user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + return { ok: false } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + return {} + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => ({ + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + }) + t.teardown(() => { + result = '' + readLocalPkgResponse = null + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + t.match( + err, + /Error: Couldn't get user data for foo: {"ok":false}/, + 'should throw user data error' + ) + t.equal(err.code, 'EOWNERUSER', 'should have expected error code') + t.end() + }) +}) + +t.test('owner add fails to PUT updates', t => { + result = '' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + return { + _id: 'org.couchdb.user:foo', + email: 'foo@github.com', + name: 'foo' + } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + return { + error: { + status: '418', + message: "I'm a teapot" + } + } + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => ({ + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + }) + t.teardown(() => { + result = '' + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + t.match( + err.message, + /Failed to update package/, + 'should throw failed to update package error' + ) + t.equal(err.code, 'EOWNERMUTATE', 'should have expected error code') + t.end() + }) +}) + +t.test('owner add fails to retrieve user info', t => { + t.plan(3) + + result = '' + npmlog.error = (title, msg) => { + t.equal(title, 'owner mutate', 'should use expected title') + t.equal(msg, 'Error getting user data for foo') + } + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + throw Object.assign( + new Error("I'm a teapot"), + { status: 418 } + ) + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => ({ + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + }) + t.teardown(() => { + result = '' + npmlog.error = noop + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + t.match( + err.message, + "I'm a teapot", + 'should throw server error response' + ) + }) +}) + +t.test('owner add no previous maintainers property from server', t => { + result = '' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + return { + _id: 'org.couchdb.user:foo', + email: 'foo@github.com', + name: 'foo' + } + } else if (uri === '/@npmcli%2fno-owners-pkg/-rev/1-foobaaa1') { + return {} + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => { + return { + _rev: '1-foobaaa1', + maintainers: null + } + } + t.teardown(() => { + result = '' + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['add', 'foo', '@npmcli/no-owners-pkg'], err => { + t.ifError(err, 'npm owner add ') + t.equal(result, '+ foo (@npmcli/no-owners-pkg)', 'should output add result') + t.end() + }) +}) + +t.test('owner add no user', t => { + result = '' + t.teardown(() => { + result = '' + }) + + owner(['add'], err => { + t.equal( + err.message, + 'usage instructions', + 'should throw usage instructions if no user provided' + ) + t.end() + }) +}) + +t.test('owner add no cwd package', t => { + result = '' + t.teardown(() => { + result = '' + }) + + owner(['add', 'foo'], err => { + t.equal( + err.message, + 'usage instructions', + 'should throw usage instructions if no user provided' + ) + t.end() + }) +}) + +t.test('owner rm ', t => { + t.plan(9) + + result = '' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:ruyadorno') { + t.ok('should request user info') + t.match(opts, { ...npm.flatOptions }, 'should use expected opts') + return { + _id: 'org.couchdb.user:ruyadorno', + email: 'ruyadorno@hotmail.com', + name: 'ruyadorno' + } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + t.ok('should put changed owner') + t.match(opts, { + ...npm.flatOptions, + method: 'PUT', + body: { + _rev: '1-foobaaa1' + }, + otp: '123456', + spec: { + name: '@npmcli/map-workspaces' + } + }, 'should use expected opts') + t.deepEqual( + opts.body.maintainers, + npmcliMaintainers.filter(m => m.name !== 'ruyadorno'), + 'should contain expected new owners, removing requested user' + ) + return {} + } else { + t.fail(`unexpected fetch json call to: ${uri}`) + } + } + pacote.packument = async (spec, opts) => { + t.equal(spec.name, '@npmcli/map-workspaces', 'should use expect pkg name') + t.match( + opts, + { + ...npm.flatOptions, + fullMetadata: true + }, + 'should forward expected options to pacote.packument' + ) + return { + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + } + } + t.teardown(() => { + result = '' + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['rm', 'ruyadorno', '@npmcli/map-workspaces'], err => { + t.ifError(err, 'npm owner rm ') + t.equal(result, '- ruyadorno (@npmcli/map-workspaces)', 'should output rm result') + }) +}) + +t.test('owner rm not a current owner', t => { + t.plan(3) + + result = '' + npmlog.info = (title, msg) => { + t.equal(title, 'owner rm', 'should log expected title') + t.equal(msg, 'Not a package owner: foo', 'should log.info not a package owner msg') + } + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:foo') { + return { + _id: 'org.couchdb.user:foo', + email: 'foo@github.com', + name: 'foo' + } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + return {} + } else { + t.fail(`unexpected fetch json call to: ${uri}`) + } + } + pacote.packument = async (spec, opts) => { + return { + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + } + } + t.teardown(() => { + result = '' + npmlog.info = noop + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['rm', 'foo', '@npmcli/map-workspaces'], err => { + t.ifError(err, 'npm owner rm not a current owner') + }) +}) + +t.test('owner rm cwd package', t => { + result = '' + readLocalPkgResponse = '@npmcli/map-workspaces' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:ruyadorno') { + return { + _id: 'org.couchdb.user:ruyadorno', + email: 'ruyadorno@hotmail.com', + name: 'ruyadorno' + } + } else if (uri === '/@npmcli%2fmap-workspaces/-rev/1-foobaaa1') { + return {} + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => ({ + _rev: '1-foobaaa1', + maintainers: npmcliMaintainers + }) + t.teardown(() => { + result = '' + readLocalPkgResponse = null + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['rm', 'ruyadorno'], err => { + t.ifError(err, 'npm owner rm cwd package') + t.equal(result, '- ruyadorno (@npmcli/map-workspaces)', 'should output rm result') + t.end() + }) +}) + +t.test('owner rm only user', t => { + result = '' + readLocalPkgResponse = 'ipt' + npmFetch.json = async (uri, opts) => { + // retrieve user info from couchdb request + if (uri === '/-/user/org.couchdb.user:ruyadorno') { + return { + _id: 'org.couchdb.user:ruyadorno', + email: 'ruyadorno@hotmail.com', + name: 'ruyadorno' + } + } else { + t.fail(`unexpected fetch json call to uri: ${uri}`) + } + } + pacote.packument = async (spec, opts) => ({ + _rev: '1-foobaaa1', + maintainers: [{ + name: 'ruyadorno', + email: 'ruyadorno@hotmail.com' + }] + }) + t.teardown(() => { + result = '' + readLocalPkgResponse = null + npmFetch.json = noop + pacote.packument = noop + }) + + owner(['rm', 'ruyadorno'], err => { + t.equal( + err.message, + 'Cannot remove all owners of a package. Add someone else first.', + 'should throw unable to remove unique owner message' + ) + t.equal(err.code, 'EOWNERRM', 'should have expected error code') + t.end() + }) +}) + +t.test('owner rm no user', t => { + result = '' + t.teardown(() => { + result = '' + }) + + owner(['rm'], err => { + t.equal( + err.message, + 'usage instructions', + 'should throw usage instructions if no user provided to rm' + ) + t.end() + }) +}) + +t.test('owner rm no cwd package', t => { + result = '' + t.teardown(() => { + result = '' + }) + + owner(['rm', 'foo'], err => { + t.equal( + err.message, + 'usage instructions', + 'should throw usage instructions if no user provided to rm' + ) + t.end() + }) +}) + +t.test('completion', t => { + const { completion } = owner + + const testComp = (argv, expect) => { + completion({ conf: { argv: { remain: argv } } }, (err, res) => { + t.ifError(err) + t.strictSame(res, expect, argv.join(' ')) + }) + } + + testComp(['npm', 'foo'], []) + testComp(['npm', 'owner'], [ + 'add', + 'rm', + 'ls' + ]) + testComp(['npm', 'owner', 'add'], []) + testComp(['npm', 'owner', 'ls'], []) + testComp(['npm', 'owner', 'rm', 'foo'], []) + + // npm owner rm completion is async + t.test('completion npm owner rm', t => { + t.plan(3) + readLocalPkgResponse = '@npmcli/map-workspaces' + pacote.packument = async spec => { + t.equal(spec.name, readLocalPkgResponse, 'should use package spec') + return { + maintainers: npmcliMaintainers + } + } + t.teardown(() => { + readLocalPkgResponse = null + pacote.packument = noop + }) + + completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }, (err, res) => { + t.ifError(err, 'npm owner rm completion') + t.strictSame( + res, + [ + 'nlf', + 'ruyadorno', + 'darcyclarke', + 'isaacs' + ], + 'should return list of current owners' + ) + }) + }) + + t.test('completion npm owner rm no cwd package', t => { + completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }, (err, res) => { + t.ifError(err, 'npm owner rm completion') + t.strictSame(res, [], 'should have no owners to autocomplete if not cwd package') + t.end() + }) + }) + + t.test('completion npm owner rm no owners found', t => { + t.plan(3) + readLocalPkgResponse = '@npmcli/map-workspaces' + pacote.packument = async spec => { + t.equal(spec.name, readLocalPkgResponse, 'should use package spec') + return { + maintainers: [] + } + } + t.teardown(() => { + readLocalPkgResponse = null + pacote.packument = noop + }) + + completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }, (err, res) => { + t.ifError(err, 'npm owner rm completion') + t.strictSame(res, [], 'should return no owners if not found') + }) + }) + + t.end() +})