Skip to content
Open
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
59 changes: 58 additions & 1 deletion src/commands/console/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const LibConsoleCLI = require('@adobe/aio-cli-lib-console')
const { CLI } = require('@adobe/aio-lib-ims/src/context')
const { getCliEnv } = require('@adobe/aio-lib-env')
const yaml = require('js-yaml')
const { CONFIG_KEYS, API_KEYS } = require('../../config')
const { CONFIG_KEYS, API_KEYS, CONSOLE_API_URLS, ORG_FEATURE_RUNTIME, ORG_TYPE_DEVELOPER, ORG_TYPE_ENTERPRISE } = require('../../config')

class ConsoleCommand extends Command {
async run () {
Expand All @@ -36,6 +36,63 @@ class ConsoleCommand extends Command {
this.consoleCLI = await LibConsoleCLI.init({ accessToken: this.accessToken, apiKey: this.apiKey, env: this.cliEnv })
}

/**
* Retrieve enabled feature flags for an org from the Developer Console web API.
*
* @param {string} orgId Organization AMS ID
* @returns {Promise<Array<{name: string, description: string}>>} feature flags
*/
Comment thread
pru55e11 marked this conversation as resolved.
async getOrgFeatures (orgId) {
const baseUrl = CONSOLE_API_URLS[this.cliEnv] || CONSOLE_API_URLS.prod
const response = await fetch(`${baseUrl}/console/api/organizations/${orgId}/features`, {
headers: {
accept: 'application/json',
authorization: `Bearer ${this.accessToken}`,
'x-api-key': this.apiKey
}
})
if (!response.ok) {
Comment thread
pru55e11 marked this conversation as resolved.
aioConsoleLogger.debug(`getOrgFeatures: non-ok response ${response.status} for org ${orgId}`)
return []
}
return response.json()
}

/**
* Test whether an org has the Runtime feature.
*
* @param {string} orgId Organization AMS ID
* @returns {Promise<boolean>} true when Runtime is enabled
*/
async hasRuntimeFeature (orgId) {
try {
const features = await this.getOrgFeatures(orgId)
return features.some(feature => feature.name === ORG_FEATURE_RUNTIME)
} catch (err) {
aioConsoleLogger.debug(err)
return false
}
}

/**
* Filter orgs to those that can be used by Developer Console App Builder flows.
*
* @param {Array<{id: string, type: string}>} orgs organizations
* @returns {Promise<Array<object>>} selectable organizations
*/
async getSelectableOrgs (orgs) {
const checks = await Promise.all(orgs.map(async org => {
if (org.type === ORG_TYPE_ENTERPRISE) {
return true
}
if (org.type === ORG_TYPE_DEVELOPER) {
return this.hasRuntimeFeature(org.id)
}
return false
}))
return orgs.filter((_, i) => checks[i])
}

/**
* Output JSON data
*
Expand Down
6 changes: 2 additions & 4 deletions src/commands/console/org/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ governing permissions and limitations under the License.
const aioConsoleLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-console:org:list', { provider: 'debug' })
const { Flags } = require('@oclif/core')
const { table } = require('../../../utils/table')
const { ORG_TYPE_ENTERPRISE } = require('../../../config')

const ConsoleCommand = require('../index')

Expand Down Expand Up @@ -48,9 +47,8 @@ class ListCommand extends ConsoleCommand {
*/
async getConsoleOrgs () {
const response = await this.consoleCLI.getOrganizations()
const orgs = response
// Filter enterprise orgs
.filter(org => org.type === ORG_TYPE_ENTERPRISE)
const selectableOrgs = await this.getSelectableOrgs(response)
const orgs = selectableOrgs
// Omit props
.map(({ id, code, name }) => ({ id, code, name }))

Expand Down
2 changes: 1 addition & 1 deletion src/commands/console/org/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class SelectCommand extends ConsoleCommand {
}

async selectOrgInteractive (preSelectedOrgIdOrCode) {
const orgs = await this.consoleCLI.getOrganizations()
const orgs = await this.getSelectableOrgs(await this.consoleCLI.getOrganizations())
const org = await this.consoleCLI.promptForSelectOrganization(
orgs,
{ orgId: preSelectedOrgIdOrCode, orgCode: preSelectedOrgIdOrCode }
Expand Down
12 changes: 11 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ governing permissions and limitations under the License.
*/

const ORG_TYPE_ENTERPRISE = 'entp'
const ORG_TYPE_DEVELOPER = 'developer'
const ORG_FEATURE_RUNTIME = 'RUNTIME'

const API_KEYS = {
prod: 'aio-cli-console-auth',
Expand All @@ -28,9 +30,17 @@ const OPEN_URLS = {
stage: 'https://developer-stage.adobe.com/console/projects'
}

const CONSOLE_API_URLS = {
prod: 'https://developer.adobe.com',
stage: 'https://developer-stage.adobe.com'
}

module.exports = {
ORG_TYPE_ENTERPRISE,
ORG_TYPE_DEVELOPER,
ORG_FEATURE_RUNTIME,
CONFIG_KEYS,
API_KEYS,
OPEN_URLS
OPEN_URLS,
CONSOLE_API_URLS
}
5 changes: 5 additions & 0 deletions test/__fixtures__/org/list.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@
"id": 3,
"code": "CODE03",
"name": "ORG03"
},
{
"id": 4,
"code": "CODE04",
"name": "ORGFOURXX"
}
]
3 changes: 2 additions & 1 deletion test/__fixtures__/org/list.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Org ID Code Org Name
────── ────── ────────
────── ────── ────────
1 CODE01 ORG01
2 CODE02 ORG02
3 CODE03 ORG03
4 CODE04 ORGFOURXX
3 changes: 3 additions & 0 deletions test/__fixtures__/org/list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
- id: 3
code: CODE03
name: ORG03
- id: 4
code: CODE04
name: ORGFOURXX

69 changes: 69 additions & 0 deletions test/commands/console/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,5 +226,74 @@ describe('ConsoleCommand', () => {
command.clearConfig()
expect(config.delete).toHaveBeenCalledWith(CONFIG_KEYS.CONSOLE)
})

test('getOrgFeatures', async () => {
command.cliEnv = STAGE_ENV
command.apiKey = API_KEYS[STAGE_ENV]
command.accessToken = 'token'
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue([{ name: 'RUNTIME' }])
})

await expect(command.getOrgFeatures('304327')).resolves.toEqual([{ name: 'RUNTIME' }])
expect(global.fetch).toHaveBeenCalledWith('https://developer-stage.adobe.com/console/api/organizations/304327/features', {
headers: {
accept: 'application/json',
authorization: 'Bearer token',
'x-api-key': API_KEYS[STAGE_ENV]
}
})
})

test('getOrgFeatures returns empty list for non-ok response', async () => {
command.cliEnv = PROD_ENV
command.apiKey = API_KEYS[PROD_ENV]
command.accessToken = 'token'
global.fetch = jest.fn().mockResolvedValue({ ok: false })

await expect(command.getOrgFeatures('304327')).resolves.toEqual([])
})

test('getOrgFeatures falls back to prod url for unknown env', async () => {
command.cliEnv = 'unknown-env'
command.apiKey = API_KEYS[PROD_ENV]
command.accessToken = 'token'
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue([])
})

await expect(command.getOrgFeatures('304327')).resolves.toEqual([])
expect(global.fetch).toHaveBeenCalledWith('https://developer.adobe.com/console/api/organizations/304327/features', expect.any(Object))
})

