From e6aabcde9b17ac780ae90ed61299d5834373f974 Mon Sep 17 00:00:00 2001 From: Katy Bowman Date: Sat, 2 Mar 2024 21:36:11 -0500 Subject: [PATCH 1/4] refactor: migrate members:add command and utils to oclif/core --- packages/cli/src/commands/members/add.ts | 33 ++++++++++++++++++++++++ packages/cli/src/lib/members/util.ts | 28 ++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 packages/cli/src/commands/members/add.ts create mode 100644 packages/cli/src/lib/members/util.ts diff --git a/packages/cli/src/commands/members/add.ts b/packages/cli/src/commands/members/add.ts new file mode 100644 index 0000000000..68cff70b64 --- /dev/null +++ b/packages/cli/src/commands/members/add.ts @@ -0,0 +1,33 @@ +import {Command, flags} from '@heroku-cli/command' +import {Args} from '@oclif/core' +import * as Heroku from '@heroku-cli/schema' +const {RoleCompletion} = require('@heroku-cli/command/lib/completions') +import {addMemberToTeam, inviteMemberToTeam} from '../../lib/members/util' +export default class Add extends Command { + static topic = 'members'; + static description = 'adds a user to a team'; + static flags = { + role: flags.string({char: 'r', required: true, description: 'member role (admin, collaborator, member, owner)', completion: RoleCompletion}), + team: flags.team({required: true}), + }; + + static args = { + email: Args.string({required: true}), + }; + + public async run(): Promise { + const {flags, args} = await this.parse(Add) + const {team, role} = flags + const {body: teamInfo} = await this.heroku.get(`/teams/${team}`) + const email = args.email + const {body: groupFeatures} = await this.heroku.get(`/teams/${team}/features`) + + if (teamInfo.type === 'team' && groupFeatures.find(feature => { + return feature.name === 'team-invite-acceptance' && feature.enabled + })) { + await inviteMemberToTeam(email, role, team, this.heroku) + } else { + await addMemberToTeam(email, role, team, this.heroku) + } + } +} diff --git a/packages/cli/src/lib/members/util.ts b/packages/cli/src/lib/members/util.ts new file mode 100644 index 0000000000..25ccb42aa6 --- /dev/null +++ b/packages/cli/src/lib/members/util.ts @@ -0,0 +1,28 @@ +import {APIClient} from '@heroku-cli/command' +import {ux} from '@oclif/core' +import color from '@heroku-cli/color' +import * as Heroku from '@heroku-cli/schema' + +export const inviteMemberToTeam = async function (email: string, role: string, team: string, heroku: APIClient) { + ux.action.start(`Inviting ${color.cyan(email)} to ${color.magenta(team)} as ${color.green(role)}`) + await heroku.request( + `/teams/${team}/invitations`, + { + headers: { + Accept: 'application/vnd.heroku+json; version=3.team-invitations', + }, method: 'PUT', + body: {email, role}, + }) + ux.action.stop() +} + +export const addMemberToTeam = async function (email: string, role: string, groupName: string, heroku: APIClient, method = 'PUT') { + ux.action.start(`Adding ${color.cyan(email)} to ${color.magenta(groupName)} as ${color.green(role)}`) + await heroku.request( + `/teams/${groupName}/members`, + { + method: method, + body: {email, role}, + }) + ux.action.stop() +} From 9b66de4c2f3b8500d945ecc64a56265d788b5889 Mon Sep 17 00:00:00 2001 From: Katy Bowman Date: Sat, 2 Mar 2024 21:45:37 -0500 Subject: [PATCH 2/4] refactor: refactor members:add tests and test utils --- packages/cli/src/commands/members/add.ts | 4 +- packages/cli/test/helpers/stubs/put.ts | 13 ++++ .../unit/commands/members/add.unit.test.ts | 63 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 packages/cli/test/helpers/stubs/put.ts create mode 100644 packages/cli/test/unit/commands/members/add.unit.test.ts diff --git a/packages/cli/src/commands/members/add.ts b/packages/cli/src/commands/members/add.ts index 68cff70b64..d19644cc1c 100644 --- a/packages/cli/src/commands/members/add.ts +++ b/packages/cli/src/commands/members/add.ts @@ -3,7 +3,7 @@ import {Args} from '@oclif/core' import * as Heroku from '@heroku-cli/schema' const {RoleCompletion} = require('@heroku-cli/command/lib/completions') import {addMemberToTeam, inviteMemberToTeam} from '../../lib/members/util' -export default class Add extends Command { +export default class MembersAdd extends Command { static topic = 'members'; static description = 'adds a user to a team'; static flags = { @@ -16,7 +16,7 @@ export default class Add extends Command { }; public async run(): Promise { - const {flags, args} = await this.parse(Add) + const {flags, args} = await this.parse(MembersAdd) const {team, role} = flags const {body: teamInfo} = await this.heroku.get(`/teams/${team}`) const email = args.email diff --git a/packages/cli/test/helpers/stubs/put.ts b/packages/cli/test/helpers/stubs/put.ts new file mode 100644 index 0000000000..2ab6b0f961 --- /dev/null +++ b/packages/cli/test/helpers/stubs/put.ts @@ -0,0 +1,13 @@ +import * as nock from 'nock' + +export function sendInvite(email = 'raulb@heroku.com', role = 'admin') { + return nock('https://api.heroku.com:443') + .put('/teams/myteam/invitations', {email, role}) + .reply(200) +} + +export function updateMemberRole(email = 'raulb@heroku.com', role = 'admin') { + return nock('https://api.heroku.com:443') + .put('/teams/myteam/members', {email, role}) + .reply(200) +} diff --git a/packages/cli/test/unit/commands/members/add.unit.test.ts b/packages/cli/test/unit/commands/members/add.unit.test.ts new file mode 100644 index 0000000000..4fc9778916 --- /dev/null +++ b/packages/cli/test/unit/commands/members/add.unit.test.ts @@ -0,0 +1,63 @@ +import {stdout, stderr} from 'stdout-stderr' +import {expect} from 'chai' +import * as nock from 'nock' +import Cmd from '../../../../src/commands/members/add' +import runCommand from '../../../helpers/runCommand' +import {sendInvite, updateMemberRole} from '../../../helpers/stubs/put' +import { + teamFeatures, + teamInfo, + variableSizeTeamInvites, + variableSizeTeamMembers, +} from '../../../helpers/stubs/get' + +describe('heroku members:add', () => { + let apiUpdateMemberRole: nock.Scope + afterEach(() => nock.cleanAll()) + + context('without the feature flag team-invite-acceptance', () => { + beforeEach(() => { + teamFeatures([]) + }) + context('and group is an enterprise org', () => { + beforeEach(() => { + teamInfo('enterprise') + variableSizeTeamMembers(1) + }) + it('adds a member to an org', () => { + apiUpdateMemberRole = updateMemberRole('foo@foo.com', 'admin') + return runCommand(Cmd, [ + '--team', + 'myteam', + '--role', + 'admin', + 'foo@foo.com', + ]) + .then(() => expect('').to.eq(stdout.output)) + .then(() => expect('Adding foo@foo.com to myteam as admin...\nAdding foo@foo.com to myteam as admin... done\n').to.eq(stderr.output)) + .then(() => apiUpdateMemberRole.done()) + }) + }) + }) + context('with the feature flag team-invite-acceptance for a team', () => { + beforeEach(() => { + teamFeatures([{name: 'team-invite-acceptance', enabled: true}]) + teamInfo('team') + }) + it('sends an invite when adding a new user to the team', () => { + const apiSendInvite = sendInvite('foo@foo.com', 'admin') + variableSizeTeamMembers(1) + variableSizeTeamInvites(0) + return runCommand(Cmd, [ + '--role', + 'admin', + '--team', + 'myteam', + 'foo@foo.com', + ]) + .then(() => expect('').to.eq(stdout.output)) + .then(() => expect('Inviting foo@foo.com to myteam as admin...\nInviting foo@foo.com to myteam as admin... done\n').to.eq(stderr.output)) + .then(() => apiSendInvite.done()) + }) + }) +}) From 2cfddf1651ac65b7bf4a9d312a67dcf8daad3c1f Mon Sep 17 00:00:00 2001 From: Katy Bowman Date: Sat, 2 Mar 2024 21:46:56 -0500 Subject: [PATCH 3/4] refactor: remove orgs-v5 members:add files --- packages/orgs-v5/commands/members/add.js | 53 ----------------- packages/orgs-v5/index.js | 1 - .../unit/commands/members/add.unit.test.js | 59 ------------------- 3 files changed, 113 deletions(-) delete mode 100644 packages/orgs-v5/commands/members/add.js delete mode 100644 packages/orgs-v5/test/unit/commands/members/add.unit.test.js diff --git a/packages/orgs-v5/commands/members/add.js b/packages/orgs-v5/commands/members/add.js deleted file mode 100644 index 21f9e91684..0000000000 --- a/packages/orgs-v5/commands/members/add.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict' - -let cli = require('heroku-cli-util') -let Utils = require('../../lib/utils') -const {flags} = require('@heroku-cli/command') -const {RoleCompletion} = require('@heroku-cli/command/lib/completions') - -async function run(context, heroku) { - let teamInfo = await Utils.teamInfo(context, heroku) - let groupName = context.flags.team - let email = context.args.email - let role = context.flags.role - let groupFeatures = await heroku.get(`/teams/${groupName}/features`) - - let inviteMemberToTeam = async function (email, role, groupName) { - let request = heroku.request({ - headers: { - Accept: 'application/vnd.heroku+json; version=3.team-invitations', - }, - method: 'PUT', - path: `/teams/${groupName}/invitations`, - body: {email, role}, - }).then(() => { - cli.action.done('email sent') - }) - - await cli.action(`Inviting ${cli.color.cyan(email)} to ${cli.color.magenta(groupName)} as ${cli.color.green(role)}`, request) - } - - if (teamInfo.type === 'team' && groupFeatures.find(feature => { - return feature.name === 'team-invite-acceptance' && feature.enabled - })) { - await inviteMemberToTeam(email, role, groupName) - } else { - await Utils.addMemberToTeam(email, role, groupName, heroku) - } -} - -let add = { - topic: 'members', - command: 'add', - description: 'adds a user to a team', - needsAuth: true, - wantsOrg: true, - args: [{name: 'email'}], - flags: [ - {name: 'role', char: 'r', hasValue: true, required: true, description: 'member role (admin, collaborator, member, owner)', completion: RoleCompletion}, - flags.team({name: 'team', hasValue: true, hidden: true}), - ], - run: cli.command(run), -} - -module.exports = add diff --git a/packages/orgs-v5/index.js b/packages/orgs-v5/index.js index b1abb605b4..8ecfa3692a 100644 --- a/packages/orgs-v5/index.js +++ b/packages/orgs-v5/index.js @@ -21,5 +21,4 @@ exports.commands = flatten([ require('./commands/apps/lock'), require('./commands/apps/transfer'), require('./commands/apps/unlock'), - require('./commands/members/add'), ]) diff --git a/packages/orgs-v5/test/unit/commands/members/add.unit.test.js b/packages/orgs-v5/test/unit/commands/members/add.unit.test.js deleted file mode 100644 index c72944aba4..0000000000 --- a/packages/orgs-v5/test/unit/commands/members/add.unit.test.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict' -/* globals beforeEach afterEach cli nock expect context */ - -let cmd = require('../../../../commands/members/add') -let stubGet = require('../../stub/get') -let stubPut = require('../../stub/put') -const unwrap = require('../../../unwrap') - -describe('heroku members:add', () => { - let apiUpdateMemberRole - - beforeEach(() => cli.mockConsole()) - afterEach(() => nock.cleanAll()) - - it('is configured for an optional team flag', function () { - expect(cmd).to.have.own.property('wantsOrg', true) - }) - - context('without the feature flag team-invite-acceptance', () => { - beforeEach(() => { - stubGet.teamFeatures([]) - }) - context('and group is an enterprise org', () => { - beforeEach(() => { - stubGet.teamInfo('enterprise') - stubGet.variableSizeTeamMembers(1) - }) - - it('adds a member to an org', () => { - apiUpdateMemberRole = stubPut.updateMemberRole('foo@foo.com', 'admin') - - return cmd.run({args: {email: 'foo@foo.com'}, flags: {team: 'myteam', role: 'admin'}}) - .then(() => expect('').to.eq(cli.stdout)) - .then(() => expect(`Adding foo@foo.com to myteam as admin... done -`).to.eq(cli.stderr)) - .then(() => apiUpdateMemberRole.done()) - }) - }) - }) - - context('with the feature flag team-invite-acceptance for a team', () => { - beforeEach(() => { - stubGet.teamFeatures([{name: 'team-invite-acceptance', enabled: true}]) - stubGet.teamInfo('team') - }) - - it('sends an invite when adding a new user to the team', () => { - let apiSendInvite = stubPut.sendInvite('foo@foo.com', 'admin') - - stubGet.variableSizeTeamMembers(1) - stubGet.variableSizeTeamInvites(0) - - return cmd.run({args: {email: 'foo@foo.com'}, flags: {role: 'admin', team: 'myteam'}}) - .then(() => expect('').to.eq(cli.stdout)) - .then(() => expect('Inviting foo@foo.com to myteam as admin... email sent\n').to.eq(cli.stderr)) - .then(() => apiSendInvite.done()) - }) - }) -}) From 6a781a45b0d34b10ff8dd86b29148c0142272a6d Mon Sep 17 00:00:00 2001 From: Katy Bowman Date: Mon, 4 Mar 2024 15:59:11 -0500 Subject: [PATCH 4/4] fix: update array.find to array.some and change require to import --- packages/cli/src/commands/members/add.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/members/add.ts b/packages/cli/src/commands/members/add.ts index d19644cc1c..e1228862b3 100644 --- a/packages/cli/src/commands/members/add.ts +++ b/packages/cli/src/commands/members/add.ts @@ -1,7 +1,7 @@ import {Command, flags} from '@heroku-cli/command' import {Args} from '@oclif/core' import * as Heroku from '@heroku-cli/schema' -const {RoleCompletion} = require('@heroku-cli/command/lib/completions') +import {RoleCompletion} from '@heroku-cli/command/lib/completions' import {addMemberToTeam, inviteMemberToTeam} from '../../lib/members/util' export default class MembersAdd extends Command { static topic = 'members'; @@ -22,7 +22,7 @@ export default class MembersAdd extends Command { const email = args.email const {body: groupFeatures} = await this.heroku.get(`/teams/${team}/features`) - if (teamInfo.type === 'team' && groupFeatures.find(feature => { + if (teamInfo.type === 'team' && groupFeatures.some(feature => { return feature.name === 'team-invite-acceptance' && feature.enabled })) { await inviteMemberToTeam(email, role, team, this.heroku)