From b37a66542ca2879069b2acd338b1904de71b7f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Fri, 24 Aug 2018 12:08:44 -0700 Subject: [PATCH] org: add npm org command Credit: @chrisdickinson Credit: @zkat --- lib/config/cmd-list.js | 5 +- lib/org.js | 127 ++++++++++++++++++++++++++++++++++++++ test/tap/org.js | 136 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 lib/org.js create mode 100644 test/tap/org.js diff --git a/lib/config/cmd-list.js b/lib/config/cmd-list.js index a453082adc1bc..fa4390fcdcba7 100644 --- a/lib/config/cmd-list.js +++ b/lib/config/cmd-list.js @@ -50,7 +50,9 @@ var affordances = { 'rm': 'uninstall', 'r': 'uninstall', 'rum': 'run-script', - 'sit': 'cit' + 'sit': 'cit', + 'urn': 'run-script', + 'ogr': 'org' } // these are filenames in . @@ -89,6 +91,7 @@ var cmdList = [ 'token', 'profile', 'audit', + 'org', 'help', 'help-search', diff --git a/lib/org.js b/lib/org.js new file mode 100644 index 0000000000000..e838f615bc37f --- /dev/null +++ b/lib/org.js @@ -0,0 +1,127 @@ +'use strict' + +const BB = require('bluebird') + +const figgyPudding = require('figgy-pudding') +const liborg = require('libnpm/org') +const npmConfig = require('./config/figgy-config.js') +const output = require('./utils/output.js') +const otplease = require('./utils/otplease.js') +const Table = require('cli-table3') + +module.exports = org + +org.subcommands = ['set', 'rm', 'ls'] + +org.usage = + 'npm org set orgname username [developer | admin | owner]\n' + + 'npm org rm orgname username\n' + + 'npm org ls orgname' + +const OrgConfig = figgyPudding({ + json: {}, + loglevel: {}, + parseable: {}, + silent: {} +}) + +org.completion = function (opts, cb) { + var argv = opts.conf.argv.remain + if (argv.length === 2) { + return cb(null, org.subcommands) + } + switch (argv[2]) { + case 'ls': + case 'add': + case 'rm': + case 'set': + return cb(null, []) + default: + return cb(new Error(argv[2] + ' not recognized')) + } +} + +function UsageError () { + throw Object.assign(new Error(org.usage), {code: 'EUSAGE'}) +} + +function org ([cmd, orgname, username, role], cb) { + otplease(npmConfig(), opts => { + opts = OrgConfig(opts) + switch (cmd) { + case 'add': + case 'set': + return orgSet(orgname, username, role, opts) + case 'rm': + return orgRm(orgname, username, opts) + case 'ls': + return orgList(orgname, opts) + default: + UsageError() + } + }).then( + x => cb(null, x), + err => err.code === 'EUSAGE' ? err.message : err + ) +} + +function orgSet (org, user, role, opts) { + return liborg.set(org, user, role, opts).then(memDeets => { + if (opts.json) { + output(JSON.stringify(memDeets, null, 2)) + } else if (opts.parseable) { + output(['org', 'orgsize', 'user', 'role'].join('\t')) + output([ + memDeets.org.name, + memDeets.org.size, + memDeets.user, + memDeets.role + ]) + } else if (!opts.silent && opts.loglevel !== 'silent') { + output(`Added ${memDeets.user} as ${memDeets.role} to ${memDeets.org.name}. You now ${memDeets.org.size} member${memDeets.org.size === 1 ? '' : 's'} in this org.`) + } + return memDeets + }) +} + +function orgRm (org, user, opts) { + return liborg.rm(org, user, opts).then(() => { + return liborg.ls(org, opts) + }).then(roster => { + user = user.replace(/^[~@]?/, '') + org = org.replace(/^[~@]?/, '') + const userCount = Object.keys(roster).length + if (opts.json) { + output(JSON.stringify({ + user, + org, + userCount, + deleted: true + })) + } else if (opts.parseable) { + output(['user', 'org', 'userCount', 'deleted'].join('\t')) + output([user, org, userCount, true].join('\t')) + } else if (!opts.silent && opts.loglevel !== 'silent') { + output(`Successfully removed ${user} from ${org}. You now have ${userCount} member${userCount === 1 ? '' : 's'} in this org.`) + } + }) +} + +function orgList (org, opts) { + return liborg.ls(org, opts).then(roster => { + if (opts.json) { + output(JSON.stringify(roster, null, 2)) + } else if (opts.parseable) { + output(['user', 'role'].join('\t')) + Object.keys(roster).forEach(user => { + output([user, roster[user]].join('\t')) + }) + } else if (!opts.silent && opts.loglevel !== 'silent') { + const table = new Table({head: ['user', 'role']}) + Object.keys(roster).sort().forEach(user => { + table.push([user, roster[user]]) + }) + output(table.toString()) + } + }) +} diff --git a/test/tap/org.js b/test/tap/org.js new file mode 100644 index 0000000000000..7dd538aec671f --- /dev/null +++ b/test/tap/org.js @@ -0,0 +1,136 @@ +'use strict' + +var mr = require('npm-registry-mock') + +var test = require('tap').test +var common = require('../common-tap.js') + +var server + +test('setup', function (t) { + mr({port: common.port}, function (err, s) { + t.ifError(err, 'registry mocked successfully') + server = s + t.end() + }) +}) + +const names = ['add', 'set'] +const roles = ['developer', 'admin', 'owner'] + +names.forEach(function (name) { + test('org ' + name + ' [orgname] [username]: defaults to developer', function (t) { + const membershipData = { + org: { + name: 'myorg', + size: 1 + }, + user: 'myuser', + role: 'developer' + } + server.put('/-/org/myorg/user', JSON.stringify({ + user: 'myuser' + })).reply(200, membershipData) + common.npm([ + 'org', 'add', 'myorg', 'myuser', + '--json', + '--registry', common.registry, + '--loglevel', 'silent' + ], {}, function (err, code, stdout, stderr) { + t.ifError(err, 'npm org') + + t.equal(code, 0, 'exited OK') + t.equal(stderr, '', 'no error output') + + t.same(JSON.parse(stdout), membershipData) + t.end() + }) + }) + + roles.forEach(function (role) { + test('org ' + name + ' [orgname] [username]: accepts role ' + role, function (t) { + const membershipData = { + org: { + name: 'myorg', + size: 1 + }, + user: 'myuser', + role: role + } + server.put('/-/org/myorg/user', JSON.stringify({ + user: 'myuser' + })).reply(200, membershipData) + common.npm([ + 'org', name, 'myorg', 'myuser', + '--json', + '--registry', common.registry, + '--loglevel', 'silent' + ], {}, function (err, code, stdout, stderr) { + t.ifError(err, 'npm org') + + t.equal(code, 0, 'exited OK') + t.equal(stderr, '', 'no error output') + + t.same(JSON.parse(stdout), membershipData) + t.end() + }) + }) + }) +}) + +test('org rm [orgname] [username]', function (t) { + const membershipData = { + otheruser: 'admin' + } + server.delete('/-/org/myorg/user', JSON.stringify({ + user: 'myuser' + })).reply(204, {}) + server.get('/-/org/myorg/user') + .reply(200, membershipData) + common.npm([ + 'org', 'rm', 'myorg', 'myuser', + '--json', + '--registry', common.registry, + '--loglevel', 'silent' + ], {}, function (err, code, stdout, stderr) { + t.ifError(err, 'npm org') + + t.equal(code, 0, 'exited OK') + t.equal(stderr, '', 'no error output') + t.deepEqual(JSON.parse(stdout), { + user: 'myuser', + org: 'myorg', + deleted: true, + userCount: 1 + }, 'got useful info') + t.end() + }) +}) + +test('org ls [orgname]', function (t) { + const membershipData = { + username: 'admin', + username2: 'foo' + } + server.get('/-/org/myorg/user') + .reply(200, membershipData) + common.npm([ + 'org', 'ls', 'myorg', + '--json', + '--registry', common.registry, + '--loglevel', 'silent' + ], {}, function (err, code, stdout, stderr) { + t.ifError(err, 'npm org') + t.equal(code, 0, 'exited OK') + t.equal(stderr, '', 'no error output') + t.same(JSON.parse(stdout), membershipData, 'outputs members') + t.end() + }) +}) + +test('cleanup', function (t) { + t.pass('cleaned up') + server.done() + server.close() + t.end() +})