From 9c32c6c8d6fc5bdfd6af685731fe26920d7e5446 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 31 Aug 2022 08:37:26 -0700 Subject: [PATCH] feat(rewrite): rewrite `npm access` BREAKING CHANGE: renames most of the `npm access` subcommands - `edit`, having never been implemented, is removed - `public` is now `set status=public` - `restricted` is now `set status=private` - `ls-packages` is now `list packages` - `ls-collaborators` is now `list collaborators` - `2fa-required` is now `set mfa=publish` - `2fa-not-required` is now `set mfa=none` - `set mfa=automation` is added - output is no longer in json by default Usage: npm access list packages [|| [] npm access list collaborators [ []] npm access get status [] npm access set status=public|private [] npm access set mfa=false|publish|automation [] npm access grant [] npm access revoke [] Options: [--json] [--otp ] [--registry ] --- docs/content/commands/npm-access.md | 35 +- lib/commands/access.js | 284 ++++---- lib/commands/deprecate.js | 4 +- lib/commands/unpublish.js | 4 +- .../test/lib/commands/completion.js.test.cjs | 16 +- .../test/lib/load-all-commands.js.test.cjs | 14 +- tap-snapshots/test/lib/npm.js.test.cjs | 14 +- test/fixtures/mock-registry.js | 39 +- test/lib/commands/access.js | 659 ++++++++---------- test/lib/commands/deprecate.js | 9 +- test/lib/commands/unpublish.js | 43 +- 11 files changed, 517 insertions(+), 604 deletions(-) diff --git a/docs/content/commands/npm-access.md b/docs/content/commands/npm-access.md index f7a98af654714..bc481eac16336 100644 --- a/docs/content/commands/npm-access.md +++ b/docs/content/commands/npm-access.md @@ -11,15 +11,13 @@ description: Set access level on published packages ```bash -npm access public [] -npm access restricted [] +npm access list packages [|| [] +npm access list collaborators [ []] +npm access get status [] +npm access set status=public|private [] +npm access set mfa=none|publish|automation [] npm access grant [] npm access revoke [] -npm access 2fa-required [] -npm access 2fa-not-required [] -npm access ls-packages [||] -npm access ls-collaborators [ []] -npm access edit [] ``` @@ -91,12 +89,17 @@ Management of teams and team memberships is done with the `npm team` command. -#### `registry` +#### `json` -* Default: "https://registry.npmjs.org/" -* Type: URL +* Default: false +* Type: Boolean -The base URL of the npm registry. +Whether or not to output JSON data, rather than the normal output. + +* In `npm pkg set` it enables parsing set values with JSON.parse() before + saving them to your `package.json`. + +Not supported by all npm commands. @@ -115,6 +118,16 @@ password, npm will prompt on the command line for one. +#### `registry` + +* Default: "https://registry.npmjs.org/" +* Type: URL + +The base URL of the npm registry. + + + + ### See Also diff --git a/lib/commands/access.js b/lib/commands/access.js index 3621861537171..d5ac5bb2f008e 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -1,223 +1,221 @@ const path = require('path') -const libaccess = require('libnpmaccess') +const libnpmaccess = require('libnpmaccess') +const npa = require('npm-package-arg') const readPackageJson = require('read-package-json-fast') +const localeCompare = require('@isaacs/string-locale-compare')('en') const otplease = require('../utils/otplease.js') const getIdentity = require('../utils/get-identity.js') -const log = require('../utils/log-shim.js') const BaseCommand = require('../base-command.js') -const subcommands = [ - 'public', - 'restricted', +const commands = [ + 'get', 'grant', + 'list', 'revoke', - 'ls-packages', - 'ls-collaborators', - 'edit', - '2fa-required', - '2fa-not-required', + 'set', ] -const deprecated = [ - '2fa-not-required', - '2fa-required', - 'ls-collaborators', - 'ls-packages', - 'public', - 'restricted', +const setCommands = [ + 'status=public', + 'status=private', + 'mfa=none', + 'mfa=publish', + 'mfa=automation', + '2fa=none', + '2fa=publish', + '2fa=automation', ] class Access extends BaseCommand { static description = 'Set access level on published packages' static name = 'access' static params = [ - 'registry', + 'json', 'otp', + 'registry', ] static ignoreImplicitWorkspace = true static usage = [ - 'public []', - 'restricted []', + 'list packages [|| []', + 'list collaborators [ []]', + 'get status []', + 'set status=public|private []', + 'set mfa=none|publish|automation []', 'grant []', 'revoke []', - '2fa-required []', - '2fa-not-required []', - 'ls-packages [||]', - 'ls-collaborators [ []]', - 'edit []', ] async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { - return subcommands + return commands } switch (argv[2]) { case 'grant': - if (argv.length === 3) { - return ['read-only', 'read-write'] - } else { - return [] - } - - case 'public': - case 'restricted': - case 'ls-packages': - case 'ls-collaborators': - case 'edit': - case '2fa-required': - case '2fa-not-required': + return ['read-only', 'read-write'] case 'revoke': return [] + case 'list': + case 'ls': + return ['packages', 'collaborators'] + case 'get': + return ['status'] + case 'set': + return setCommands default: throw new Error(argv[2] + ' not recognized') } } - async exec ([cmd, ...args]) { + async exec ([cmd, subcmd, ...args]) { if (!cmd) { - throw this.usageError('Subcommand is required.') + throw this.usageError() } - - if (!subcommands.includes(cmd) || !this[cmd]) { - throw this.usageError(`${cmd} is not a recognized subcommand.`) + if (!commands.includes(cmd)) { + throw this.usageError(`${cmd} is not a valid access command`) } - - if (deprecated.includes(cmd)) { - log.warn('access', `${cmd} subcommand will be removed in the next version of npm`) + // All commands take at least one more parameter so we can do this check up front + if (!subcmd) { + throw this.usageError() } - return this[cmd](args, { - ...this.npm.flatOptions, - }) - } - - public ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.public) - } - - restricted ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.restricted) - } - - async grant ([perms, scopeteam, pkg], opts) { - if (!perms || (perms !== 'read-only' && perms !== 'read-write')) { - throw this.usageError('First argument must be either `read-only` or `read-write`.') - } - - if (!scopeteam) { - throw this.usageError('`` argument is required.') - } - - const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] - - if (!scope && !team) { - throw this.usageError( - 'Second argument used incorrect format.\n' + - 'Example: @example:developers' - ) - } - - return this.modifyPackage(pkg, opts, (pkgName, opts) => - libaccess.grant(pkgName, scopeteam, perms, opts), false) - } - - async revoke ([scopeteam, pkg], opts) { - if (!scopeteam) { - throw this.usageError('`` argument is required.') - } - - const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] - - if (!scope || !team) { - throw this.usageError( - 'First argument used incorrect format.\n' + - 'Example: @example:developers' - ) + switch (cmd) { + case 'grant': + if (!['read-only', 'read-write'].includes(subcmd)) { + throw this.usageError('grant must be either `read-only` or `read-write`') + } + if (!args[0]) { + throw this.usageError('`` argument is required') + } + return this.#grant(subcmd, args[0], args[1]) + case 'revoke': + return this.#revoke(subcmd, args[0]) + case 'list': + case 'ls': + if (subcmd === 'packages') { + return this.#listPackages(args[0], args[1]) + } + if (subcmd === 'collaborators') { + return this.#listCollaborators(args[0], args[1]) + } + throw this.usageError(`list ${subcmd} is not a valid access command`) + case 'get': + if (subcmd !== 'status') { + throw this.usageError(`get ${subcmd} is not a valid access command`) + } + return this.#getStatus(args[0]) + case 'set': + if (!setCommands.includes(subcmd)) { + throw this.usageError(`set ${subcmd} is not a valid access command`) + } + return this.#set(subcmd, args[0]) } - - return this.modifyPackage(pkg, opts, (pkgName, opts) => - libaccess.revoke(pkgName, scopeteam, opts)) - } - - get ['2fa-required'] () { - return this.tfaRequired - } - - tfaRequired ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.tfaRequired, false) } - get ['2fa-not-required'] () { - return this.tfaNotRequired + async #grant (permissions, scope, pkg) { + await libnpmaccess.setPermissions(scope, pkg, permissions) } - tfaNotRequired ([pkg], opts) { - return this.modifyPackage(pkg, opts, libaccess.tfaNotRequired, false) + async #revoke (scope, pkg) { + await libnpmaccess.removePermissions(scope, pkg) } - get ['ls-packages'] () { - return this.lsPackages - } - - async lsPackages ([owner], opts) { + async #listPackages (owner, pkg) { if (!owner) { - owner = await getIdentity(this.npm, opts) + owner = await getIdentity(this.npm, this.npm.flatOptions) } - - const pkgs = await libaccess.lsPackages(owner, opts) - - // TODO - print these out nicely (breaking change) - this.npm.output(JSON.stringify(pkgs, null, 2)) + const pkgs = await libnpmaccess.getPackages(owner, this.npm.flatOptions) + this.#output(pkgs, pkg) } - get ['ls-collaborators'] () { - return this.lsCollaborators + async #listCollaborators (pkg, user) { + const pkgName = await this.#getPackage(pkg, false) + const collabs = await libnpmaccess.getCollaborators(pkgName, this.npm.flatOptions) + this.#output(collabs, user) } - async lsCollaborators ([pkg, usr], opts) { - const pkgName = await this.getPackage(pkg, false) - const collabs = await libaccess.lsCollaborators(pkgName, usr, opts) + async #getStatus (pkg) { + const pkgName = await this.#getPackage(pkg, false) + const visibility = await libnpmaccess.getVisibility(pkgName, this.npm.flatOptions) + this.#output({ [pkgName]: visibility.public ? 'public' : 'private' }) + } - // TODO - print these out nicely (breaking change) - this.npm.output(JSON.stringify(collabs, null, 2)) + async #set (subcmd, pkg) { + const [subkey, subval] = subcmd.split('=') + switch (subkey) { + case 'mfa': + case '2fa': + return this.#setMfa(pkg, subval) + case 'status': + return this.#setStatus(pkg, subval) + } } - async edit () { - throw new Error('edit subcommand is not implemented') + async #setMfa (pkg, level) { + const pkgName = await this.#getPackage(pkg, false) + await otplease(this.npm, this.npm.flatOptions, (opts) => { + return libnpmaccess.setMfa(pkgName, level, opts) + }) } - modifyPackage (pkg, opts, fn, requireScope = true) { - return this.getPackage(pkg, requireScope) - .then(pkgName => otplease(this.npm, opts, opts => fn(pkgName, opts))) + async #setStatus (pkg, status) { + // only scoped packages can have their access changed + const pkgName = await this.#getPackage(pkg, true) + if (status === 'private') { + status = 'restricted' + } + await otplease(this.npm, this.npm.flatOptions, (opts) => { + return libnpmaccess.setAccess(pkgName, status, opts) + }) + return this.#getStatus(pkgName) } - async getPackage (name, requireScope) { - if (name && name.trim()) { - return name.trim() - } else { + async #getPackage (name, requireScope) { + if (!name) { try { const pkg = await readPackageJson(path.resolve(this.npm.prefix, 'package.json')) name = pkg.name } catch (err) { if (err.code === 'ENOENT') { - throw new Error( - 'no package name passed to command and no package.json found' - ) + throw Object.assign(new Error('no package name given and no package.json found'), { + code: 'ENOENT', + }) } else { throw err } } + } - if (requireScope && !name.match(/^@[^/]+\/.*$/)) { - throw this.usageError('This command is only available for scoped packages.') - } else { - return name + const spec = npa(name) + if (requireScope && !spec.scope) { + throw this.usageError('This command is only available for scoped packages.') + } + return name + } + + #output (items, limiter) { + const output = {} + const lookup = { + __proto__: null, + read: 'read-only', + write: 'read-write', + } + for (const item in items) { + const val = items[item] + output[item] = lookup[val] || val + } + if (this.npm.config.get('json')) { + this.npm.output(JSON.stringify(output, null, 2)) + } else { + for (const item of Object.keys(output).sort(localeCompare)) { + if (!limiter || limiter === item) { + this.npm.output(`${item}: ${output[item]}`) + } } } } diff --git a/lib/commands/deprecate.js b/lib/commands/deprecate.js index 068bfdbcec717..c41546eb1b85e 100644 --- a/lib/commands/deprecate.js +++ b/lib/commands/deprecate.js @@ -23,10 +23,10 @@ class Deprecate extends BaseCommand { } const username = await getIdentity(this.npm, this.npm.flatOptions) - const packages = await libaccess.lsPackages(username, this.npm.flatOptions) + const packages = await libaccess.getPackages(username, this.npm.flatOptions) return Object.keys(packages) .filter((name) => - packages[name] === 'read-write' && + packages[name] === 'write' && (opts.conf.argv.remain.length === 0 || name.startsWith(opts.conf.argv.remain[0]))) } diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index 0e5ef3dc5e91d..968bcf8018958 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -42,11 +42,11 @@ class Unpublish extends BaseCommand { return [] } - const access = await libaccess.lsPackages(username, opts) + const access = await libaccess.getPackages(username, opts) // do a bit of filtering at this point, so that we don't need // to fetch versions for more than one thing, but also don't // accidentally unpublish a whole project - let pkgs = Object.keys(access || {}) + let pkgs = Object.keys(access) if (!partialWord || !pkgs.length) { return pkgs } diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs index fb4c53a0205fb..85a883bd58b26 100644 --- a/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -172,11 +172,7 @@ Array [ ` exports[`test/lib/commands/completion.js TAP completion filtered subcommands > filtered subcommands 1`] = ` -Array [ - Array [ - "public", - ], -] +Array [] ` exports[`test/lib/commands/completion.js TAP completion flags > flags 1`] = ` @@ -220,15 +216,11 @@ exports[`test/lib/commands/completion.js TAP completion subcommand completion > Array [ Array [ String( - public - restricted + get grant + list revoke - ls-packages - ls-collaborators - edit - 2fa-required - 2fa-not-required + set ), ], ] diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index d4a340431792a..038121be95435 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -9,18 +9,16 @@ exports[`test/lib/load-all-commands.js TAP load each command access > must match Set access level on published packages Usage: -npm access public [] -npm access restricted [] +npm access list packages [|| [] +npm access list collaborators [ []] +npm access get status [] +npm access set status=public|private [] +npm access set mfa=none|publish|automation [] npm access grant [] npm access revoke [] -npm access 2fa-required [] -npm access 2fa-not-required [] -npm access ls-packages [||] -npm access ls-collaborators [ []] -npm access edit [] Options: -[--registry ] [--otp ] +[--json] [--otp ] [--registry ] Run "npm help access" for more info ` diff --git a/tap-snapshots/test/lib/npm.js.test.cjs b/tap-snapshots/test/lib/npm.js.test.cjs index 68adb55516c72..1c3dbe39bd4ae 100644 --- a/tap-snapshots/test/lib/npm.js.test.cjs +++ b/tap-snapshots/test/lib/npm.js.test.cjs @@ -168,18 +168,16 @@ All commands: access Set access level on published packages Usage: - npm access public [] - npm access restricted [] + npm access list packages [|| [] + npm access list collaborators [ []] + npm access get status [] + npm access set status=public|private [] + npm access set mfa=none|publish|automation [] npm access grant [] npm access revoke [] - npm access 2fa-required [] - npm access 2fa-not-required [] - npm access ls-packages [||] - npm access ls-collaborators [ []] - npm access edit [] Options: - [--registry ] [--otp ] + [--json] [--otp ] [--registry ] Run "npm help access" for more info diff --git a/test/fixtures/mock-registry.js b/test/fixtures/mock-registry.js index 8fb5a055ff2d7..65d4759627aa6 100644 --- a/test/fixtures/mock-registry.js +++ b/test/fixtures/mock-registry.js @@ -67,21 +67,19 @@ class MockRegistry { } } - access ({ spec, access, publishRequires2fa }) { - const body = {} - if (access !== undefined) { - body.access = access - } - if (publishRequires2fa !== undefined) { - body.publish_requires_tfa = publishRequires2fa - } + setAccess ({ spec, body = {} }) { this.nock = this.nock.post( - `/-/package/${encodeURIComponent(spec)}/access`, + `/-/package/${npa(spec).escapedName}/access`, body ).reply(200) } - grant ({ spec, team, permissions }) { + getVisibility ({ spec, visibility }) { + this.nock = this.nock.get(`/-/package/${npa(spec).escapedName}/visibility`) + .reply(200, visibility) + } + + setPermissions ({ spec, team, permissions }) { if (team.startsWith('@')) { team = team.slice(1) } @@ -92,7 +90,7 @@ class MockRegistry { ).reply(200) } - revoke ({ spec, team }) { + removePermissions ({ spec, team }) { if (team.startsWith('@')) { team = team.slice(1) } @@ -141,27 +139,22 @@ class MockRegistry { } // team can be a team or a username - lsPackages ({ team, packages = {}, times = 1 }) { + getPackages ({ team, packages = {}, times = 1 }) { if (team.startsWith('@')) { team = team.slice(1) } - const [scope, teamName] = team.split(':') + const [scope, teamName] = team.split(':').map(encodeURIComponent) let uri if (teamName) { - uri = `/-/team/${encodeURIComponent(scope)}/${encodeURIComponent(teamName)}/package` + uri = `/-/team/${scope}/${teamName}/package` } else { - uri = `/-/org/${encodeURIComponent(scope)}/package` + uri = `/-/org/${scope}/package` } - this.nock = this.nock.get(uri).query({ format: 'cli' }).times(times).reply(200, packages) + this.nock = this.nock.get(uri).times(times).reply(200, packages) } - lsCollaborators ({ spec, user, collaborators = {} }) { - const query = { format: 'cli' } - if (user) { - query.user = user - } - this.nock = this.nock.get(`/-/package/${encodeURIComponent(spec)}/collaborators`) - .query(query) + getCollaborators ({ spec, collaborators = {} }) { + this.nock = this.nock.get(`/-/package/${npa(spec).escapedName}/collaborators`) .reply(200, collaborators) } diff --git a/test/lib/commands/access.js b/test/lib/commands/access.js index aa748b10681df..ae26f4c9332dc 100644 --- a/test/lib/commands/access.js +++ b/test/lib/commands/access.js @@ -3,6 +3,7 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const MockRegistry = require('../../fixtures/mock-registry.js') +const token = 'test-auth-token' const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' } t.test('completion', async t => { @@ -13,20 +14,21 @@ t.test('completion', async t => { t.resolves(res, expect, argv.join(' ')) } - testComp(['npm', 'access'], [ - 'public', 'restricted', 'grant', 'revoke', 'ls-packages', - 'ls-collaborators', 'edit', '2fa-required', '2fa-not-required', + testComp(['npm', 'access'], ['list', 'get', 'set', 'grant', 'revoke']) + testComp(['npm', 'access', 'list'], ['packages', 'collaborators']) + testComp(['npm', 'access', 'ls'], ['packages', 'collaborators']) + testComp(['npm', 'access', 'get'], ['status']) + testComp(['npm', 'access', 'set'], [ + 'status=public', + 'status=private', + 'mfa=none', + 'mfa=publish', + 'mfa=automation', + '2fa=none', + '2fa=publish', + '2fa=automation', ]) testComp(['npm', 'access', 'grant'], ['read-only', 'read-write']) - testComp(['npm', 'access', 'grant', 'read-only'], []) - testComp(['npm', 'access', 'public'], []) - testComp(['npm', 'access', 'restricted'], []) - testComp(['npm', 'access', 'revoke'], []) - testComp(['npm', 'access', 'ls-packages'], []) - testComp(['npm', 'access', 'ls-collaborators'], []) - testComp(['npm', 'access', 'edit'], []) - testComp(['npm', 'access', '2fa-required'], []) - testComp(['npm', 'access', '2fa-not-required'], []) testComp(['npm', 'access', 'revoke'], []) await t.rejects( @@ -35,406 +37,337 @@ t.test('completion', async t => { ) }) -t.test('subcommand required', async t => { +t.test('command required', async t => { const { npm } = await loadMockNpm(t) - const access = await npm.cmd('access') - await t.rejects( - npm.exec('access', []), - access.usageError('Subcommand is required.') - ) + await t.rejects(npm.exec('access', []), { code: 'EUSAGE' }) }) -t.test('unrecognized subcommand', async t => { +t.test('unrecognized command', async t => { const { npm } = await loadMockNpm(t) await t.rejects( - npm.exec('access', ['blerg']), - /blerg is not a recognized subcommand/, - 'should throw EUSAGE on missing subcommand' - ) + npm.exec('access', ['blerg']), { code: 'EUSAGE' }) }) -t.test('edit', async t => { +t.test('subcommand required', async t => { const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', ['edit', '@scoped/another']), - /edit subcommand is not implemented/, - 'should throw not implemented yet error' - ) + await t.rejects(npm.exec('access', ['get']), { code: 'EUSAGE' }) }) -t.test('access public on unscoped package', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': JSON.stringify({ - name: 'npm-access-public-pkg', - }), - }, - }) - await t.rejects( - npm.exec('access', ['public']), - /This command is only available for scoped packages/, - 'should throw scoped-restricted error' - ) +t.test('unrecognized subcommand', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['list', 'blerg']), { code: 'EUSAGE' }) }) -t.test('access public on scoped package', async t => { - const name = '@scoped/npm-access-public-pkg' - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ name }), - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', +t.test('grant', t => { + t.test('invalid permissions', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['grant', 'other']), { code: 'EUSAGE' }) }) - registry.access({ spec: name, access: 'public' }) - await npm.exec('access', ['public']) - t.match(logs.warn[0], ['access', 'public subcommand will be removed in the next version of npm']) - t.equal(joinedOutput(), '') -}) -t.test('access public on missing package.json', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', ['public']), - /no package name passed to command and no package.json found/, - 'should throw no package.json found error' - ) -}) + t.test('no permissions', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['grant', 'read-only']), { code: 'EUSAGE' }) + }) -t.test('access public on invalid package.json', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': '{\n', - node_modules: {}, - }, + t.test('read-only', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + const permissions = 'read-only' + registry.setPermissions({ spec: '@npmcli/test-package', team: '@npm:test-team', permissions }) + await npm.exec('access', ['grant', permissions, '@npm:test-team', '@npmcli/test-package']) }) - await t.rejects( - npm.exec('access', ['public']), - { code: 'EJSONPARSE' }, - 'should throw failed to parse package.json' - ) + t.end() }) -t.test('access restricted on unscoped package', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': JSON.stringify({ - name: 'npm-access-restricted-pkg', - }), - }, +t.test('revoke', t => { + t.test('success', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.removePermissions({ spec: '@npmcli/test-package', team: '@npm:test-team' }) + await npm.exec('access', ['revoke', '@npm:test-team', '@npmcli/test-package']) }) - await t.rejects( - npm.exec('access', ['public']), - /This command is only available for scoped packages/, - 'should throw scoped-restricted error' - ) + t.end() }) -t.test('access restricted on scoped package', async t => { - const name = '@scoped/npm-access-restricted-pkg' - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ name }), - }, +t.test('list', t => { + const packages = { + '@npmcli/test-package': 'read', + '@npmcli/other-package': 'write', + } + const collaborators = { + npm: 'write', + github: 'read', + } + t.test('invalid subcommand', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['list', 'other'], { code: 'EUSAGE' })) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + + t.test('packages explicit user', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getPackages({ team: '@npm:test-team', packages }) + await npm.exec('access', ['list', 'packages', '@npm:test-team']) + t.same(outputs, [ + ['@npmcli/other-package: read-write'], + ['@npmcli/test-package: read-only'], + ]) }) - registry.access({ spec: name, access: 'restricted' }) - await npm.exec('access', ['restricted']) - t.match(logs.warn[0], - ['access', 'restricted subcommand will be removed in the next version of npm'] - ) - t.equal(joinedOutput(), '') -}) -t.test('access restricted on missing package.json', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', ['restricted']), - /no package name passed to command and no package.json found/, - 'should throw no package.json found error' - ) -}) + t.test('packages infer user', async t => { + const { npm, outputs } = await loadMockNpm(t, { config: { ...auth } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: token, + }) + registry.whoami({ username: 'npm' }) + registry.getPackages({ team: 'npm', packages }) + await npm.exec('access', ['list', 'packages']) + t.same(outputs, [ + ['@npmcli/other-package: read-write'], + ['@npmcli/test-package: read-only'], + ]) + }) -t.test('access restricted on invalid package.json', async t => { - const { npm } = await loadMockNpm(t, { - prefixDir: { - 'package.json': '{\n', - node_modules: {}, - }, + t.test('packages json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { config: { json: true } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getPackages({ team: '@npm:test-team', packages }) + await npm.exec('access', ['list', 'packages', '@npm:test-team']) + t.same(JSON.parse(joinedOutput()), { + '@npmcli/test-package': 'read-only', + '@npmcli/other-package': 'read-write', + }) }) - await t.rejects( - npm.exec('access', ['restricted']), - { code: 'EJSONPARSE' }, - 'should throw failed to parse package.json' - ) -}) -t.test('access grant read-only', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('collaborators explicit package', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getCollaborators({ spec: '@npmcli/test-package', collaborators }) + await npm.exec('access', ['list', 'collaborators', '@npmcli/test-package']) + t.same(outputs, [ + ['github: read-only'], + ['npm: read-write'], + ]) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + + t.test('collaborators user', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getCollaborators({ spec: '@npmcli/test-package', collaborators }) + await npm.exec('access', ['list', 'collaborators', '@npmcli/test-package', 'npm']) + t.same(outputs, [ + ['npm: read-write'], + ]) }) - registry.grant({ spec: '@scoped/another', team: 'myorg:myteam', permissions: 'read-only' }) - await npm.exec('access', ['grant', 'read-only', 'myorg:myteam', '@scoped/another']) - t.equal(joinedOutput(), '') + t.end() }) -t.test('access grant read-write', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', +t.test('get', t => { + t.test('invalid subcommand', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects(npm.exec('access', ['get', 'other'], { code: 'EUSAGE' })) }) - registry.grant({ spec: '@scoped/another', team: 'myorg:myteam', permissions: 'read-write' }) - await npm.exec('access', ['grant', 'read-write', 'myorg:myteam', '@scoped/another']) - t.equal(joinedOutput(), '') -}) -t.test('access grant current cwd', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ - name: 'yargs', - }), - }, + t.test('status explicit package', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['get', 'status', '@npmcli/test-package']) + t.same(outputs, [['@npmcli/test-package: public']]) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status implicit package', async t => { + const { npm, outputs } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ name: '@npmcli/test-package' }), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['get', 'status']) + t.same(outputs, [['@npmcli/test-package: public']]) }) - registry.grant({ spec: 'yargs', team: 'myorg:myteam', permissions: 'read-write' }) - await npm.exec('access', ['grant', 'read-write', 'myorg:myteam']) - t.equal(joinedOutput(), '') -}) - -t.test('access grant others', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'grant', - 'rerere', - 'myorg:myteam', - '@scoped/another', - ]), - /First argument must be either `read-only` or `read-write`./, - 'should throw unrecognized argument error' - ) -}) - -t.test('access grant missing team args', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'grant', - 'read-only', - undefined, - '@scoped/another', - ]), - /`` argument is required./, - 'should throw missing argument error' - ) -}) - -t.test('access grant malformed team arg', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'grant', - 'read-only', - 'foo', - '@scoped/another', - ]), - /Second argument used incorrect format.\n/, - 'should throw malformed arg error' - ) -}) - -t.test('access 2fa-required', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('status no package', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['get', 'status']), + { code: 'ENOENT' } + ) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status invalid package', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { 'package.json': '[not:valid_json}' }, + }) + await t.rejects( + npm.exec('access', ['get', 'status']), + { code: 'EJSONPARSE' } + ) }) - registry.access({ spec: '@scope/pkg', publishRequires2fa: true }) - await npm.exec('access', ['2fa-required', '@scope/pkg']) - t.match(logs.warn[0], - ['access', '2fa-required subcommand will be removed in the next version of npm'] - ) - t.equal(joinedOutput(), '') + t.test('status json', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { config: { json: true } }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['get', 'status', '@npmcli/test-package']) + t.same(JSON.parse(joinedOutput()), { '@npmcli/test-package': 'public' }) + }) + t.end() }) -t.test('access 2fa-not-required', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, +t.test('set', t => { + t.test('status=public', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', body: { access: 'public' } }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: true } }) + await npm.exec('access', ['set', 'status=public', '@npmcli/test-package']) + t.same(outputs, [['@npmcli/test-package: public']]) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status=private', async t => { + const { npm, outputs } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', body: { access: 'restricted' } }) + registry.getVisibility({ spec: '@npmcli/test-package', visibility: { public: false } }) + await npm.exec('access', ['set', 'status=private', '@npmcli/test-package']) + t.same(outputs, [['@npmcli/test-package: private']]) }) - registry.access({ spec: '@scope/pkg', publishRequires2fa: false }) - await npm.exec('access', ['2fa-not-required', '@scope/pkg']) - t.match(logs.warn[0], - ['access', '2fa-not-required subcommand will be removed in the next version of npm'] - ) - t.equal(joinedOutput(), '') -}) - -t.test('access revoke', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('status=invalid', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', 'status=invalid', '@npmcli/test-package']), + { code: 'EUSAGE' } + ) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('status non scoped package', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', 'status=public', 'npm']), + { code: 'EUSAGE' } + ) }) - registry.revoke({ spec: '@scoped/another', team: 'myorg:myteam' }) - await npm.exec('access', ['revoke', 'myorg:myteam', '@scoped/another']) - t.equal(joinedOutput(), '') -}) - -t.test('access revoke missing team args', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'revoke', - undefined, - '@scoped/another', - ]), - /`` argument is required./, - 'should throw missing argument error' - ) -}) - -t.test('access revoke malformed team arg', async t => { - const { npm } = await loadMockNpm(t) - await t.rejects( - npm.exec('access', [ - 'revoke', - 'foo', - '@scoped/another', - ]), - /First argument used incorrect format.\n/, - 'should throw malformed arg error' - ) -}) - -t.test('npm access ls-packages with no team', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('mfa=none', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: false, + } }) + await npm.exec('access', ['set', 'mfa=none', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('mfa=publish', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: false, + } }) + await npm.exec('access', ['set', 'mfa=publish', '@npmcli/test-package']) }) - const team = 'foo' - const packages = { 'test-package': 'read-write' } - registry.whoami({ username: team }) - registry.lsPackages({ team, packages }) - await npm.exec('access', ['ls-packages']) - t.match(logs.warn[0], - ['access', 'ls-packages subcommand will be removed in the next version of npm'] - ) - t.match(JSON.parse(joinedOutput()), packages) -}) - -t.test('access ls-packages on team', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('mfa=automation', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: true, + } }) + await npm.exec('access', ['set', 'mfa=automation', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('mfa=invalid', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', 'mfa=invalid']), + { code: 'EUSAGE' } + ) }) - const team = 'myorg:myteam' - const packages = { 'test-package': 'read-write' } - registry.lsPackages({ team, packages }) - await npm.exec('access', ['ls-packages', 'myorg:myteam']) - t.match(JSON.parse(joinedOutput()), packages) -}) - -t.test('access ls-collaborators on current', async t => { - const { npm, joinedOutput, logs } = await loadMockNpm(t, { - config: { - ...auth, - }, - prefixDir: { - 'package.json': JSON.stringify({ - name: 'yargs', - }), - }, + t.test('2fa=none', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: false, + } }) + await npm.exec('access', ['set', '2fa=none', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('2fa=publish', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: false, + } }) + await npm.exec('access', ['set', '2fa=publish', '@npmcli/test-package']) }) - const collaborators = { 'test-user': 'read-write' } - registry.lsCollaborators({ spec: 'yargs', collaborators }) - await npm.exec('access', ['ls-collaborators']) - t.match(logs.warn[0], - ['access', 'ls-collaborators subcommand will be removed in the next version of npm'] - ) - t.match(JSON.parse(joinedOutput()), collaborators) -}) - -t.test('access ls-collaborators on spec', async t => { - const { npm, joinedOutput } = await loadMockNpm(t, { - config: { - ...auth, - }, + t.test('2fa=automation', async t => { + const { npm } = await loadMockNpm(t) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.setAccess({ spec: '@npmcli/test-package', + body: { + publish_requires_tfa: true, + automation_token_overrides_tfa: true, + } }) + await npm.exec('access', ['set', '2fa=automation', '@npmcli/test-package']) }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', + t.test('2fa=invalid', async t => { + const { npm } = await loadMockNpm(t) + await t.rejects( + npm.exec('access', ['set', '2fa=invalid']), + { code: 'EUSAGE' } + ) }) - const collaborators = { 'test-user': 'read-write' } - registry.lsCollaborators({ spec: 'yargs', collaborators }) - await npm.exec('access', ['ls-collaborators', 'yargs']) - t.match(JSON.parse(joinedOutput()), collaborators) + + t.end() }) diff --git a/test/lib/commands/deprecate.js b/test/lib/commands/deprecate.js index 3a610a703a2dc..57ed9c93ba0f4 100644 --- a/test/lib/commands/deprecate.js +++ b/test/lib/commands/deprecate.js @@ -32,7 +32,7 @@ t.test('completion', async t => { }) registry.whoami({ username: user, times: 4 }) - registry.lsPackages({ team: user, packages, times: 4 }) + registry.getPackages({ team: user, packages, times: 4 }) await Promise.all([ testComp([], ['foo', 'bar', 'baz']), testComp(['b'], ['bar', 'baz']), @@ -42,9 +42,12 @@ t.test('completion', async t => { await testComp(['foo', 'something'], []) - registry.whoami({ statusCode: 404, body: {} }) + registry.whoami({ responseCode: 401, body: {} }) - t.rejects(testComp([], []), { code: 'EINVALIDTYPE' }) + await t.rejects( + testComp([], []), + { code: 'E401' } + ) }) t.test('no args', async t => { diff --git a/test/lib/commands/unpublish.js b/test/lib/commands/unpublish.js index 28f93ea3e77a4..9efd2a147d42f 100644 --- a/test/lib/commands/unpublish.js +++ b/test/lib/commands/unpublish.js @@ -424,7 +424,7 @@ t.test('completion', async t => { }) await registry.package({ manifest, query: { write: true } }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { [pkg]: 'write' }) + registry.getPackages({ team: user, packages: { [pkg]: 'write' } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -446,7 +446,7 @@ t.test('completion', async t => { manifest.versions = {} await registry.package({ manifest, query: { write: true } }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { [pkg]: 'write' }) + registry.getPackages({ team: user, packages: { [pkg]: 'write' } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -465,11 +465,12 @@ t.test('completion', async t => { authorization: 'test-auth-token', }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { - [pkg]: 'write', - [`${pkg}a`]: 'write', - [`${pkg}b`]: 'write', - }) + registry.getPackages({ team: user, + packages: { + [pkg]: 'write', + [`${pkg}a`]: 'write', + [`${pkg}b`]: 'write', + } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -489,7 +490,7 @@ t.test('completion', async t => { authorization: 'test-auth-token', }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, {}) + registry.getPackages({ team: user, packages: {} }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -506,10 +507,11 @@ t.test('completion', async t => { authorization: 'test-auth-token', }) registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, { - [pkg]: 'write', - [`${pkg}a`]: 'write', - }) + registry.getPackages({ team: user, + packages: { + [pkg]: 'write', + [`${pkg}a`]: 'write', + } }) await testComp(t, { argv: ['npm', 'unpublish'], @@ -519,23 +521,6 @@ t.test('completion', async t => { }) }) - t.test('no pkg names retrieved from user account', async t => { - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - authorization: 'test-auth-token', - }) - registry.whoami({ username: user }) - registry.nock.get('/-/org/test-user/package?format=cli').reply(200, null) - - await testComp(t, { - argv: ['npm', 'unpublish'], - partialWord: pkg, - expect: [], - title: 'should have no autocomplete', - }) - }) - t.test('logged out user', async t => { const registry = new MockRegistry({ tap: t,