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