-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: migrate apps:transfer to oclif/core (#2767)
* refactor: initial migration of apps:transfer to oclif/core * refactor: move access utils since they are used by apps command * refactor: add isValidEmail to teamUtils * refactor: migrate AppTransfer lib class * refactor: fix types and update getAppsToTransfer * refactor: add confirm flag and construct app name * refactor: print error message and update lock.run call * refactor: initial refactor of apps:transfer tests * refactor: replace AppTransfer class with function and fix message output * refactor: test fixes * refactor: remove .only * refactor: add await to getConfig for test setup * fix: add app flag to AppsLock.run call and fix test * fix: add async await for commands that need the config * fix: add nock for addons:wait test api call * fix: fix apps:transfer test accounting for mac and windows formatting * fix: one more windows test fix for apps:transfer * refactor: remove orgs-v5 package directory * fix: update yarn.lock * refactor: remove import of orgs-v5 plugin from cli
- Loading branch information
Showing
33 changed files
with
338 additions
and
1,681 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import color from '@heroku-cli/color' | ||
import {Command, flags} from '@heroku-cli/command' | ||
import {Args, ux} from '@oclif/core' | ||
import * as Heroku from '@heroku-cli/schema' | ||
import {sortBy} from 'lodash' | ||
import * as inquirer from 'inquirer' | ||
import {getOwner, isTeamApp, isValidEmail} from '../../lib/teamUtils' | ||
import AppsLock from './lock' | ||
import {appTransfer} from '../../lib/apps/app-transfer' | ||
import confirmApp from '../../lib/apps/confirm-app' | ||
|
||
function getAppsToTransfer(apps: Heroku.App[]) { | ||
return inquirer.prompt([{ | ||
type: 'checkbox', | ||
name: 'choices', | ||
pageSize: 20, | ||
message: 'Select applications you would like to transfer', | ||
choices: apps.map(function (app) { | ||
return { | ||
name: `${app.name} (${getOwner(app.owner?.email)})`, value: {name: app.name, owner: app.owner?.email}, | ||
} | ||
}), | ||
}]) | ||
} | ||
|
||
export default class AppsTransfer extends Command { | ||
static topic = 'apps'; | ||
static description = 'transfer applications to another user or team'; | ||
static flags = { | ||
locked: flags.boolean({char: 'l', required: false, description: 'lock the app upon transfer'}), | ||
bulk: flags.boolean({required: false, description: 'transfer applications in bulk'}), | ||
app: flags.app(), | ||
remote: flags.remote({char: 'r'}), | ||
confirm: flags.string({char: 'c', hidden: true}), | ||
}; | ||
|
||
static args = { | ||
recipient: Args.string({description: 'user or team to transfer applications to', required: true}), | ||
}; | ||
|
||
static examples = [`$ heroku apps:transfer collaborator@example.com | ||
Transferring example to collaborator@example.com... done | ||
$ heroku apps:transfer acme-widgets | ||
Transferring example to acme-widgets... done | ||
$ heroku apps:transfer --bulk acme-widgets | ||
...`] | ||
|
||
public async run() { | ||
const {flags, args} = await this.parse(AppsTransfer) | ||
const {app, bulk, locked, confirm} = flags | ||
const recipient = args.recipient | ||
if (bulk) { | ||
const {body: allApps} = await this.heroku.get<Heroku.App[]>('/apps') | ||
const selectedApps = await getAppsToTransfer(sortBy(allApps, 'name')) | ||
ux.warn(`Transferring applications to ${color.magenta(recipient)}...\n`) | ||
for (const app of selectedApps.choices) { | ||
try { | ||
await appTransfer({ | ||
heroku: this.heroku, | ||
appName: app.name, | ||
recipient: recipient, | ||
personalToPersonal: isValidEmail(recipient) && !isTeamApp(app.owner), | ||
bulk: true, | ||
}) | ||
} catch (error) { | ||
const {message} = error as {message: string} | ||
ux.error(message) | ||
} | ||
} | ||
} else { | ||
const {body: appInfo} = await this.heroku.get<Heroku.App>(`/apps/${app}`) | ||
const appName = appInfo.name ?? app ?? '' | ||
if (isValidEmail(recipient) && isTeamApp(appInfo.owner?.email)) { | ||
await confirmApp(appName, confirm, 'All collaborators will be removed from this app') | ||
} | ||
|
||
await appTransfer({ | ||
heroku: this.heroku, | ||
appName, | ||
recipient, | ||
personalToPersonal: isValidEmail(recipient) && !isTeamApp(appInfo.owner?.email), | ||
bulk, | ||
}) | ||
if (locked) { | ||
await AppsLock.run(['--app', appName], this.config) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import {APIClient} from '@heroku-cli/command' | ||
import {color} from '@heroku-cli/color' | ||
import * as Heroku from '@heroku-cli/schema' | ||
import {ux} from '@oclif/core' | ||
|
||
type Options = { | ||
heroku: APIClient, | ||
appName: string, | ||
recipient: string, | ||
personalToPersonal: boolean, | ||
bulk: boolean, | ||
} | ||
|
||
const getRequestOpts = (options: Options) => { | ||
const {appName, bulk, recipient, personalToPersonal} = options | ||
const isPersonalToPersonal = personalToPersonal || personalToPersonal === undefined | ||
const requestOpts = isPersonalToPersonal ? | ||
{ | ||
body: {app: appName, recipient}, | ||
transferMsg: `Initiating transfer of ${color.app(appName)}`, | ||
path: '/account/app-transfers', | ||
method: 'POST', | ||
} : { | ||
body: {owner: recipient}, | ||
transferMsg: `Transferring ${color.app(appName)}`, | ||
path: `/teams/apps/${appName}`, | ||
method: 'PATCH', | ||
} | ||
if (!bulk) requestOpts.transferMsg += ` to ${color.magenta(recipient)}` | ||
return requestOpts | ||
} | ||
|
||
export const appTransfer = async (options: Options) => { | ||
const {body, transferMsg, path, method} = getRequestOpts(options) | ||
ux.action.start(transferMsg) | ||
const {body: request} = await options.heroku.request<Heroku.TeamApp>( | ||
path, | ||
{ | ||
method, | ||
body, | ||
}, | ||
) | ||
const message = request.state === 'pending' ? 'email sent' : undefined | ||
ux.action.stop(message) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,14 @@ | ||
import {APIClient} from '@heroku-cli/command' | ||
import {Config} from '@oclif/core' | ||
|
||
export const getConfig = () => new Config({root: '../../package.json'}) | ||
export const getConfig = async () => { | ||
const pjsonPath = require.resolve('../../package.json') | ||
const conf = new Config({root: pjsonPath}) | ||
await conf.load() | ||
return conf | ||
} | ||
|
||
export const getHerokuAPI = () => new APIClient(getConfig()) | ||
export const getHerokuAPI = async () => { | ||
const conf = await getConfig() | ||
return new APIClient(conf) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
packages/cli/test/unit/commands/apps/transfer.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import {stdout, stderr} from 'stdout-stderr' | ||
import * as nock from 'nock' | ||
import * as proxyquire from 'proxyquire' | ||
import {expect} from 'chai' | ||
import runCommand, {GenericCmd} from '../../../helpers/runCommand' | ||
import {apps, personalApp, teamApp} from '../../../helpers/stubs/get' | ||
import {teamAppTransfer} from '../../../helpers/stubs/patch' | ||
import {personalToPersonal} from '../../../helpers/stubs/post' | ||
|
||
let Cmd: GenericCmd | ||
let inquirer: {prompt?: (prompts: { choices: any }[]) => void} = {} | ||
|
||
describe('heroku apps:transfer', () => { | ||
beforeEach(() => { | ||
inquirer = {} | ||
const {default: proxyCmd} = proxyquire('../../../../src/commands/apps/transfer', { | ||
inquirer, | ||
'@noCallThru': true, | ||
}) | ||
Cmd = proxyCmd | ||
}) | ||
afterEach(() => nock.cleanAll()) | ||
context('when transferring in bulk', () => { | ||
beforeEach(() => { | ||
apps() | ||
}) | ||
it('transfers selected apps to a team', async () => { | ||
inquirer.prompt = (prompts: { choices: any }[]) => { | ||
const choices = prompts[0].choices | ||
expect(choices).to.eql([ | ||
{ | ||
name: 'my-team-app (team)', value: {name: 'my-team-app', owner: 'team@herokumanager.com'}, | ||
}, { | ||
name: 'myapp (foo@foo.com)', value: {name: 'myapp', owner: 'foo@foo.com'}, | ||
}, | ||
]) | ||
return Promise.resolve({choices: [{name: 'myapp', owner: 'foo@foo.com'}]}) | ||
} | ||
|
||
const api = teamAppTransfer() | ||
await runCommand(Cmd, [ | ||
'--bulk', | ||
'team', | ||
]) | ||
api.done() | ||
expect(stderr.output).to.include('Warning: Transferring applications to team...\n') | ||
expect(stderr.output).to.include('\nTransferring ⬢ myapp...\nTransferring ⬢ myapp... done\n') | ||
}) | ||
it('transfers selected apps to a personal account', async () => { | ||
inquirer.prompt = (prompts: { choices: any }[]) => { | ||
const choices = prompts[0].choices | ||
expect(choices).to.eql([ | ||
{ | ||
name: 'my-team-app (team)', value: {name: 'my-team-app', owner: 'team@herokumanager.com'}, | ||
}, { | ||
name: 'myapp (foo@foo.com)', value: {name: 'myapp', owner: 'foo@foo.com'}, | ||
}, | ||
]) | ||
return Promise.resolve({choices: [{name: 'myapp', owner: 'foo@foo.com'}]}) | ||
} | ||
|
||
const api = personalToPersonal() | ||
await runCommand(Cmd, [ | ||
'--bulk', | ||
'raulb@heroku.com', | ||
]) | ||
api.done() | ||
expect(stderr.output).to.include('Warning: Transferring applications to raulb@heroku.com...\n') | ||
expect(stderr.output).to.include('\nInitiating transfer of ⬢ myapp...\nInitiating transfer of ⬢ myapp... email sent\n') | ||
}) | ||
}) | ||
context('when it is a personal app', () => { | ||
beforeEach(() => { | ||
personalApp() | ||
}) | ||
it('transfers the app to a personal account', async () => { | ||
const api = personalToPersonal() | ||
await runCommand(Cmd, [ | ||
'--app', | ||
'myapp', | ||
'raulb@heroku.com', | ||
]) | ||
expect('').to.eq(stdout.output) | ||
expect('Initiating transfer of ⬢ myapp to raulb@heroku.com...\nInitiating transfer of ⬢ myapp to raulb@heroku.com... email sent\n').to.eq(stderr.output) | ||
api.done() | ||
}) | ||
it('transfers the app to a team', async () => { | ||
const api = teamAppTransfer() | ||
await runCommand(Cmd, [ | ||
'--app', | ||
'myapp', | ||
'team', | ||
]) | ||
expect('').to.eq(stdout.output) | ||
expect('Transferring ⬢ myapp to team...\nTransferring ⬢ myapp to team... done\n').to.eq(stderr.output) | ||
api.done() | ||
}) | ||
}) | ||
context('when it is an org app', () => { | ||
beforeEach(() => { | ||
teamApp() | ||
}) | ||
it('transfers the app to a personal account confirming app name', async () => { | ||
const api = teamAppTransfer() | ||
await runCommand(Cmd, [ | ||
'--app', | ||
'myapp', | ||
'--confirm', | ||
'myapp', | ||
'team', | ||
]) | ||
expect('').to.eq(stdout.output) | ||
expect('Transferring ⬢ myapp to team...\nTransferring ⬢ myapp to team... done\n').to.eq(stderr.output) | ||
api.done() | ||
}) | ||
it('transfers the app to a team', async () => { | ||
const api = teamAppTransfer() | ||
await runCommand(Cmd, [ | ||
'--app', | ||
'myapp', | ||
'team', | ||
]) | ||
expect('').to.eq(stdout.output) | ||
expect('Transferring ⬢ myapp to team...\nTransferring ⬢ myapp to team... done\n').to.eq(stderr.output) | ||
api.done() | ||
}) | ||
it('transfers and locks the app if --locked is passed', async () => { | ||
const api = teamAppTransfer() | ||
const lockedAPI = nock('https://api.heroku.com:443') | ||
.get('/teams/apps/myapp') | ||
.reply(200, {name: 'myapp', locked: false}) | ||
.patch('/teams/apps/myapp', {locked: true}) | ||
.reply(200) | ||
await runCommand(Cmd, [ | ||
'--app', | ||
'myapp', | ||
'--locked', | ||
'team', | ||
]) | ||
expect('').to.eq(stdout.output) | ||
expect('Transferring ⬢ myapp to team...\nTransferring ⬢ myapp to team... done\nLocking myapp...\nLocking myapp... done\n').to.eq(stderr.output) | ||
api.done() | ||
lockedAPI.done() | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.