From 1c9d594a7f833de585920a15588373243947e443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Wed, 29 Aug 2018 17:34:56 -0700 Subject: [PATCH] feat(unpublish): add new api with unpublish support --- index.js | 4 + publish.js | 1 + test/unpublish.js | 249 +++++++++++++++++++++++++++++++++++++++++++++ test/util/tnock.js | 12 +++ unpublish.js | 82 +++++++++++++++ 5 files changed, 348 insertions(+) create mode 100644 index.js create mode 100644 publish.js create mode 100644 test/unpublish.js create mode 100644 test/util/tnock.js create mode 100644 unpublish.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..38de9ec --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +module.exports = { + publish: require('./publish.js'), + unpublish: require('./unpublish.js') +} diff --git a/publish.js b/publish.js new file mode 100644 index 0000000..ccacec3 --- /dev/null +++ b/publish.js @@ -0,0 +1 @@ +'use strict' diff --git a/test/unpublish.js b/test/unpublish.js new file mode 100644 index 0000000..8013a64 --- /dev/null +++ b/test/unpublish.js @@ -0,0 +1,249 @@ +'use strict' + +const figgyPudding = require('figgy-pudding') +const test = require('tap').test +const tnock = require('./util/tnock.js') + +const OPTS = figgyPudding({ registry: {} })({ + registry: 'https://mock.reg/' +}) + +const REG = OPTS.registry +const REV = '72-47f2986bfd8e8b55068b204588bbf484' +const unpub = require('../unpublish.js') + +test('basic test', t => { + const doc = { + _id: 'foo', + _rev: REV, + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + } + } + } + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(200, doc) + srv.delete(`/foo/-rev/${REV}`).reply(201) + return unpub('foo', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('scoped basic test', t => { + const doc = { + _id: '@foo/bar', + _rev: REV, + name: '@foo/bar', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: '@foo/bar', + dist: { + tarball: `${REG}/@foo/bar/-/foo-1.0.0.tgz` + } + } + } + } + const srv = tnock(t, REG) + srv.get('/@foo%2fbar?write=true').reply(200, doc) + srv.delete(`/@foo%2fbar/-rev/${REV}`).reply(201) + return unpub('@foo/bar', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('unpublish specific, last version', t => { + const doc = { + _id: 'foo', + _rev: REV, + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + } + } + } + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(200, doc) + srv.delete(`/foo/-rev/${REV}`).reply(201) + return unpub('foo@1.0.0', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('unpublish specific version', t => { + const doc = { + _id: 'foo', + _rev: REV, + _revisions: [1, 2, 3], + _attachments: [1, 2, 3], + name: 'foo', + 'dist-tags': { + latest: '1.0.1' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + }, + '1.0.1': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.1.tgz` + } + } + } + } + const postEdit = { + _id: 'foo', + _rev: REV, + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + } + } + } + + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(200, doc) + srv.put(`/foo/-rev/${REV}`, postEdit).reply(200) + srv.get('/foo?write=true').reply(200, postEdit) + srv.delete(`/foo/-/foo-1.0.1.tgz/-rev/${REV}`).reply(200) + return unpub('foo@1.0.1', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('404 considered a success', t => { + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(404) + return unpub('foo', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('non-404 errors', t => { + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(500) + return unpub('foo', OPTS).then( + () => { throw new Error('should not have succeeded') }, + err => { t.equal(err.code, 'E500', 'got right error from server') } + ) +}) + +test('packument with missing versions unpublishes whole thing', t => { + const doc = { + _id: 'foo', + _rev: REV, + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + } + } + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(200, doc) + srv.delete(`/foo/-rev/${REV}`).reply(201) + return unpub('foo@1.0.0', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('packument with missing specific version assumed unpublished', t => { + const doc = { + _id: 'foo', + _rev: REV, + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + } + } + } + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(200, doc) + return unpub('foo@1.0.1', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) + +test('unpublish specific version without dist-tag update', t => { + const doc = { + _id: 'foo', + _rev: REV, + _revisions: [1, 2, 3], + _attachments: [1, 2, 3], + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + }, + '1.0.1': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.1.tgz` + } + } + } + } + const postEdit = { + _id: 'foo', + _rev: REV, + name: 'foo', + 'dist-tags': { + latest: '1.0.0' + }, + versions: { + '1.0.0': { + name: 'foo', + dist: { + tarball: `${REG}/foo/-/foo-1.0.0.tgz` + } + } + } + } + const srv = tnock(t, REG) + srv.get('/foo?write=true').reply(200, doc) + srv.put(`/foo/-rev/${REV}`, postEdit).reply(200) + srv.get('/foo?write=true').reply(200, postEdit) + srv.delete(`/foo/-/foo-1.0.1.tgz/-rev/${REV}`).reply(200) + return unpub('foo@1.0.1', OPTS).then(() => { + t.ok(true, 'foo was unpublished') + }) +}) diff --git a/test/util/tnock.js b/test/util/tnock.js new file mode 100644 index 0000000..00b6e16 --- /dev/null +++ b/test/util/tnock.js @@ -0,0 +1,12 @@ +'use strict' + +const nock = require('nock') + +module.exports = tnock +function tnock (t, host) { + const server = nock(host) + t.tearDown(function () { + server.done() + }) + return server +} diff --git a/unpublish.js b/unpublish.js new file mode 100644 index 0000000..bb25095 --- /dev/null +++ b/unpublish.js @@ -0,0 +1,82 @@ +'use strict' + +const figgyPudding = require('figgy-pudding') +const npa = require('npm-package-arg') +const npmFetch = require('npm-registry-fetch') +const semver = require('semver') +const url = require('url') + +const UnpublishConfig = figgyPudding({ + force: { default: false }, + Promise: { default: () => Promise } +}) + +module.exports = unpublish +function unpublish (spec, opts) { + opts = UnpublishConfig(opts) + spec = npa(spec) + const pkgUri = spec.escapedName + return npmFetch.json(pkgUri, opts.concat({ + query: { write: true } + })).then(pkg => { + if (!spec.rawSpec || spec.rawSpec === '*') { + return npmFetch(`${pkgUri}/-rev/${pkg._rev}`, opts.concat({ + method: 'DELETE', + ignoreBody: true + })) + } else { + const version = spec.rawSpec + const allVersions = pkg.versions || {} + const versionPublic = allVersions[version] + let dist + if (versionPublic) { + dist = allVersions[version].dist + } + delete allVersions[version] + // if it was the only version, then delete the whole package. + if (!Object.keys(allVersions).length) { + return npmFetch(`${pkgUri}/-rev/${pkg._rev}`, opts.concat({ + method: 'DELETE', + ignoreBody: true + })) + } else if (versionPublic) { + const latestVer = pkg['dist-tags'].latest + Object.keys(pkg['dist-tags']).forEach(tag => { + if (pkg['dist-tags'][tag] === version) { + delete pkg['dist-tags'][tag] + } + }) + + if (latestVer === version) { + pkg['dist-tags'].latest = Object.keys( + allVersions + ).sort(semver.compareLoose).pop() + } + + delete pkg._revisions + delete pkg._attachments + // Update packument with removed versions + return npmFetch(`${pkgUri}/-rev/${pkg._rev}`, opts.concat({ + method: 'PUT', + body: pkg, + ignoreBody: true + })).then(() => { + // Remove the tarball itself + return npmFetch.json(pkgUri, opts.concat({ + query: { write: true } + })).then(({ _rev, _id }) => { + const tarballUrl = url.parse(dist.tarball).pathname.substr(1) + return npmFetch(`${tarballUrl}/-rev/${_rev}`, opts.concat({ + method: 'DELETE', + ignoreBody: true + })) + }) + }) + } + } + }, err => { + if (err.code !== 'E404') { + throw err + } + }) +}