test('hasRuntimeFeature', async () => {
command.getOrgFeatures = jest.fn().mockResolvedValue([{ name: 'RUNTIME' }])
await expect(command.hasRuntimeFeature('304327')).resolves.toBe(true)

command.getOrgFeatures = jest.fn().mockResolvedValue([{ name: 'OTHER' }])
await expect(command.hasRuntimeFeature('304327')).resolves.toBe(false)
})

test('hasRuntimeFeature returns false when feature lookup fails', async () => {
command.getOrgFeatures = jest.fn().mockRejectedValue(new Error('bad feature lookup'))
await expect(command.hasRuntimeFeature('304327')).resolves.toBe(false)
})

test('getSelectableOrgs includes enterprise orgs and developer orgs with Runtime', async () => {
command.hasRuntimeFeature = jest.fn(orgId => Promise.resolve(orgId === 'developer-runtime'))
const orgs = [
{ id: 'enterprise', type: 'entp' },
{ id: 'developer-runtime', type: 'developer' },
{ id: 'developer-no-runtime', type: 'developer' },
{ id: 'other', type: 'other' }
]

await expect(command.getSelectableOrgs(orgs)).resolves.toEqual([
{ id: 'enterprise', type: 'entp' },
{ id: 'developer-runtime', type: 'developer' }
])
})
})
})
10 changes: 8 additions & 2 deletions test/commands/console/org/list.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ function setDefaultMockConsoleCLI () {
{ id: 1, code: 'CODE01', name: 'ORG01', type: 'entp' },
{ id: 2, code: 'CODE02', name: 'ORG02', type: 'entp' },
{ id: 3, code: 'CODE03', name: 'ORG03', type: 'entp' },
{ id: 3, code: 'CODE03', name: 'ORG03', type: 'not_entp' }
{ id: 4, code: 'CODE04', name: 'ORGFOURXX', type: 'developer' },
{ id: 5, code: 'CODE05', name: 'ORG05', type: 'not_entp' },
{ id: 6, code: 'CODE06', name: 'ORG06', type: 'developer' }
])
}
jest.mock('@adobe/aio-cli-lib-console', () => ({
Expand All @@ -31,6 +33,10 @@ const ListCommand = require('../../../../src/commands/console/org/list')
let command
beforeEach(() => {
setDefaultMockConsoleCLI()
global.fetch = jest.fn(url => Promise.resolve({
ok: true,
json: () => Promise.resolve(url.endsWith('/4/features') ? [{ name: 'RUNTIME' }] : [])
}))
command = new ListCommand([])
})

Expand Down Expand Up @@ -64,7 +70,7 @@ describe('console:org:list', () => {
})
test('should return list of orgs', async () => {
await expect(command.run()).resolves.not.toThrow()
expect(stdout.output).toMatchFixture('org/list.txt')
expect(stdout.output).toMatchFixtureIgnoreWhite('org/list.txt')
})

test('should return list of orgs as json', async () => {
Expand Down
26 changes: 22 additions & 4 deletions test/commands/console/org/select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ const orgs = [
{ id: '1', code: 'CODE01', name: 'ORG01', type: 'entp' },
{ id: '2', code: 'CODE02', name: 'ORG02', type: 'entp' },
{ id: '3', code: 'CODE03', name: 'ORG03', type: 'entp' },
{ id: '33', code: 'CODE03', name: 'ORG03', type: 'not_entp' }
{ id: '4', code: 'CODE04', name: 'ORG04', type: 'developer' },
{ id: '6', code: 'CODE06', name: 'ORG06', type: 'developer' },
{ id: '33', code: 'CODE33', name: 'ORG33', type: 'not_entp' }
]
const selectableOrgs = orgs.slice(0, 4)
const selectedOrg = { id: '1', code: 'CODE01', name: 'ORG01', type: 'entp' }
const selectedDeveloperOrg = { id: '4', code: 'CODE04', name: 'ORG04', type: 'developer' }
/** @private */
function setDefaultMockConsoleCLI () {
mockConsoleCLIInstance.getOrganizations = jest.fn().mockResolvedValue(orgs)
Expand All @@ -36,6 +40,10 @@ let command
beforeEach(() => {
command = new SelectCommand([])
setDefaultMockConsoleCLI()
global.fetch = jest.fn(url => Promise.resolve({
ok: true,
json: () => Promise.resolve(url.endsWith('/4/features') ? [{ name: 'RUNTIME' }] : [])
}))
config.set.mockReset()
config.delete.mockReset()
})
Expand Down Expand Up @@ -73,7 +81,7 @@ describe('console:org:select', () => {
test('should select the provided org code', async () => {
command.argv = [selectedOrg.code]
await expect(command.run()).resolves.not.toThrow()
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(orgs, { orgCode: selectedOrg.code, orgId: selectedOrg.code })
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(selectableOrgs, { orgCode: selectedOrg.code, orgId: selectedOrg.code })
// stores the org configuration
expect(config.set).toHaveBeenCalledWith('console.org', { code: selectedOrg.code, id: selectedOrg.id, name: selectedOrg.name })
expect(config.delete).toHaveBeenCalledWith('console.project')
Expand All @@ -82,7 +90,7 @@ describe('console:org:select', () => {
test('should select the provided org id', async () => {
command.argv = [selectedOrg.id]
await expect(command.run()).resolves.not.toThrow()
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(orgs, { orgCode: selectedOrg.id, orgId: selectedOrg.id })
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(selectableOrgs, { orgCode: selectedOrg.id, orgId: selectedOrg.id })
// stores the org configuration
expect(config.set).toHaveBeenCalledWith('console.org', { code: selectedOrg.code, id: selectedOrg.id, name: selectedOrg.name })
expect(config.delete).toHaveBeenCalledWith('console.project')
Expand All @@ -91,13 +99,23 @@ describe('console:org:select', () => {
test('should prompt for selection if no org is provided', async () => {
command.argv = []
await expect(command.run()).resolves.not.toThrow()
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(orgs, { orgCode: undefined, orgId: undefined })
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(selectableOrgs, { orgCode: undefined, orgId: undefined })
// stores the org configuration
expect(config.set).toHaveBeenCalledWith('console.org', { code: selectedOrg.code, id: selectedOrg.id, name: selectedOrg.name })
expect(config.delete).toHaveBeenCalledWith('console.project')
expect(config.delete).toHaveBeenCalledWith('console.workspace')
})

test('should select the provided developer org id', async () => {
mockConsoleCLIInstance.promptForSelectOrganization.mockResolvedValue(selectedDeveloperOrg)
command.argv = [selectedDeveloperOrg.id]
await expect(command.run()).resolves.not.toThrow()
expect(mockConsoleCLIInstance.promptForSelectOrganization).toHaveBeenCalledWith(selectableOrgs, { orgCode: selectedDeveloperOrg.id, orgId: selectedDeveloperOrg.id })
expect(config.set).toHaveBeenCalledWith('console.org', { code: selectedDeveloperOrg.code, id: selectedDeveloperOrg.id, name: selectedDeveloperOrg.name })
expect(config.delete).toHaveBeenCalledWith('console.project')
expect(config.delete).toHaveBeenCalledWith('console.workspace')
})

test('throw Error retrieving Orgs', async () => {
mockConsoleCLIInstance.getOrganizations.mockRejectedValue(new Error('error org'))
await expect(command.run()).rejects.toThrow(new Error('error org'))
Expand Down
Loading