Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions src/commands/apps/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Command, flags} from '@heroku-cli/command'
import {SpaceCompletion} from '@heroku-cli/command/lib/completions.js'
import * as Heroku from '@heroku-cli/schema'
import {color, hux} from '@heroku/heroku-cli-util'
import {HerokuSDK} from '@heroku/sdk'
import {ux} from '@oclif/core/ux'

import {lazyModuleLoader} from '../../lib/lazy-module-loader.js'
Expand Down Expand Up @@ -37,20 +37,25 @@ export default class AppsIndex extends Command {
let team = (!flags.personal && teamIdentifier) ? teamIdentifier : null
const {all, json, space} = flags
const internalRouting = flags['internal-routing']

const sdk = new HerokuSDK()
const {platform} = sdk

if (space) {
const teamResponse = await this.heroku.get<Heroku.Team>(`/spaces/${space}`)
team = teamResponse.body.team.name
const spaceInfo = await platform.space.info(space)
team = spaceInfo.team?.name ?? null
}

let path = '/users/~/apps'
if (team) path = `/teams/${team}/apps`
else if (all) path = '/apps'
const [appsResponse, userResponse] = await Promise.all([
this.heroku.get<Heroku.App>(path),
this.heroku.get<Heroku.Account>('/account'),
const [appsList, user] = await Promise.all([
(async () => {
if (team) return platform.teamApp.listByTeam(team)
if (all) return platform.app.list()
return platform.app.listOwnedAndCollaborated('~')
})(),
platform.account.info(),
])
let apps = appsResponse.body
const user = userResponse.body

let apps = appsList as unknown as App[]

apps = _.sortBy(apps, 'name')
if (space) {
Expand Down Expand Up @@ -82,11 +87,11 @@ function annotateAppName(app: App) {
return name
}

function listApps(apps: Heroku.App) {
function listApps(apps: App[]) {
apps.forEach((app: App) => ux.stdout(regionizeAppName(app)))
}

function print(apps: Heroku.App, user: Heroku.Account, space: string | undefined, team: null | string | undefined, _: any) {
function print(apps: App[], user: {email?: string}, space: string | undefined, team: null | string | undefined, _: any) {
if (apps.length === 0) {
if (space) ux.stdout(`There are no apps in space ${color.space(space)}.`)
else if (team) ux.stdout(`There are no apps in team ${color.team(team)}.`)
Expand All @@ -98,10 +103,10 @@ function print(apps: Heroku.App, user: Heroku.Account, space: string | undefined
hux.styledHeader(`Apps in team ${color.team(team)}`)
listApps(apps)
} else {
apps = _.partition(apps, (app: App) => app.owner.email === user.email)
if (apps[0].length > 0) {
const [ownedApps, collabApps] = _.partition(apps, (app: App) => app.owner.email === user.email)
if (ownedApps.length > 0) {
hux.styledHeader(`${color.user(user.email!)} Apps`)
listApps(apps[0])
listApps(ownedApps)
}

const columns = {
Expand All @@ -110,9 +115,9 @@ function print(apps: Heroku.App, user: Heroku.Account, space: string | undefined
Email: {get: ({owner}: any) => color.user(owner.email)},
}

if (apps[1].length > 0) {
if (collabApps.length > 0) {
ux.stdout()
hux.table(apps[1], columns, {title: 'Collaborated Apps\n', titleOptions: {bold: true}})
hux.table(collabApps, columns, {title: 'Collaborated Apps\n', titleOptions: {bold: true}})
}
}
}
Expand Down
172 changes: 72 additions & 100 deletions test/unit/commands/apps/index.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import {runCommand} from '@heroku-cli/test-utils'
import {HerokuSDK} from '@heroku/sdk'
import {expect} from 'chai'
import nock from 'nock'
import * as sinon from 'sinon'

import Apps from '../../../../src/commands/apps/index.js'
import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js'

type FakePlatform = {
account: {info: sinon.SinonStub}
app: {
list: sinon.SinonStub
listOwnedAndCollaborated: sinon.SinonStub
}
space: {info: sinon.SinonStub}
teamApp: {listByTeam: sinon.SinonStub}
}

function buildFakePlatform(): FakePlatform {
return {
account: {info: sinon.stub()},
app: {
list: sinon.stub(),
listOwnedAndCollaborated: sinon.stub(),
},
space: {info: sinon.stub()},
teamApp: {listByTeam: sinon.stub()},
}
}

describe('apps', function () {
const example = {
name: 'example',
Expand Down Expand Up @@ -76,40 +99,32 @@ describe('apps', function () {
space: {id: 'test-space-id', name: 'test-space'},
}

let euLockedApp = {}
let euInternalApp = {}
let euInternalLockedApp = {}
let api: nock.Scope
let fakePlatform: FakePlatform

beforeEach(function () {
api = nock('https://api.heroku.com')
fakePlatform = buildFakePlatform()
sinon.stub(HerokuSDK.prototype, 'platform').get(() => fakePlatform)
})

afterEach(function () {
api.done()
nock.cleanAll()
sinon.restore()
})

describe('with no args', function () {
it('displays a message when the user has no apps', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([])

const {stderr, stdout} = await runCommand(Apps, [])

expect(stderr).to.equal('')
expect(stdout).to.equal('You have no apps.\n')
expect(fakePlatform.app.listOwnedAndCollaborated.calledOnceWithExactly('~')).to.equal(true)
})

it('list all user apps', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, collabApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, collabApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -126,11 +141,8 @@ describe('apps', function () {
})

it('lists all apps', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/apps')
.reply(200, [example, collabApp, teamApp1])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.list.resolves([example, collabApp, teamApp1])

const {stderr, stdout} = await runCommand(Apps, ['--all'])

Expand All @@ -144,14 +156,13 @@ describe('apps', function () {
expect(actual).to.include(expectedPersonalApps)
expect(actual).to.include(expectedCollaboratedAppsHeader)
expect(actual).to.include(expectedCollaboratedApps)
expect(fakePlatform.app.list.calledOnce).to.equal(true)
expect(fakePlatform.app.listOwnedAndCollaborated.called).to.equal(false)
})

it('shows as json', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, collabApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, collabApp])

const {stderr, stdout} = await runCommand(Apps, ['--json'])

Expand All @@ -160,11 +171,8 @@ describe('apps', function () {
})

it('shows region if not us', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -173,11 +181,8 @@ describe('apps', function () {
})

it('shows locked app', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp, lockedApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp, lockedApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -186,13 +191,9 @@ describe('apps', function () {
})

it('shows locked eu app', async function () {
euLockedApp = Object.assign(lockedApp, {region: {name: 'eu'}})

api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp, euLockedApp])
const euLockedApp = {...lockedApp, region: {name: 'eu'}}
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp, euLockedApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -201,11 +202,8 @@ describe('apps', function () {
})

it('shows internal app', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp, internalApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp, internalApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -214,11 +212,8 @@ describe('apps', function () {
})

it('shows internal locked app', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp, internalLockedApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp, internalLockedApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -227,13 +222,9 @@ describe('apps', function () {
})

it('shows internal eu app', async function () {
euInternalApp = Object.assign(internalApp, {region: {name: 'eu'}})

api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp, euInternalApp])
const euInternalApp = {...internalApp, region: {name: 'eu'}}
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp, euInternalApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -242,13 +233,9 @@ describe('apps', function () {
})

it('shows internal locked eu app', async function () {
euInternalLockedApp = Object.assign(internalLockedApp, {region: {name: 'eu'}})

api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/users/~/apps')
.reply(200, [example, euApp, euInternalLockedApp])
const euInternalLockedApp = {...internalLockedApp, region: {name: 'eu'}}
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.app.listOwnedAndCollaborated.resolves([example, euApp, euInternalLockedApp])

const {stderr, stdout} = await runCommand(Apps, [])

Expand All @@ -259,24 +246,19 @@ describe('apps', function () {

describe('with team', function () {
it('displays a message when the team has no apps', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/teams/test-team/apps')
.reply(200, [])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.teamApp.listByTeam.resolves([])

const {stderr, stdout} = await runCommand(Apps, ['--team', 'test-team'])

expect(stderr).to.equal('')
expect(stdout).to.equal('There are no apps in team test-team.\n')
expect(fakePlatform.teamApp.listByTeam.calledOnceWithExactly('test-team')).to.equal(true)
})

it('list all in a team', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/teams/test-team/apps')
.reply(200, [teamApp1, teamApp2])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.teamApp.listByTeam.resolves([teamApp1, teamApp2])

const {stderr, stdout} = await runCommand(Apps, ['--team', 'test-team'])

Expand All @@ -287,28 +269,22 @@ describe('apps', function () {

describe('with space', function () {
it('displays a message when the space has no apps', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/spaces/test-space')
.reply(200, {team: {name: 'test-team'}})
.get('/teams/test-team/apps')
.reply(200, [])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.space.info.resolves({team: {name: 'test-team'}})
fakePlatform.teamApp.listByTeam.resolves([])

const {stderr, stdout} = await runCommand(Apps, ['--space', 'test-space'])

expect(stderr).to.equal('')
expect(stdout).to.equal('There are no apps in space ⬡ test-space.\n')
expect(fakePlatform.space.info.calledOnceWithExactly('test-space')).to.equal(true)
expect(fakePlatform.teamApp.listByTeam.calledOnceWithExactly('test-team')).to.equal(true)
})

it('lists only apps in spaces by name', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/spaces/test-space')
.reply(200, {team: {name: 'test-team'}})
.get('/teams/test-team/apps')
.reply(200, [teamSpaceApp1, teamSpaceApp2, teamApp1])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.space.info.resolves({team: {name: 'test-team'}})
fakePlatform.teamApp.listByTeam.resolves([teamSpaceApp1, teamSpaceApp2, teamApp1])

const {stderr, stdout} = await runCommand(Apps, ['--space', 'test-space'])

Expand All @@ -317,13 +293,9 @@ describe('apps', function () {
})

it('lists only internal apps in spaces by name', async function () {
api
.get('/account')
.reply(200, {email: 'foo@bar.com'})
.get('/spaces/test-space')
.reply(200, {team: {name: 'test-team'}})
.get('/teams/test-team/apps')
.reply(200, [teamSpaceApp1, teamSpaceApp2, teamApp1, teamSpaceInternalApp])
fakePlatform.account.info.resolves({email: 'foo@bar.com'})
fakePlatform.space.info.resolves({team: {name: 'test-team'}})
fakePlatform.teamApp.listByTeam.resolves([teamSpaceApp1, teamSpaceApp2, teamApp1, teamSpaceInternalApp])

const {stderr, stdout} = await runCommand(Apps, ['--space', 'test-space', '--internal-routing'])

Expand Down
Loading