1const t = require('tap')
2const { load: loadMockNpm } = require('../../fixtures/mock-npm')
3
4const MockRegistry = require('@npmcli/mock-registry')
5const user = 'test-user'
6const pkg = 'test-package'
7const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' }
8
9t.test('no args --force success', async t => {
10  const { joinedOutput, npm } = await loadMockNpm(t, {
11    config: {
12      force: true,
13      ...auth,
14    },
15    prefixDir: {
16      'package.json': JSON.stringify({
17        name: pkg,
18        version: '1.0.0',
19      }, null, 2),
20    },
21  })
22
23  const registry = new MockRegistry({
24    tap: t,
25    registry: npm.config.get('registry'),
26    authorization: 'test-auth-token',
27  })
28  const manifest = registry.manifest({ name: pkg })
29  await registry.package({ manifest, query: { write: true }, times: 2 })
30  registry.unpublish({ manifest })
31  await npm.exec('unpublish', [])
32  t.equal(joinedOutput(), '- test-package')
33})
34
35t.test('no args --force missing package.json', async t => {
36  const { npm } = await loadMockNpm(t, {
37    config: {
38      force: true,
39    },
40  })
41
42  await t.rejects(
43    npm.exec('unpublish', []),
44    { code: 'EUSAGE' },
45    'should throw usage instructions on missing package.json'
46  )
47})
48
49t.test('no args --force error reading package.json', async t => {
50  const { npm } = await loadMockNpm(t, {
51    config: {
52      force: true,
53    },
54    prefixDir: {
55      'package.json': '{ not valid json ]',
56    },
57  })
58
59  await t.rejects(
60    npm.exec('unpublish', []),
61    /Invalid package.json/,
62    'should throw error from reading package.json'
63  )
64})
65
66t.test('with args --force error reading package.json', async t => {
67  const { npm } = await loadMockNpm(t, {
68    config: {
69      force: true,
70    },
71    prefixDir: {
72      'package.json': '{ not valid json ]',
73    },
74  })
75
76  await t.rejects(
77    npm.exec('unpublish', [pkg]),
78    /Invalid package.json/,
79    'should throw error from reading package.json'
80  )
81})
82
83t.test('no force entire project', async t => {
84  const { npm } = await loadMockNpm(t)
85
86  await t.rejects(
87    npm.exec('unpublish', ['@npmcli/unpublish-test']),
88    /Refusing to delete entire project/
89  )
90})
91
92t.test('too many args', async t => {
93  const { npm } = await loadMockNpm(t)
94
95  await t.rejects(
96    npm.exec('unpublish', ['a', 'b']),
97    { code: 'EUSAGE' },
98    'should throw usage instructions if too many args'
99  )
100})
101
102t.test('range', async t => {
103  const { npm } = await loadMockNpm(t)
104
105  await t.rejects(
106    npm.exec('unpublish', ['a@>1.0.0']),
107    { code: 'EUSAGE' },
108    /single version/
109  )
110})
111
112t.test('tag', async t => {
113  const { npm } = await loadMockNpm(t)
114
115  await t.rejects(
116    npm.exec('unpublish', ['a@>1.0.0']),
117    { code: 'EUSAGE' },
118    /single version/
119  )
120})
121
122t.test('unpublish <pkg>@version not the last version', async t => {
123  const { joinedOutput, npm } = await loadMockNpm(t, {
124    config: {
125      force: true,
126      ...auth,
127    },
128  })
129  const registry = new MockRegistry({
130    tap: t,
131    registry: npm.config.get('registry'),
132    authorization: 'test-auth-token',
133  })
134  const manifest = registry.manifest({
135    name: pkg,
136    packuments: ['1.0.0', '1.0.1'],
137  })
138  await registry.package({ manifest, query: { write: true }, times: 3 })
139  registry.nock.put(`/${pkg}/-rev/${manifest._rev}`, body => {
140    // sets latest and deletes version 1.0.1
141    return body['dist-tags'].latest === '1.0.0' && body.versions['1.0.1'] === undefined
142  }).reply(201)
143    .intercept(`/${pkg}/-/${pkg}-1.0.1.tgz/-rev/${manifest._rev}`, 'DELETE').reply(201)
144
145  await npm.exec('unpublish', ['test-package@1.0.1'])
146  t.equal(joinedOutput(), '- test-package@1.0.1')
147})
148
149t.test('unpublish <pkg>@version last version', async t => {
150  const { npm } = await loadMockNpm(t, {
151    config: {
152      ...auth,
153    },
154  })
155  const registry = new MockRegistry({
156    tap: t,
157    registry: npm.config.get('registry'),
158    authorization: 'test-auth-token',
159  })
160  const manifest = registry.manifest({ name: pkg })
161  await registry.package({ manifest, query: { write: true } })
162
163  await t.rejects(
164    npm.exec('unpublish', ['test-package@1.0.0']),
165    /Refusing to delete the last version of the package/
166  )
167})
168
169t.test('no version found in package.json no force', async t => {
170  const { npm } = await loadMockNpm(t, {
171    config: {
172      ...auth,
173    },
174    prefixDir: {
175      'package.json': JSON.stringify({
176        name: pkg,
177      }, null, 2),
178    },
179  })
180  await t.rejects(
181    npm.exec('unpublish', []),
182    /Refusing to delete entire project/
183  )
184})
185
186t.test('no version found in package.json with force', async t => {
187  const { joinedOutput, npm } = await loadMockNpm(t, {
188    config: {
189      force: true,
190      ...auth,
191    },
192    prefixDir: {
193      'package.json': JSON.stringify({
194        name: pkg,
195      }, null, 2),
196    },
197  })
198  const registry = new MockRegistry({
199    tap: t,
200    registry: npm.config.get('registry'),
201    authorization: 'test-auth-token',
202  })
203  const manifest = registry.manifest({ name: pkg })
204  await registry.package({ manifest, query: { write: true }, times: 2 })
205  registry.unpublish({ manifest })
206
207  await npm.exec('unpublish', [])
208  t.equal(joinedOutput(), '- test-package')
209})
210
211t.test('unpublish <pkg> --force no version set', async t => {
212  const { joinedOutput, npm } = await loadMockNpm(t, {
213    config: {
214      force: true,
215      ...auth,
216    },
217  })
218  const registry = new MockRegistry({
219    tap: t,
220    registry: npm.config.get('registry'),
221    authorization: 'test-auth-token',
222  })
223  const manifest = registry.manifest({ name: pkg })
224  await registry.package({ manifest, query: { write: true }, times: 2 })
225  registry.unpublish({ manifest })
226
227  await npm.exec('unpublish', ['test-package'])
228  t.equal(joinedOutput(), '- test-package')
229})
230
231t.test('silent', async t => {
232  const { joinedOutput, npm } = await loadMockNpm(t, {
233    config: {
234      force: true,
235      loglevel: 'silent',
236      ...auth,
237    },
238  })
239  const registry = new MockRegistry({
240    tap: t,
241    registry: npm.config.get('registry'),
242    authorization: 'test-auth-token',
243  })
244  const manifest = registry.manifest({
245    name: pkg,
246    packuments: ['1.0.0', '1.0.1'],
247  })
248  await registry.package({ manifest, query: { write: true }, times: 3 })
249  registry.nock.put(`/${pkg}/-rev/${manifest._rev}`, body => {
250    // sets latest and deletes version 1.0.1
251    return body['dist-tags'].latest === '1.0.0' && body.versions['1.0.1'] === undefined
252  }).reply(201)
253    .delete(`/${pkg}/-/${pkg}-1.0.1.tgz/-rev/${manifest._rev}`).reply(201)
254
255  await npm.exec('unpublish', ['test-package@1.0.1'])
256  t.equal(joinedOutput(), '')
257})
258
259t.test('workspaces', async t => {
260  const prefixDir = {
261    'package.json': JSON.stringify({
262      name: pkg,
263      version: '1.0.0',
264      workspaces: ['workspace-a', 'workspace-b', 'workspace-c'],
265    }, null, 2),
266    'workspace-a': {
267      'package.json': JSON.stringify({
268        name: 'workspace-a',
269        version: '1.2.3-a',
270        repository: 'http://repo.workspace-a/',
271      }),
272    },
273    'workspace-b': {
274      'package.json': JSON.stringify({
275        name: 'workspace-b',
276        version: '1.2.3-b',
277        repository: 'https://github.com/npm/workspace-b',
278      }),
279    },
280    'workspace-c': {
281      'package.json': JSON.stringify({
282        name: 'workspace-n',
283        version: '1.2.3-n',
284      }),
285    },
286  }
287
288  t.test('with package name no force', async t => {
289    const { npm } = await loadMockNpm(t, {
290      config: {
291        workspace: ['workspace-a'],
292      },
293      prefixDir,
294    })
295    await t.rejects(
296      npm.exec('unpublish', ['workspace-a']),
297      /Refusing to delete entire project/
298    )
299  })
300
301  t.test('all workspaces last version --force', async t => {
302    const { joinedOutput, npm } = await loadMockNpm(t, {
303      config: {
304        workspaces: true,
305        force: true,
306        ...auth,
307      },
308      prefixDir,
309    })
310    const registry = new MockRegistry({
311      tap: t,
312      registry: npm.config.get('registry'),
313      authorization: 'test-auth-token',
314    })
315    const manifestA = registry.manifest({ name: 'workspace-a', versions: ['1.2.3-a'] })
316    const manifestB = registry.manifest({ name: 'workspace-b', versions: ['1.2.3-b'] })
317    const manifestN = registry.manifest({ name: 'workspace-n', versions: ['1.2.3-n'] })
318    await registry.package({ manifest: manifestA, query: { write: true }, times: 2 })
319    await registry.package({ manifest: manifestB, query: { write: true }, times: 2 })
320    await registry.package({ manifest: manifestN, query: { write: true }, times: 2 })
321    registry.nock.delete(`/workspace-a/-rev/${manifestA._rev}`).reply(201)
322      .delete(`/workspace-b/-rev/${manifestB._rev}`).reply(201)
323      .delete(`/workspace-n/-rev/${manifestN._rev}`).reply(201)
324
325    await npm.exec('unpublish', [])
326    t.equal(joinedOutput(), '- workspace-a\n- workspace-b\n- workspace-n')
327  })
328})
329
330t.test('dryRun with spec', async t => {
331  const { joinedOutput, npm } = await loadMockNpm(t, {
332    config: {
333      'dry-run': true,
334      ...auth,
335    },
336  })
337  const registry = new MockRegistry({
338    tap: t,
339    registry: npm.config.get('registry'),
340    authorization: 'test-auth-token',
341  })
342  const manifest = registry.manifest({
343    name: pkg,
344    packuments: ['1.0.0', '1.0.1'],
345  })
346  await registry.package({ manifest, query: { write: true } })
347
348  await npm.exec('unpublish', ['test-package@1.0.1'])
349  t.equal(joinedOutput(), '- test-package@1.0.1')
350})
351
352t.test('dryRun with no args', async t => {
353  const { joinedOutput, npm } = await loadMockNpm(t, {
354    config: {
355      force: true,
356      'dry-run': true,
357      ...auth,
358    },
359    prefixDir: {
360      'package.json': JSON.stringify({
361        name: pkg,
362        version: '1.0.0',
363      }, null, 2),
364    },
365  })
366  const registry = new MockRegistry({
367    tap: t,
368    registry: npm.config.get('registry'),
369    authorization: 'test-auth-token',
370  })
371  const manifest = registry.manifest({
372    name: pkg,
373    packuments: ['1.0.0', '1.0.1'],
374  })
375  await registry.package({ manifest, query: { write: true } })
376
377  await npm.exec('unpublish', [])
378  t.equal(joinedOutput(), '- test-package@1.0.0')
379})
380
381t.test('publishConfig no spec', async t => {
382  const alternateRegistry = 'https://other.registry.npmjs.org'
383  const { joinedOutput, npm } = await loadMockNpm(t, {
384    config: {
385      force: true,
386      '//other.registry.npmjs.org/:_authToken': 'test-other-token',
387    },
388    prefixDir: {
389      'package.json': JSON.stringify({
390        name: pkg,
391        version: '1.0.0',
392        publishConfig: {
393          registry: alternateRegistry,
394        },
395      }, null, 2),
396    },
397  })
398
399  const registry = new MockRegistry({
400    tap: t,
401    registry: alternateRegistry,
402    authorization: 'test-other-token',
403  })
404  const manifest = registry.manifest({ name: pkg })
405  await registry.package({ manifest, query: { write: true }, times: 2 })
406  registry.unpublish({ manifest })
407  await npm.exec('unpublish', [])
408  t.equal(joinedOutput(), '- test-package')
409})
410
411t.test('publishConfig with spec', async t => {
412  const alternateRegistry = 'https://other.registry.npmjs.org'
413  const { joinedOutput, npm } = await loadMockNpm(t, {
414    config: {
415      force: true,
416      '//other.registry.npmjs.org/:_authToken': 'test-other-token',
417    },
418    prefixDir: {
419      'package.json': JSON.stringify({
420        name: pkg,
421        version: '1.0.0',
422        publishConfig: {
423          registry: alternateRegistry,
424        },
425      }, null, 2),
426    },
427  })
428
429  const registry = new MockRegistry({
430    tap: t,
431    registry: alternateRegistry,
432    authorization: 'test-other-token',
433  })
434  const manifest = registry.manifest({ name: pkg })
435  await registry.package({ manifest, query: { write: true }, times: 2 })
436  registry.unpublish({ manifest })
437  await npm.exec('unpublish', ['test-package'])
438  t.equal(joinedOutput(), '- test-package')
439})
440
441t.test('scoped registry config', async t => {
442  const scopedPkg = `@npm/test-package`
443  const alternateRegistry = 'https://other.registry.npmjs.org'
444  const { npm } = await loadMockNpm(t, {
445    config: {
446      force: true,
447      '@npm:registry': alternateRegistry,
448      '//other.registry.npmjs.org/:_authToken': 'test-other-token',
449    },
450    prefixDir: {
451      'package.json': JSON.stringify({
452        name: pkg,
453        version: '1.0.0',
454        publishConfig: {
455          registry: alternateRegistry,
456        },
457      }, null, 2),
458    },
459  })
460  const registry = new MockRegistry({
461    tap: t,
462    registry: alternateRegistry,
463    authorization: 'test-other-token',
464  })
465  const manifest = registry.manifest({ name: scopedPkg })
466  await registry.package({ manifest, query: { write: true }, times: 2 })
467  registry.unpublish({ manifest })
468  await npm.exec('unpublish', [scopedPkg])
469})
470
471t.test('completion', async t => {
472  const { npm, unpublish } = await loadMockNpm(t, {
473    command: 'unpublish',
474    config: {
475      ...auth,
476    },
477  })
478
479  const testComp =
480    async (t, { argv, partialWord, expect, title }) => {
481      const res = await unpublish.completion(
482        { conf: { argv: { remain: argv } }, partialWord }
483      )
484      t.strictSame(res, expect, title || argv.join(' '))
485    }
486
487  t.test('completing with multiple versions from the registry', async t => {
488    const registry = new MockRegistry({
489      tap: t,
490      registry: npm.config.get('registry'),
491      authorization: 'test-auth-token',
492    })
493    const manifest = registry.manifest({
494      name: pkg,
495      packuments: ['1.0.0', '1.0.1'],
496    })
497    await registry.package({ manifest, query: { write: true } })
498    registry.whoami({ username: user })
499    registry.getPackages({ team: user, packages: { [pkg]: 'write' } })
500
501    await testComp(t, {
502      argv: ['npm', 'unpublish'],
503      partialWord: 'test-package',
504      expect: [
505        'test-package@1.0.0',
506        'test-package@1.0.1',
507      ],
508    })
509  })
510
511  t.test('no versions retrieved', async t => {
512    const registry = new MockRegistry({
513      tap: t,
514      registry: npm.config.get('registry'),
515      authorization: 'test-auth-token',
516    })
517    const manifest = registry.manifest({ name: pkg })
518    manifest.versions = {}
519    await registry.package({ manifest, query: { write: true } })
520    registry.whoami({ username: user })
521    registry.getPackages({ team: user, packages: { [pkg]: 'write' } })
522
523    await testComp(t, {
524      argv: ['npm', 'unpublish'],
525      partialWord: pkg,
526      expect: [
527        pkg,
528      ],
529      title: 'should autocomplete package name only',
530    })
531  })
532
533  t.test('packages starting with same letters', async t => {
534    const registry = new MockRegistry({
535      tap: t,
536      registry: npm.config.get('registry'),
537      authorization: 'test-auth-token',
538    })
539    registry.whoami({ username: user })
540    registry.getPackages({ team: user,
541      packages: {
542        [pkg]: 'write',
543        [`${pkg}a`]: 'write',
544        [`${pkg}b`]: 'write',
545      } })
546
547    await testComp(t, {
548      argv: ['npm', 'unpublish'],
549      partialWord: pkg,
550      expect: [
551        pkg,
552        `${pkg}a`,
553        `${pkg}b`,
554      ],
555    })
556  })
557
558  t.test('no packages retrieved', async t => {
559    const registry = new MockRegistry({
560      tap: t,
561      registry: npm.config.get('registry'),
562      authorization: 'test-auth-token',
563    })
564    registry.whoami({ username: user })
565    registry.getPackages({ team: user, packages: {} })
566
567    await testComp(t, {
568      argv: ['npm', 'unpublish'],
569      partialWord: 'pkg',
570      expect: [],
571      title: 'should have no autocompletion',
572    })
573  })
574
575  t.test('no pkg name to complete', async t => {
576    const registry = new MockRegistry({
577      tap: t,
578      registry: npm.config.get('registry'),
579      authorization: 'test-auth-token',
580    })
581    registry.whoami({ username: user })
582    registry.getPackages({ team: user,
583      packages: {
584        [pkg]: 'write',
585        [`${pkg}a`]: 'write',
586      } })
587
588    await testComp(t, {
589      argv: ['npm', 'unpublish'],
590      partialWord: undefined,
591      expect: [pkg, `${pkg}a`],
592      title: 'should autocomplete with available package names from user',
593    })
594  })
595
596  t.test('logged out user', async t => {
597    const registry = new MockRegistry({
598      tap: t,
599      registry: npm.config.get('registry'),
600      authorization: 'test-auth-token',
601    })
602    registry.whoami({ responseCode: 404 })
603
604    await testComp(t, {
605      argv: ['npm', 'unpublish'],
606      partialWord: pkg,
607      expect: [],
608    })
609  })
610
611  t.test('too many args', async t => {
612    await testComp(t, {
613      argv: ['npm', 'unpublish', 'foo'],
614      partialWord: undefined,
615      expect: [],
616    })
617  })
618})
619