From 81afa5a8838c71a3a5037e2c8b4ae196e19fe0d7 Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 24 Mar 2022 09:07:44 -0700 Subject: [PATCH] fix(unpublish): properly apply publishConfig (#4601) The tests for unpublish were mocked so heavily they weren't actually asserting anything. In rewriting them several bugs were found. - `write=true` was not being consistenly used when fetching packument data, it is now. - The decision on when to load the local package.json file was not working at all, that has been fixed. If the cwd contains a package.json whose name matches the package you are uninstalling, the local package.json will be read and its publishConfig applied to your request. - dead code inside the `npm unpublish` path was removed. There is no need to check if you are unpublishing the last version here, you're already unpublishing the entire project. - publishConfig is now being applied through the config flatten method, not a raw object assignment. --- lib/commands/unpublish.js | 90 +-- .../test/lib/commands/unpublish.js.test.cjs | 14 - test/fixtures/tnock.js | 10 +- test/lib/commands/unpublish.js | 716 +++++++++--------- 4 files changed, 388 insertions(+), 442 deletions(-) delete mode 100644 tap-snapshots/test/lib/commands/unpublish.js.test.cjs diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index d8de9edfa6ba7..f27be2e41c107 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -1,13 +1,15 @@ -const path = require('path') -const util = require('util') -const npa = require('npm-package-arg') const libaccess = require('libnpmaccess') -const npmFetch = require('npm-registry-fetch') const libunpub = require('libnpmpublish').unpublish +const npa = require('npm-package-arg') +const npmFetch = require('npm-registry-fetch') +const path = require('path') +const util = require('util') const readJson = util.promisify(require('read-package-json')) + +const flatten = require('../utils/config/flatten.js') +const getIdentity = require('../utils/get-identity.js') const log = require('../utils/log-shim') const otplease = require('../utils/otplease.js') -const getIdentity = require('../utils/get-identity.js') const LAST_REMAINING_VERSION_ERROR = 'Refusing to delete the last version of the package. ' + 'It will block from republishing a new version for 24 hours.\n' + @@ -22,7 +24,8 @@ class Unpublish extends BaseCommand { static ignoreImplicitWorkspace = false async getKeysOfVersions (name, opts) { - const json = await npmFetch.json(npa(name).escapedName, opts) + const pkgUri = npa(name).escapedName + const json = await npmFetch.json(`${pkgUri}?write=true`, opts) return Object.keys(json.versions) } @@ -67,12 +70,10 @@ class Unpublish extends BaseCommand { throw this.usageError() } - const spec = args.length && npa(args[0]) + let spec = args.length && npa(args[0]) const force = this.npm.config.get('force') const { silent } = this.npm const dryRun = this.npm.config.get('dry-run') - let pkgName - let pkgVersion log.silly('unpublish', 'args[0]', args[0]) log.silly('unpublish', 'spec', spec) @@ -85,53 +86,52 @@ class Unpublish extends BaseCommand { } const opts = { ...this.npm.flatOptions } - if (!spec || path.resolve(spec.name) === this.npm.localPrefix) { - // if there's a package.json in the current folder, then - // read the package name and version out of that. + + let pkgName + let pkgVersion + let manifest + let manifestErr + try { const pkgJson = path.join(this.npm.localPrefix, 'package.json') - let manifest - try { - manifest = await readJson(pkgJson) - } catch (err) { - if (err && err.code !== 'ENOENT' && err.code !== 'ENOTDIR') { - throw err - } else { + manifest = await readJson(pkgJson) + } catch (err) { + manifestErr = err + } + if (spec) { + // If cwd has a package.json with a name that matches the package being + // unpublished, load up the publishConfig + if (manifest && manifest.name === spec.name && manifest.publishConfig) { + flatten(manifest.publishConfig, opts) + } + const versions = await this.getKeysOfVersions(spec.name, opts) + if (versions.length === 1 && !force) { + throw this.usageError(LAST_REMAINING_VERSION_ERROR) + } + pkgName = spec.name + pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : '' + } else { + if (manifestErr) { + if (manifestErr.code === 'ENOENT' || manifestErr.code === 'ENOTDIR') { throw this.usageError() + } else { + throw manifestErr } } log.verbose('unpublish', manifest) - const { name, version, publishConfig } = manifest - const pkgJsonSpec = npa.resolve(name, version) - const optsWithPub = { ...opts, publishConfig } - - const versions = await this.getKeysOfVersions(name, optsWithPub) - if (versions.length === 1 && !force) { - throw this.usageError( - LAST_REMAINING_VERSION_ERROR - ) + spec = npa.resolve(manifest.name, manifest.version) + if (manifest.publishConfig) { + flatten(manifest.publishConfig, opts) } - if (!dryRun) { - await otplease(opts, opts => libunpub(pkgJsonSpec, optsWithPub)) - } - pkgName = name - pkgVersion = version ? `@${version}` : '' - } else { - const versions = await this.getKeysOfVersions(spec.name, opts) - if (versions.length === 1 && !force) { - throw this.usageError( - LAST_REMAINING_VERSION_ERROR - ) - } - if (!dryRun) { - await otplease(opts, opts => libunpub(spec, opts)) - } - pkgName = spec.name - pkgVersion = spec.type === 'version' ? `@${spec.rawSpec}` : '' + pkgName = manifest.name + pkgVersion = manifest.version ? `@${manifest.version}` : '' } + if (!dryRun) { + await otplease(opts, opts => libunpub(spec, opts)) + } if (!silent) { this.npm.output(`- ${pkgName}${pkgVersion}`) } diff --git a/tap-snapshots/test/lib/commands/unpublish.js.test.cjs b/tap-snapshots/test/lib/commands/unpublish.js.test.cjs deleted file mode 100644 index d84f26f299868..0000000000000 --- a/tap-snapshots/test/lib/commands/unpublish.js.test.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* 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/commands/unpublish.js TAP workspaces all workspaces --force > should output all workspaces 1`] = ` -- workspace-a- workspace-b- workspace-n -` - -exports[`test/lib/commands/unpublish.js TAP workspaces one workspace --force > should output one workspaces 1`] = ` -- workspace-a -` diff --git a/test/fixtures/tnock.js b/test/fixtures/tnock.js index c5acec510543d..8e236733fcee3 100644 --- a/test/fixtures/tnock.js +++ b/test/fixtures/tnock.js @@ -2,13 +2,15 @@ const nock = require('nock') -// TODO (other tests actually make network calls today, which is bad) -// nock.disableNetConnect() +// Uncomment this to find requests that aren't matching +// nock.emitter.on('no match', req => console.log(req.options)) module.exports = tnock -function tnock (t, host) { - const server = nock(host) +function tnock (t, host, opts) { + nock.disableNetConnect() + const server = nock(host, opts) t.teardown(function () { + nock.enableNetConnect() server.done() }) return server diff --git a/test/lib/commands/unpublish.js b/test/lib/commands/unpublish.js index b9c94e5d5265f..ce6ce82f624ed 100644 --- a/test/lib/commands/unpublish.js +++ b/test/lib/commands/unpublish.js @@ -1,262 +1,219 @@ const t = require('tap') -const { fake: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') +const tnock = require('../../fixtures/tnock.js') -let result = '' -const noop = () => null -const versions = async () => { +const pkgManifest = (name, version = '1.0.0') => { return { + _id: `${name}@${version}`, + _rev: '00-testdeadbeef', + name, versions: { - '1.0.0': {}, - '1.0.1': {}, + '1.0.0': { + name, + version: '1.0.0', + dist: { + tarball: `https://registry.npmjs.org/${name}/-/${name}-1.0.0.tgz`, + }, + }, + [version]: { + name, + version: version, + dist: { + tarball: `https://registry.npmjs.org/${name}/-/${name}-${version}.tgz`, + }, + }, }, - } -} - -const singleVersion = async () => { - return { - versions: { - '1.0.0': {}, + time: { + '1.0.0': new Date(), + [version]: new Date(), }, + 'dist-tags': { latest: version }, } } -const config = { - force: false, -} - -const testDir = t.testdir({ - 'package.json': JSON.stringify({ - name: 'pkg', - version: '1.0.0', - }, null, 2), -}) - -const npm = mockNpm({ - localPrefix: testDir, - config, - output: (...msg) => { - result += msg.join('\n') - }, -}) - -const mocks = { - libnpmaccess: { lsPackages: noop }, - libnpmpublish: { unpublish: noop }, - 'npm-registry-fetch': { json: versions }, - '../../../lib/utils/otplease.js': async (opts, fn) => fn(opts), - '../../../lib/utils/get-identity.js': async () => 'foo', - 'proc-log': { silly () {}, verbose () {} }, -} - -t.afterEach(() => { - npm.localPrefix = testDir - result = '' - config['dry-run'] = false - config.force = false - npm.config.set('loglevel', 'info') -}) +const user = 'test-user' +const pkg = 'test-package' +const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' } -t.test('no args --force', async t => { - config.force = true - - const log = { - silly (title) { - t.equal(title, 'unpublish', 'should silly log args') - }, - verbose (title, msg) { - t.equal(title, 'unpublish', 'should have expected title') - t.match( - msg, - { name: 'pkg', version: '1.0.0' }, - 'should have msg printing package.json contents' - ) +t.test('no args --force success', async t => { + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + ...auth, }, - } - - const libnpmpublish = { - unpublish (spec, opts) { - t.equal(spec.raw, 'pkg@1.0.0', 'should unpublish expected spec') - t.match( - opts, - { - publishConfig: undefined, - }, - 'should unpublish with expected opts' - ) + prefixDir: { + 'package.json': JSON.stringify({ + name: pkg, + version: '1.0.0', + }, null, 2), }, - } - - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmpublish, - 'proc-log': log, }) - const unpublish = new Unpublish(npm) - - await unpublish.exec([]) + const manifest = pkgManifest(pkg) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).reply(200, manifest) + .delete(`/${pkg}/-rev/${manifest._rev}`).reply(201) - t.equal( - result, - '- pkg@1.0.0', - 'should output removed pkg@version on success' - ) + await npm.exec('unpublish', []) + t.equal(joinedOutput(), '- test-package@1.0.0') }) t.test('no args --force missing package.json', async t => { - config.force = true - - const testDir = t.testdir({}) - npm.localPrefix = testDir - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, + const { npm } = await loadMockNpm(t, { + config: { + force: true, + }, }) - const unpublish = new Unpublish(npm) await t.rejects( - unpublish.exec([]), + npm.exec('unpublish', []), /Usage: npm unpublish/, 'should throw usage instructions on missing package.json' ) }) -t.test('no args --force unknown error reading package.json', async t => { - config.force = true - - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - 'read-package-json': (path, cb) => cb(new Error('ERR')), +t.test('no args --force error reading package.json', async t => { + const { npm } = await loadMockNpm(t, { + config: { + force: true, + }, + prefixDir: { + 'package.json': '{ not valid json ]', + }, }) - const unpublish = new Unpublish(npm) await t.rejects( - unpublish.exec([]), - /ERR/, - 'should throw unknown error from reading package.json' + npm.exec('unpublish', []), + /Failed to parse json/, + 'should throw error from reading package.json' ) }) -t.test('no args', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - }) - const unpublish = new Unpublish(npm) +t.test('no args entire project', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( - unpublish.exec([]), - /Refusing to delete entire project/, - 'should throw --force required error on no args' + npm.exec('unpublish', []), + /Usage: Refusing to delete entire project/ ) }) t.test('too many args', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - }) - const unpublish = new Unpublish(npm) + const { npm } = await loadMockNpm(t) await t.rejects( - unpublish.exec(['a', 'b']), + npm.exec('unpublish', ['a', 'b']), /Usage: npm unpublish/, 'should throw usage instructions if too many args' ) }) -t.test('unpublish @version', async t => { - const log = { - silly (title, key, value) { - t.equal(title, 'unpublish', 'should silly log args') - if (key === 'spec') { - t.match(value, { name: 'pkg', rawSpec: '1.0.0' }) - } else { - t.equal(value, 'pkg@1.0.0', 'should log originally passed arg') - } +t.test('unpublish @version not the last version', async t => { + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + ...auth, }, - } + }) + const manifest = pkgManifest(pkg, '1.0.1') + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).times(3).reply(200, manifest) + .put(`/${pkg}/-rev/${manifest._rev}`, body => { + // sets latest and deletes version 1.0.1 + return body['dist-tags'].latest === '1.0.0' && body.versions['1.0.1'] === undefined + }).reply(201) + .intercept(`/${pkg}/-/${pkg}-1.0.1.tgz/-rev/${manifest._rev}`, 'DELETE').reply(201) + + await npm.exec('unpublish', ['test-package@1.0.1']) + t.equal(joinedOutput(), '- test-package@1.0.1') +}) - const libnpmpublish = { - unpublish (spec, opts) { - t.equal(spec.raw, 'pkg@1.0.0', 'should unpublish expected parsed spec') +t.test('unpublish @version last version', async t => { + const { npm } = await loadMockNpm(t, { + config: { + ...auth, }, - } - - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmpublish, - 'proc-log': log, }) - const unpublish = new Unpublish(npm) + const manifest = pkgManifest(pkg) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).reply(200, manifest) - await unpublish.exec(['pkg@1.0.0']) - - t.equal( - result, - '- pkg@1.0.0', - 'should output removed pkg@version on success' + await t.rejects( + npm.exec('unpublish', ['test-package@1.0.0']), + /Refusing to delete the last version of the package/ ) }) t.test('no version found in package.json', async t => { - config.force = true - - const testDir = t.testdir({ - 'package.json': JSON.stringify({ - name: 'pkg', - }, null, 2), + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + ...auth, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: pkg, + }, null, 2), + }, }) - npm.localPrefix = testDir - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - }) - const unpublish = new Unpublish(npm) + const manifest = pkgManifest(pkg) - await unpublish.exec([]) - t.equal( - result, - '- pkg', - 'should output removed pkg on success' - ) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).reply(200, manifest) + .delete(`/${pkg}/-rev/${manifest._rev}`).reply(201) + + await npm.exec('unpublish', []) + t.equal(joinedOutput(), '- test-package') }) t.test('unpublish --force no version set', async t => { - config.force = true - - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + ...auth, + }, }) - const unpublish = new Unpublish(npm) - - await unpublish.exec(['pkg']) - - t.equal( - result, - '- pkg', - 'should output pkg removed' - ) + const manifest = pkgManifest(pkg) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).times(2).reply(200, manifest) + .delete(`/${pkg}/-rev/${manifest._rev}`).reply(201) + + await npm.exec('unpublish', ['test-package']) + t.equal(joinedOutput(), '- test-package') }) t.test('silent', async t => { - npm.config.set('loglevel', 'silent') - - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + loglevel: 'silent', + ...auth, + }, }) - const unpublish = new Unpublish(npm) - - await unpublish.exec(['pkg@1.0.0']) - - t.equal( - result, - '', - 'should have no output' - ) + const manifest = pkgManifest(pkg, '1.0.1') + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).times(3).reply(200, manifest) + .put(`/${pkg}/-rev/${manifest._rev}`, body => { + // sets latest and deletes version 1.0.1 + return body['dist-tags'].latest === '1.0.0' && body.versions['1.0.1'] === undefined + }).reply(201) + .delete(`/${pkg}/-/${pkg}-1.0.1.tgz/-rev/${manifest._rev}`).reply(201) + + await npm.exec('unpublish', ['test-package@1.0.1']) + t.equal(joinedOutput(), '') }) t.test('workspaces', async t => { - const testDir = t.testdir({ + const prefixDir = { 'package.json': JSON.stringify({ - name: 'my-cool-pkg', + name: pkg, version: '1.0.0', workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], }, null, 2), @@ -280,75 +237,168 @@ t.test('workspaces', async t => { version: '1.2.3-n', }), }, - }) - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - }) - const unpublish = new Unpublish(npm) + } t.test('no force', async t => { - npm.localPrefix = testDir + const { npm } = await loadMockNpm(t, { + config: { + workspaces: true, + }, + prefixDir, + }) await t.rejects( - unpublish.execWorkspaces([], []), - /--force/, - 'should require force' + npm.exec('unpublish', []), + /Usage: Refusing to delete entire project/ ) }) t.test('all workspaces --force', async t => { - npm.localPrefix = testDir - config.force = true - await unpublish.execWorkspaces([], []) - t.matchSnapshot(result, 'should output all workspaces') + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + workspaces: true, + force: true, + ...auth, + }, + prefixDir, + }) + const manifestA = pkgManifest('workspace-a') + const manifestB = pkgManifest('workspace-b') + const manifestN = pkgManifest('workspace-n') + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/workspace-a?write=true').times(2).reply(200, manifestA) + .delete(`/workspace-a/-rev/${manifestA._rev}`).reply(201) + .get('/workspace-b?write=true').times(2).reply(200, manifestB) + .delete(`/workspace-b/-rev/${manifestB._rev}`).reply(201) + .get('/workspace-n?write=true').times(2).reply(200, manifestN) + .delete(`/workspace-n/-rev/${manifestN._rev}`).reply(201) + + await npm.exec('unpublish', []) + t.equal(joinedOutput(), '- workspace-a\n- workspace-b\n- workspace-n') }) t.test('one workspace --force', async t => { - npm.localPrefix = testDir - config.force = true - await unpublish.execWorkspaces([], ['workspace-a']) - t.matchSnapshot(result, 'should output one workspaces') + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + workspace: ['workspace-a'], + force: true, + ...auth, + }, + prefixDir, + }) + const manifestA = pkgManifest('workspace-a') + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/workspace-a?write=true').times(2).reply(200, manifestA) + .delete(`/workspace-a/-rev/${manifestA._rev}`).reply(201) + + await npm.exec('unpublish', []) + t.equal(joinedOutput(), '- workspace-a') }) }) t.test('dryRun with spec', async t => { - config['dry-run'] = true - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmpublish: { unpublish: () => { - throw new Error('should not be called') - } }, + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + 'dry-run': true, + ...auth, + }, }) - const unpublish = new Unpublish(npm) - await unpublish.exec(['pkg@1.0.0']) - t.equal( - result, - '- pkg@1.0.0', - 'should output removed pkg@version on success' - ) + const manifest = pkgManifest(pkg, '1.0.1') + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get(`/${pkg}?write=true`).reply(200, manifest) + + await npm.exec('unpublish', ['test-package@1.0.1']) + t.equal(joinedOutput(), '- test-package@1.0.1') }) -t.test('dryRun with local package', async t => { - config['dry-run'] = true - config.force = true - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmpublish: { unpublish: () => { - throw new Error('should not be called') - } }, +t.test('dryRun with no args', async t => { + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + 'dry-run': true, + ...auth, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: pkg, + version: '1.0.0', + }, null, 2), + }, }) - const unpublish = new Unpublish(npm) - await unpublish.exec([]) - t.equal( - result, - '- pkg@1.0.0', - 'should output removed pkg@1.0.0 on success' - ) + + await npm.exec('unpublish', []) + t.equal(joinedOutput(), '- test-package@1.0.0') +}) + +t.test('publishConfig no spec', async t => { + const alternateRegistry = 'https://other.registry.npmjs.org' + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + 'fetch-retries': 0, + '//other.registry.npmjs.org/:_authToken': 'test-other-token', + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: pkg, + version: '1.0.0', + publishConfig: { + registry: alternateRegistry, + }, + }, null, 2), + }, + }) + + const manifest = pkgManifest(pkg) + tnock(t, alternateRegistry, + { reqheaders: { authorization: 'Bearer test-other-token' } }) + .get(`/${pkg}?write=true`).reply(200, manifest) + .delete(`/${pkg}/-rev/${manifest._rev}`).reply(201) + await npm.exec('unpublish', []) + t.equal(joinedOutput(), '- test-package@1.0.0') +}) + +t.test('publishConfig with spec', async t => { + const alternateRegistry = 'https://other.registry.npmjs.org' + const { joinedOutput, npm } = await loadMockNpm(t, { + config: { + force: true, + 'fetch-retries': 0, + '//other.registry.npmjs.org/:_authToken': 'test-other-token', + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: pkg, + version: '1.0.0', + publishConfig: { + registry: alternateRegistry, + }, + }, null, 2), + }, + }) + + const manifest = pkgManifest(pkg) + tnock(t, alternateRegistry, + { reqheaders: { authorization: 'Bearer test-other-token' } }) + .get(`/${pkg}?write=true`).reply(200, manifest) + .delete(`/${pkg}/-rev/${manifest._rev}`).reply(201) + await npm.exec('unpublish', ['test-package']) + t.equal(joinedOutput(), '- test-package') }) t.test('completion', async t => { + const { npm } = await loadMockNpm(t, { + config: { + ...auth, + }, + }) + + const unpublish = await npm.cmd('unpublish') const testComp = - async (t, { unpublish, argv, partialWord, expect, title }) => { + async (t, { argv, partialWord, expect, title }) => { const res = await unpublish.completion( { conf: { argv: { remain: argv } }, partialWord } ) @@ -356,114 +406,70 @@ t.test('completion', async t => { } t.test('completing with multiple versions from the registry', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmaccess: { - async lsPackages () { - return { - pkg: 'write', - bar: 'write', - } - }, - }, - 'npm-registry-fetch': { - async json () { - return { - versions: { - '1.0.0': {}, - '1.0.1': {}, - '2.0.0': {}, - }, - } - }, - }, - }) - const unpublish = new Unpublish(npm) + const manifest = pkgManifest(pkg, '1.0.1') + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(200, { username: user }) + .get('/-/org/test-user/package?format=cli').reply(200, { [pkg]: 'write' }) + .get(`/${pkg}?write=true`).reply(200, manifest) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], - partialWord: 'pkg', + partialWord: 'test-package', expect: [ - 'pkg@1.0.0', - 'pkg@1.0.1', - 'pkg@2.0.0', + 'test-package@1.0.0', + 'test-package@1.0.1', ], }) }) t.test('no versions retrieved', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmaccess: { - async lsPackages () { - return { - pkg: 'write', - bar: 'write', - } - }, - }, - 'npm-registry-fetch': { - async json () { - return { - versions: {}, - } - }, - }, - }) - const unpublish = new Unpublish(npm) + const manifest = pkgManifest(pkg) + manifest.versions = {} + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(200, { username: user }) + .get('/-/org/test-user/package?format=cli').reply(200, { [pkg]: 'write' }) + .get(`/${pkg}?write=true`).reply(200, manifest) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], - partialWord: 'pkg', + partialWord: pkg, expect: [ - 'pkg', + pkg, ], title: 'should autocomplete package name only', }) }) t.test('packages starting with same letters', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmaccess: { - async lsPackages () { - return { - pkg: 'write', - pkga: 'write', - pkgb: 'write', - } - }, - }, - }) - const unpublish = new Unpublish(npm) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(200, { username: user }) + .get('/-/org/test-user/package?format=cli').reply(200, { + [pkg]: 'write', + [`${pkg}a`]: 'write', + [`${pkg}b`]: 'write', + }) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], - partialWord: 'pkg', + partialWord: pkg, expect: [ - 'pkg', - 'pkga', - 'pkgb', + pkg, + `${pkg}a`, + `${pkg}b`, ], }) }) t.test('no packages retrieved', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmaccess: { - async lsPackages () { - return {} - }, - }, - }) - const unpublish = new Unpublish(npm) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(200, { username: user }) + .get('/-/org/test-user/package?format=cli').reply(200, {}) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], partialWord: 'pkg', expect: [], @@ -472,101 +478,53 @@ t.test('completion', async t => { }) t.test('no pkg name to complete', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmaccess: { - async lsPackages () { - return { - pkg: {}, - bar: {}, - } - }, - }, - }) - const unpublish = new Unpublish(npm) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(200, { username: user }) + .get('/-/org/test-user/package?format=cli').reply(200, { + [pkg]: 'write', + [`${pkg}a`]: 'write', + }) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], partialWord: undefined, - expect: ['pkg', 'bar'], + expect: [pkg, `${pkg}a`], title: 'should autocomplete with available package names from user', }) }) t.test('no pkg names retrieved from user account', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - libnpmaccess: { - async lsPackages () { - return null - }, - }, - }) - const unpublish = new Unpublish(npm) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(200, { username: user }) + .get('/-/org/test-user/package?format=cli').reply(200, null) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], - partialWord: 'pkg', + partialWord: pkg, expect: [], title: 'should have no autocomplete', }) }) t.test('logged out user', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - '../../../lib/utils/get-identity.js': () => Promise.reject(new Error('ERR')), - }) - const unpublish = new Unpublish(npm) + tnock(t, npm.config.get('registry'), + { reqheaders: { authorization: 'Bearer test-auth-token' } }) + .get('/-/whoami').reply(404) await testComp(t, { - unpublish, argv: ['npm', 'unpublish'], - partialWord: 'pkg', + partialWord: pkg, expect: [], }) }) t.test('too many args', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', mocks) - const unpublish = new Unpublish(npm) - await testComp(t, { - unpublish, argv: ['npm', 'unpublish', 'foo'], partialWord: undefined, expect: [], }) }) }) - -t.test('show error on unpublish @version with package.json and the last version', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - 'npm-registry-fetch': { json: singleVersion }, - path: { resolve: () => testDir, join: () => testDir + '/package.json' }, - }) - const unpublish = new Unpublish(npm) - await t.rejects( - unpublish.exec(['pkg@1.0.0']), - 'Refusing to delete the last version of the package. ' + - 'It will block from republishing a new version for 24 hours.\n' + - 'Run with --force to do this.' - ) -}) - -t.test('show error on unpublish @version when the last version', async t => { - const Unpublish = t.mock('../../../lib/commands/unpublish.js', { - ...mocks, - 'npm-registry-fetch': { json: singleVersion }, - }) - const unpublish = new Unpublish(npm) - await t.rejects( - unpublish.exec(['pkg@1.0.0']), - 'Refusing to delete the last version of the package. ' + - 'It will block from republishing a new version for 24 hours.\n' + - 'Run with --force to do this.' - ) -})