1const t = require('tap')
2const { load: loadMockNpm } = require('../../fixtures/mock-npm.js')
3const MockRegistry = require('@npmcli/mock-registry')
4
5const path = require('path')
6const npa = require('npm-package-arg')
7const packageName = '@npmcli/test-package'
8const spec = npa(packageName)
9const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' }
10
11const maintainers = [
12  { email: 'test-user-a@npmjs.org', name: 'test-user-a' },
13  { email: 'test-user-b@npmjs.org', name: 'test-user-b' },
14]
15
16const workspaceFixture = {
17  'package.json': JSON.stringify({
18    name: packageName,
19    version: '1.2.3-test',
20    workspaces: ['workspace-a', 'workspace-b', 'workspace-c'],
21  }),
22  'workspace-a': {
23    'package.json': JSON.stringify({
24      name: 'workspace-a',
25      version: '1.2.3-a',
26    }),
27  },
28  'workspace-b': {
29    'package.json': JSON.stringify({
30      name: 'workspace-b',
31      version: '1.2.3-n',
32    }),
33  },
34  'workspace-c': JSON.stringify({
35    'package.json': {
36      name: 'workspace-n',
37      version: '1.2.3-n',
38    },
39  }),
40}
41
42function registryPackage (t, registry, name) {
43  const mockRegistry = new MockRegistry({ tap: t, registry })
44
45  const manifest = mockRegistry.manifest({
46    name,
47    packuments: [{ maintainers, version: '1.0.0' }],
48  })
49  return mockRegistry.package({ manifest })
50}
51
52t.test('owner no args', async t => {
53  const { npm } = await loadMockNpm(t)
54  await t.rejects(
55    npm.exec('owner', []),
56    { code: 'EUSAGE' },
57    'rejects with usage'
58  )
59})
60
61t.test('owner ls no args', async t => {
62  const { npm, joinedOutput } = await loadMockNpm(t, {
63    prefixDir: {
64      'package.json': JSON.stringify({ name: packageName }),
65    },
66  })
67  const registry = new MockRegistry({
68    tap: t,
69    registry: npm.config.get('registry'),
70  })
71
72  const manifest = registry.manifest({
73    name: packageName,
74    packuments: [{ maintainers, version: '1.0.0' }],
75  })
76  await registry.package({ manifest })
77
78  await npm.exec('owner', ['ls'])
79  t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
80})
81
82t.test('local package.json has no name', async t => {
83  const { npm } = await loadMockNpm(t, {
84    prefixDir: {
85      'package.json': JSON.stringify({ hello: 'world' }),
86    },
87  })
88  await t.rejects(
89    npm.exec('owner', ['ls']),
90    { code: 'EUSAGE' }
91  )
92})
93
94t.test('owner ls global', async t => {
95  const { npm } = await loadMockNpm(t, {
96    config: { global: true },
97  })
98
99  await t.rejects(
100    npm.exec('owner', ['ls']),
101    { code: 'EUSAGE' },
102    'rejects with usage'
103  )
104})
105
106t.test('owner ls no args no cwd package', async t => {
107  const { npm } = await loadMockNpm(t)
108
109  await t.rejects(
110    npm.exec('owner', ['ls'])
111  )
112})
113
114t.test('owner ls fails to retrieve packument', async t => {
115  const { npm, logs } = await loadMockNpm(t, {
116    prefixDir: {
117      'package.json': JSON.stringify({ name: packageName }),
118    },
119  })
120  const registry = new MockRegistry({
121    tap: t,
122    registry: npm.config.get('registry'),
123  })
124  registry.nock.get(`/${spec.escapedName}`).reply(404)
125  await t.rejects(npm.exec('owner', ['ls']))
126  t.match(logs.error, [['owner ls', "Couldn't get owner data", '@npmcli/test-package']])
127})
128
129t.test('owner ls <pkg>', async t => {
130  const { npm, joinedOutput } = await loadMockNpm(t)
131  const registry = new MockRegistry({
132    tap: t,
133    registry: npm.config.get('registry'),
134  })
135
136  const manifest = registry.manifest({
137    name: packageName,
138    packuments: [{ maintainers, version: '1.0.0' }],
139  })
140  await registry.package({ manifest })
141
142  await npm.exec('owner', ['ls', packageName])
143  t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
144})
145
146t.test('owner ls <pkg> no maintainers', async t => {
147  const { npm, joinedOutput } = await loadMockNpm(t)
148  const registry = new MockRegistry({
149    tap: t,
150    registry: npm.config.get('registry'),
151  })
152  const manifest = registry.manifest({
153    name: packageName,
154    versions: ['1.0.0'],
155  })
156  await registry.package({ manifest })
157
158  await npm.exec('owner', ['ls', packageName])
159  t.equal(joinedOutput(), 'no admin found')
160})
161
162t.test('owner add <user> <pkg>', async t => {
163  const { npm, joinedOutput } = await loadMockNpm(t, {
164    config: { ...auth },
165  })
166  const username = 'foo'
167  const registry = new MockRegistry({
168    tap: t,
169    registry: npm.config.get('registry'),
170  })
171  const manifest = registry.manifest({
172    name: packageName,
173    packuments: [{ maintainers, version: '1.0.0' }],
174  })
175  registry.couchuser({ username })
176  await registry.package({ manifest })
177  registry.nock.put(`/${spec.escapedName}/-rev/${manifest._rev}`, body => {
178    t.match(body, {
179      _id: manifest._id,
180      _rev: manifest._rev,
181      maintainers: [
182        ...manifest.maintainers,
183        { name: username, email: '' },
184      ],
185    })
186    return true
187  }).reply(200, {})
188  await npm.exec('owner', ['add', username, packageName])
189  t.equal(joinedOutput(), `+ ${username} (${packageName})`)
190})
191
192t.test('owner add <user> cwd package', async t => {
193  const { npm, joinedOutput } = await loadMockNpm(t, {
194    prefixDir: {
195      'package.json': JSON.stringify({ name: packageName }),
196    },
197    config: { ...auth },
198  })
199  const username = 'foo'
200  const registry = new MockRegistry({
201    tap: t,
202    registry: npm.config.get('registry'),
203  })
204  const manifest = registry.manifest({
205    name: packageName,
206    packuments: [{ maintainers, version: '1.0.0' }],
207  })
208  registry.couchuser({ username })
209  await registry.package({ manifest })
210  registry.nock.put(`/${spec.escapedName}/-rev/${manifest._rev}`, body => {
211    t.match(body, {
212      _id: manifest._id,
213      _rev: manifest._rev,
214      maintainers: [
215        ...manifest.maintainers,
216        { name: username, email: '' },
217      ],
218    })
219    return true
220  }).reply(200, {})
221  await npm.exec('owner', ['add', username])
222  t.equal(joinedOutput(), `+ ${username} (${packageName})`)
223})
224
225t.test('owner add <user> <pkg> already an owner', async t => {
226  const { npm, joinedOutput, logs } = await loadMockNpm(t, {
227    config: { ...auth },
228  })
229  const username = maintainers[0].name
230  const registry = new MockRegistry({
231    tap: t,
232    registry: npm.config.get('registry'),
233  })
234  const manifest = registry.manifest({
235    name: packageName,
236    packuments: [{ maintainers, version: '1.0.0' }],
237  })
238  registry.couchuser({ username })
239  await registry.package({ manifest })
240  await npm.exec('owner', ['add', username, packageName])
241  t.equal(joinedOutput(), '')
242  t.match(
243    logs.info,
244    [['owner add', 'Already a package owner: test-user-a <test-user-a@npmjs.org>']]
245  )
246})
247
248t.test('owner add <user> <pkg> fails to retrieve user', async t => {
249  const { npm, logs } = await loadMockNpm(t, {
250    config: { ...auth },
251  })
252  const username = 'foo'
253  const registry = new MockRegistry({
254    tap: t,
255    registry: npm.config.get('registry'),
256  })
257  registry.couchuser({ username, responseCode: 404, body: {} })
258  await t.rejects(npm.exec('owner', ['add', username, packageName]))
259  t.match(logs.error, [['owner mutate', `Error getting user data for ${username}`]])
260})
261
262t.test('owner add <user> <pkg> fails to PUT updates', async t => {
263  const { npm } = await loadMockNpm(t, {
264    config: { ...auth },
265  })
266  const username = 'foo'
267  const registry = new MockRegistry({
268    tap: t,
269    registry: npm.config.get('registry'),
270  })
271  const manifest = registry.manifest({
272    name: packageName,
273    packuments: [{ maintainers, version: '1.0.0' }],
274  })
275  registry.couchuser({ username })
276  await registry.package({ manifest })
277  registry.nock.put(`/${spec.escapedName}/-rev/${manifest._rev}`).reply(404, {})
278  await t.rejects(
279    npm.exec('owner', ['add', username, packageName]),
280    { code: 'EOWNERMUTATE' }
281  )
282})
283
284t.test('owner add <user> <pkg> no previous maintainers property from server', async t => {
285  const { npm, joinedOutput } = await loadMockNpm(t, {
286    config: { ...auth },
287  })
288  const username = 'foo'
289  const registry = new MockRegistry({
290    tap: t,
291    registry: npm.config.get('registry'),
292  })
293  const manifest = registry.manifest({
294    name: packageName,
295    packuments: [{ maintainers: undefined, version: '1.0.0' }],
296  })
297  registry.couchuser({ username })
298  await registry.package({ manifest })
299  registry.nock.put(`/${spec.escapedName}/-rev/${manifest._rev}`, body => {
300    t.match(body, {
301      _id: manifest._id,
302      _rev: manifest._rev,
303      maintainers: [{ name: username, email: '' }],
304    })
305    return true
306  }).reply(200, {})
307  await npm.exec('owner', ['add', username, packageName])
308  t.equal(joinedOutput(), `+ ${username} (${packageName})`)
309})
310
311t.test('owner add no user', async t => {
312  const { npm } = await loadMockNpm(t)
313
314  await t.rejects(
315    npm.exec('owner', ['add']),
316    { code: 'EUSAGE' }
317  )
318})
319
320t.test('owner add <user> no pkg global', async t => {
321  const { npm } = await loadMockNpm(t, {
322    config: { global: true },
323  })
324
325  await t.rejects(
326    npm.exec('owner', ['add', 'foo']),
327    { code: 'EUSAGE' }
328  )
329})
330
331t.test('owner add <user> no cwd package', async t => {
332  const { npm } = await loadMockNpm(t)
333
334  await t.rejects(
335    npm.exec('owner', ['add', 'foo']),
336    { code: 'EUSAGE' }
337  )
338})
339
340t.test('owner rm <user> <pkg>', async t => {
341  const { npm, joinedOutput } = await loadMockNpm(t, {
342    config: { ...auth },
343  })
344  const username = maintainers[0].name
345  const registry = new MockRegistry({
346    tap: t,
347    registry: npm.config.get('registry'),
348  })
349  const manifest = registry.manifest({
350    name: packageName,
351    packuments: [{ maintainers, version: '1.0.0' }],
352  })
353  registry.couchuser({ username })
354  await registry.package({ manifest })
355  registry.nock.put(`/${spec.escapedName}/-rev/${manifest._rev}`, body => {
356    t.match(body, {
357      _id: manifest._id,
358      _rev: manifest._rev,
359      maintainers: maintainers.slice(1),
360    })
361    return true
362  }).reply(200, {})
363  await npm.exec('owner', ['rm', username, packageName])
364  t.equal(joinedOutput(), `- ${username} (${packageName})`)
365})
366
367t.test('owner rm <user> <pkg> not a current owner', async t => {
368  const { npm, logs } = await loadMockNpm(t, {
369    config: { ...auth },
370  })
371  const username = 'foo'
372  const registry = new MockRegistry({
373    tap: t,
374    registry: npm.config.get('registry'),
375  })
376  const manifest = registry.manifest({
377    name: packageName,
378    packuments: [{ maintainers, version: '1.0.0' }],
379  })
380  registry.couchuser({ username })
381  await registry.package({ manifest })
382  await npm.exec('owner', ['rm', username, packageName])
383  t.match(logs.info, [['owner rm', `Not a package owner: ${username}`]])
384})
385
386t.test('owner rm <user> cwd package', async t => {
387  const { npm, joinedOutput } = await loadMockNpm(t, {
388    prefixDir: {
389      'package.json': JSON.stringify({ name: packageName }),
390    },
391    config: { ...auth },
392  })
393  const username = maintainers[0].name
394  const registry = new MockRegistry({
395    tap: t,
396    registry: npm.config.get('registry'),
397  })
398  const manifest = registry.manifest({
399    name: packageName,
400    packuments: [{ maintainers, version: '1.0.0' }],
401  })
402  registry.couchuser({ username })
403  await registry.package({ manifest })
404  registry.nock.put(`/${spec.escapedName}/-rev/${manifest._rev}`, body => {
405    t.match(body, {
406      _id: manifest._id,
407      _rev: manifest._rev,
408      maintainers: maintainers.slice(1),
409    })
410    return true
411  }).reply(200, {})
412  await npm.exec('owner', ['rm', username])
413  t.equal(joinedOutput(), `- ${username} (${packageName})`)
414})
415
416t.test('owner rm <user> only user', async t => {
417  const { npm } = await loadMockNpm(t, {
418    prefixDir: {
419      'package.json': JSON.stringify({ name: packageName }),
420    },
421    config: { ...auth },
422  })
423  const username = maintainers[0].name
424  const registry = new MockRegistry({
425    tap: t,
426    registry: npm.config.get('registry'),
427  })
428  const manifest = registry.manifest({
429    name: packageName,
430    packuments: [{ maintainers: maintainers.slice(0, 1), version: '1.0.0' }],
431  })
432  registry.couchuser({ username })
433  await registry.package({ manifest })
434  await t.rejects(
435    npm.exec('owner', ['rm', username]),
436    {
437      code: 'EOWNERRM',
438      message: 'Cannot remove all owners of a package. Add someone else first.',
439    }
440  )
441})
442
443t.test('owner rm no user', async t => {
444  const { npm } = await loadMockNpm(t)
445  await t.rejects(
446    npm.exec('owner', ['rm']),
447    { code: 'EUSAGE' }
448  )
449})
450
451t.test('owner rm no pkg global', async t => {
452  const { npm } = await loadMockNpm(t, {
453    config: { global: true },
454  })
455  await t.rejects(
456    npm.exec('owner', ['rm', 'foo']),
457    { code: 'EUSAGE' }
458  )
459})
460
461t.test('owner rm <user> no cwd package', async t => {
462  const { npm } = await loadMockNpm(t)
463  await t.rejects(
464    npm.exec('owner', ['rm', 'foo']),
465    { code: 'EUSAGE' }
466  )
467})
468
469t.test('workspaces', async t => {
470  t.test('owner no args --workspace', async t => {
471    const { npm } = await loadMockNpm(t, {
472      prefixDir: workspaceFixture,
473      config: {
474        workspace: 'workspace-a',
475      },
476    })
477    await t.rejects(
478      npm.exec('owner', []),
479      { code: 'EUSAGE' },
480      'rejects with usage'
481    )
482  })
483
484  t.test('owner ls implicit workspace', async t => {
485    const { npm, joinedOutput } = await loadMockNpm(t, {
486      prefixDir: workspaceFixture,
487      chdir: ({ prefix }) => path.join(prefix, 'workspace-a'),
488    })
489    await registryPackage(t, npm.config.get('registry'), 'workspace-a')
490    await npm.exec('owner', ['ls'])
491    t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
492  })
493
494  t.test('owner ls explicit workspace', async t => {
495    const { npm, joinedOutput } = await loadMockNpm(t, {
496      prefixDir: workspaceFixture,
497      config: {
498        workspace: 'workspace-a',
499      },
500    })
501    await registryPackage(t, npm.config.get('registry'), 'workspace-a')
502    await npm.exec('owner', ['ls'])
503    t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
504  })
505
506  t.test('owner ls <pkg> implicit workspace', async t => {
507    const { npm, joinedOutput } = await loadMockNpm(t, {
508      prefixDir: workspaceFixture,
509      chdir: ({ prefix }) => path.join(prefix, 'workspace-a'),
510    })
511    await registryPackage(t, npm.config.get('registry'), packageName)
512    await npm.exec('owner', ['ls', packageName])
513    t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
514  })
515
516  t.test('owner ls <pkg> explicit workspace', async t => {
517    const { npm, joinedOutput } = await loadMockNpm(t, {
518      prefixDir: workspaceFixture,
519      config: {
520        workspace: 'workspace-a',
521      },
522    })
523    await registryPackage(t, npm.config.get('registry'), packageName)
524    await npm.exec('owner', ['ls', packageName])
525    t.match(joinedOutput(), maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
526  })
527
528  t.test('owner add implicit workspace', async t => {
529    const { npm, joinedOutput } = await loadMockNpm(t, {
530      prefixDir: workspaceFixture,
531      chdir: ({ prefix }) => path.join(prefix, 'workspace-a'),
532    })
533    const username = 'foo'
534    const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
535
536    const manifest = registry.manifest({
537      name: 'workspace-a',
538      packuments: [{ maintainers, version: '1.0.0' }],
539    })
540    await registry.package({ manifest })
541    registry.couchuser({ username })
542    registry.nock.put(`/workspace-a/-rev/${manifest._rev}`, body => {
543      t.match(body, {
544        _id: manifest._id,
545        _rev: manifest._rev,
546        maintainers: [
547          ...manifest.maintainers,
548          { name: username, email: '' },
549        ],
550      })
551      return true
552    }).reply(200, {})
553    await npm.exec('owner', ['add', username])
554    t.equal(joinedOutput(), `+ ${username} (workspace-a)`)
555  })
556
557  t.test('owner add --workspace', async t => {
558    const { npm, joinedOutput } = await loadMockNpm(t, {
559      prefixDir: workspaceFixture,
560      config: {
561        workspace: 'workspace-a',
562      },
563    })
564    const username = 'foo'
565    const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
566
567    const manifest = registry.manifest({
568      name: 'workspace-a',
569      packuments: [{ maintainers, version: '1.0.0' }],
570    })
571    await registry.package({ manifest })
572    registry.couchuser({ username })
573    registry.nock.put(`/workspace-a/-rev/${manifest._rev}`, body => {
574      t.match(body, {
575        _id: manifest._id,
576        _rev: manifest._rev,
577        maintainers: [
578          ...manifest.maintainers,
579          { name: username, email: '' },
580        ],
581      })
582      return true
583    }).reply(200, {})
584    await npm.exec('owner', ['add', username])
585    t.equal(joinedOutput(), `+ ${username} (workspace-a)`)
586  })
587
588  t.test('owner rm --workspace', async t => {
589    const { npm, joinedOutput } = await loadMockNpm(t, {
590      prefixDir: workspaceFixture,
591      chdir: ({ prefix }) => path.join(prefix, 'workspace-a'),
592    })
593    const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
594
595    const username = maintainers[0].name
596    const manifest = registry.manifest({
597      name: 'workspace-a',
598      packuments: [{ maintainers, version: '1.0.0' }],
599    })
600    await registry.package({ manifest })
601    registry.couchuser({ username })
602    registry.nock.put(`/workspace-a/-rev/${manifest._rev}`, body => {
603      t.match(body, {
604        _id: manifest._id,
605        _rev: manifest._rev,
606        maintainers: maintainers.slice(1),
607      })
608      return true
609    }).reply(200, {})
610    await npm.exec('owner', ['rm', username])
611    t.equal(joinedOutput(), `- ${username} (workspace-a)`)
612  })
613})
614
615t.test('completion', async t => {
616  const mockCompletion = (t, opts) => loadMockNpm(t, { command: 'owner', ...opts })
617
618  t.test('basic commands', async t => {
619    const { owner } = await mockCompletion(t)
620    const testComp = async (argv, expect) => {
621      const res = await owner.completion({ conf: { argv: { remain: argv } } })
622      t.strictSame(res, expect, argv.join(' '))
623    }
624
625    await Promise.all([
626      testComp(['npm', 'foo'], []),
627      testComp(['npm', 'owner'], ['add', 'rm', 'ls']),
628      testComp(['npm', 'owner', 'add'], []),
629      testComp(['npm', 'owner', 'ls'], []),
630      testComp(['npm', 'owner', 'rm', 'foo'], []),
631    ])
632  })
633
634  t.test('completion npm owner rm', async t => {
635    const { npm, owner } = await mockCompletion(t, {
636      prefixDir: { 'package.json': JSON.stringify({ name: packageName }) },
637    })
638    const registry = new MockRegistry({
639      tap: t,
640      registry: npm.config.get('registry'),
641    })
642    const manifest = registry.manifest({
643      name: packageName,
644      packuments: [{ maintainers, version: '1.0.0' }],
645    })
646    await registry.package({ manifest })
647    const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } })
648    t.strictSame(res, maintainers.map(m => m.name), 'should return list of current owners')
649  })
650
651  t.test('completion npm owner rm no cwd package', async t => {
652    const { owner } = await mockCompletion(t)
653    const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } })
654    t.strictSame(res, [], 'should have no owners to autocomplete if not cwd package')
655  })
656
657  t.test('completion npm owner rm global', async t => {
658    const { owner } = await mockCompletion(t, {
659      config: { global: true },
660    })
661    const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } })
662    t.strictSame(res, [], 'should have no owners to autocomplete if global')
663  })
664
665  t.test('completion npm owner rm no owners found', async t => {
666    const { npm, owner } = await mockCompletion(t, {
667      prefixDir: { 'package.json': JSON.stringify({ name: packageName }) },
668    })
669    const registry = new MockRegistry({
670      tap: t,
671      registry: npm.config.get('registry'),
672    })
673    const manifest = registry.manifest({
674      name: packageName,
675      packuments: [{ maintainers: [], version: '1.0.0' }],
676    })
677    await registry.package({ manifest })
678
679    const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } })
680    t.strictSame(res, [], 'should return no owners if not found')
681  })
682})
683