diff --git a/src/commands/app/console/index.js b/src/commands/app/console/index.js new file mode 100644 index 00000000..f6ea5367 --- /dev/null +++ b/src/commands/app/console/index.js @@ -0,0 +1,26 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { Help } = require('@oclif/core') +const BaseCommand = require('../../../BaseCommand') + +class ConsoleCommand extends BaseCommand { + async run () { + const help = new Help(this.config) + await help.showHelp(['app:console', '--help']) + } +} + +ConsoleCommand.description = 'Display Adobe Developer Console org/project/workspace configuration for this app' + +ConsoleCommand.args = {} + +module.exports = ConsoleCommand diff --git a/src/commands/app/console/open.js b/src/commands/app/console/open.js new file mode 100644 index 00000000..18a8fb68 --- /dev/null +++ b/src/commands/app/console/open.js @@ -0,0 +1,42 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const open = require('open') +const { getCliEnv } = require('@adobe/aio-lib-env') +const BaseCommand = require('../../../BaseCommand') +const { loadCurrentConfiguration, hasLocalConfiguration } = require('../../../lib/console-helper') +const { OPEN_URLS } = require('../../../lib/defaults') + +class OpenCommand extends BaseCommand { + async run () { + await this.parse(OpenCommand) + + if (!hasLocalConfiguration()) { + this.error('This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.') + } + + const { org, project, workspace } = loadCurrentConfiguration('local') + + let url = `${OPEN_URLS[getCliEnv()]}/${org.id}/${project.id}/` + url += workspace.id ? `workspaces/${workspace.id}/details` : 'overview' + await open(url) + } +} + +OpenCommand.description = 'Open the Adobe Developer Console workspace this app is set to use (as set by `aio app use`) in the default web browser' + +OpenCommand.flags = { + ...BaseCommand.flags +} + +OpenCommand.args = {} + +module.exports = OpenCommand diff --git a/src/commands/app/console/where.js b/src/commands/app/console/where.js new file mode 100644 index 00000000..a3dc6e09 --- /dev/null +++ b/src/commands/app/console/where.js @@ -0,0 +1,59 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { Flags } = require('@oclif/core') +const yaml = require('js-yaml') +const BaseCommand = require('../../../BaseCommand') +const { loadCurrentConfiguration, configString, hasLocalConfiguration } = require('../../../lib/console-helper') +const { EOL } = require('os') + +class WhereCommand extends BaseCommand { + async run () { + const { flags } = await this.parse(WhereCommand) + + if (!hasLocalConfiguration()) { + this.error('This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.') + } + + const currentConfig = loadCurrentConfiguration('local') + + if (flags.json) { + this.log(JSON.stringify(currentConfig, null, 2)) + return + } + if (flags.yml) { + this.log(yaml.dump(JSON.parse(JSON.stringify(currentConfig)), {})) + return + } + + this.log(`This app is set to use:${EOL}${configString(currentConfig)}`) + } +} + +WhereCommand.description = 'Display the Adobe Developer Console org/project/workspace configuration this app is set to use (as set by `aio app use`)' + +WhereCommand.flags = { + ...BaseCommand.flags, + json: Flags.boolean({ + description: 'Output json', + char: 'j', + exclusive: ['yml'] + }), + yml: Flags.boolean({ + description: 'Output yml', + char: 'y', + exclusive: ['json'] + }) +} + +WhereCommand.args = {} + +module.exports = WhereCommand diff --git a/src/commands/app/use.js b/src/commands/app/use.js index 896f6c0d..e42fb5e7 100644 --- a/src/commands/app/use.js +++ b/src/commands/app/use.js @@ -12,6 +12,7 @@ governing permissions and limitations under the License. const BaseCommand = require('../../BaseCommand') const { CONSOLE_CONFIG_KEY, getProjectCredentialType } = require('../../lib/import-helper') const { importConsoleConfig, downloadConsoleConfigToBuffer } = require('../../lib/import') +const { loadCurrentConfiguration, configString, isCompleteConfig } = require('../../lib/console-helper') const { Flags, Args } = require('@oclif/core') const inquirer = require('inquirer') const config = require('@adobe/aio-lib-core-config') @@ -38,9 +39,9 @@ class Use extends BaseCommand { const prompt = inquirer.createPromptModule({ output: process.stderr }) // load local config - const currentConfig = this.loadCurrentConfiguration() - const currentConfigString = this.configString(currentConfig) - const currentConfigIsComplete = this.isCompleteConfig(currentConfig) + const currentConfig = loadCurrentConfiguration() + const currentConfigString = configString(currentConfig) + const currentConfigIsComplete = isCompleteConfig(currentConfig) this.log(`You are currently in:${EOL}${currentConfigString}${EOL}`) if (args.config_file_path) { @@ -55,7 +56,7 @@ class Use extends BaseCommand { // load global console config const globalConfig = this.loadGlobalConfiguration() - const globalConfigString = this.configString(globalConfig, 4) + const globalConfigString = configString(globalConfig, 4) // load from global configuration or select workspace ? const globalOperationFromFlag = flags.global ? 'global' : null @@ -71,7 +72,7 @@ class Use extends BaseCommand { // load the new workspace, project, org config let newConfig if (useOperation === 'global') { - if (!this.isCompleteConfig(globalConfig)) { + if (!isCompleteConfig(globalConfig)) { const message = `Your global Console configuration is incomplete.${EOL}` + 'Use the `aio console` commands to select your Organization, Project, and Workspace.' this.error(message) @@ -129,31 +130,10 @@ class Use extends BaseCommand { } } - loadCurrentConfiguration () { - const projectConfig = config.get('project') || {} - const org = (projectConfig.org && { id: projectConfig.org.id, name: projectConfig.org.name }) || {} - const project = { name: projectConfig.name, id: projectConfig.id } - const workspace = (projectConfig.workspace && { ...projectConfig.workspace }) || {} - return { org, project, workspace } - } - loadGlobalConfiguration () { return config.get(CONSOLE_CONFIG_KEY) || {} } - configString (config, spaces = 0) { - const { org = {}, project = {}, workspace = {} } = config - const list = [ - `1. Org: ${org.name || ''}`, - `2. Project: ${project.name || ''}`, - `3. Workspace: ${workspace.name || ''}` - ] - - return list - .map(line => ' '.repeat(spaces) + line) - .join(EOL) - } - async promptForUseOperation (prompt, globalConfigString) { const op = await prompt([ { @@ -169,13 +149,6 @@ class Use extends BaseCommand { return op.res } - isCompleteConfig (config) { - return config && - config.org && config.org.id && config.org.name && - config.project && config.project.id && config.project.name && - config.workspace && config.workspace.id && config.workspace.name - } - /** * @param {LibConsoleCLI} consoleCLI lib console config * @param {object} config local configuration @@ -322,9 +295,9 @@ class Use extends BaseCommand { async finalLogMessage (consoleConfig) { const config = { org: consoleConfig.project.org, project: consoleConfig.project, workspace: consoleConfig.project.workspace } - const configString = this.configString(config) + const configStr = configString(config) this.log(chalk.green(chalk.bold( - `${EOL}✔ Successfully imported configuration for:${EOL}${configString}.` + `${EOL}✔ Successfully imported configuration for:${EOL}${configStr}.` ))) } diff --git a/src/lib/console-helper.js b/src/lib/console-helper.js new file mode 100644 index 00000000..affe798b --- /dev/null +++ b/src/lib/console-helper.js @@ -0,0 +1,78 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const config = require('@adobe/aio-lib-core-config') +const { EOL } = require('os') + +/** + * Loads the per-app org/project/workspace configuration, as set by `aio app use`. + * + * @param {string} [source] pass 'local' to only read from the local `.aio` file, + * bypassing the merge with the global config + * @returns {object} { org, project, workspace } + */ +function loadCurrentConfiguration (source) { + const projectConfig = config.get('project', source) || {} + const org = (projectConfig.org && { id: projectConfig.org.id, name: projectConfig.org.name }) || {} + const project = { name: projectConfig.name, id: projectConfig.id } + const workspace = (projectConfig.workspace && { ...projectConfig.workspace }) || {} + return { org, project, workspace } +} + +/** + * @param {object} config { org, project, workspace } + * @param {number} spaces number of leading spaces for each line + * @returns {string} human readable representation of the org/project/workspace configuration + */ +function configString (config, spaces = 0) { + const { org = {}, project = {}, workspace = {} } = config + const list = [ + `1. Org: ${org.name || ''}`, + `2. Project: ${project.name || ''}`, + `3. Workspace: ${workspace.name || ''}` + ] + + return list + .map(line => ' '.repeat(spaces) + line) + .join(EOL) +} + +/** + * @param {object} config { org, project, workspace } + * @returns {boolean} true if org, project, and workspace are all fully defined + */ +function isCompleteConfig (config) { + return config && + config.org && config.org.id && config.org.name && + config.project && config.project.id && config.project.name && + config.workspace && config.workspace.id && config.workspace.name +} + +/** + * Checks whether a local `.aio` file (as written by `aio app use`) identifies an + * Org and Project on its own. Workspace is intentionally not required here, as + * an Org/Project can be locally selected before a Workspace is (e.g. via `aio app use --global` + * flows or partial imports) - callers should treat a missing local Workspace as + * "no workspace selected", not as "no local configuration at all". + * + * @returns {boolean} true if the local `.aio` file identifies an Org and Project + */ +function hasLocalConfiguration () { + const { org, project } = loadCurrentConfiguration('local') + return !!(org.id && org.name && project.id && project.name) +} + +module.exports = { + loadCurrentConfiguration, + configString, + isCompleteConfig, + hasLocalConfiguration +} diff --git a/src/lib/defaults.js b/src/lib/defaults.js index 9c7cf028..bb1258da 100644 --- a/src/lib/defaults.js +++ b/src/lib/defaults.js @@ -28,6 +28,10 @@ module.exports = { prod: 'aio-cli-console-auth', stage: 'aio-cli-console-auth-stage' }, + OPEN_URLS: { + prod: 'https://developer.adobe.com/console/projects', + stage: 'https://developer-stage.adobe.com/console/projects' + }, defaultHttpServerPort: 9080, AIO_CONFIG_WORKSPACE_SERVICES: 'project.workspace.details.services', AIO_CONFIG_ORG_SERVICES: 'project.org.details.services', diff --git a/test/commands/app/console/index.test.js b/test/commands/app/console/index.test.js new file mode 100644 index 00000000..c91aeca6 --- /dev/null +++ b/test/commands/app/console/index.test.js @@ -0,0 +1,22 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const TheCommand = require('../../../../src/commands/app/console/index.js') +const { Help } = require('@oclif/core') + +test('returns help file for app:console command', () => { + const command = new TheCommand([]) + command.config = global.createOclifMockConfig() + const spy = jest.spyOn(Help.prototype, 'showHelp').mockReturnValue(true) + return command.run().then(() => { + expect(spy).toHaveBeenCalledWith(['app:console', '--help']) + }) +}) diff --git a/test/commands/app/console/open.test.js b/test/commands/app/console/open.test.js new file mode 100644 index 00000000..6308d04e --- /dev/null +++ b/test/commands/app/console/open.test.js @@ -0,0 +1,119 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +jest.mock('open', () => jest.fn()) +jest.mock('@adobe/aio-lib-core-config') + +const TheCommand = require('../../../../src/commands/app/console/open') +const BaseCommand = require('../../../../src/BaseCommand') +const open = require('open') +const mockConfig = require('@adobe/aio-lib-core-config') +const libEnv = require('@adobe/aio-lib-env') + +let fakeCurrentConfig = {} +/** @private */ +function setConfigMock (localConfig = fakeCurrentConfig) { + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project') { + return source === 'local' ? localConfig : fakeCurrentConfig + } + }) +} + +beforeEach(() => { + jest.clearAllMocks() + libEnv.getCliEnv.mockReturnValue('prod') + fakeCurrentConfig = { + name: 'projectname', + id: 'projectid', + org: { name: 'org name', id: 'org-id' }, + workspace: { name: 'workspacename', id: 'workspaceid' } + } + setConfigMock() +}) + +test('exports', async () => { + expect(typeof TheCommand).toEqual('function') + expect(TheCommand.prototype instanceof BaseCommand).toBeTruthy() + expect(typeof TheCommand.description).toBe('string') +}) + +test('opens the workspace details url for a complete config (prod)', async () => { + await TheCommand.run([]) + expect(open).toHaveBeenCalledWith( + 'https://developer.adobe.com/console/projects/org-id/projectid/workspaces/workspaceid/details' + ) +}) + +test('opens the stage url when the cli env is stage', async () => { + libEnv.getCliEnv.mockReturnValue('stage') + await TheCommand.run([]) + expect(open).toHaveBeenCalledWith( + 'https://developer-stage.adobe.com/console/projects/org-id/projectid/workspaces/workspaceid/details' + ) +}) + +test('opens the project overview url when there is no workspace selected', async () => { + delete fakeCurrentConfig.workspace + setConfigMock() + await TheCommand.run([]) + expect(open).toHaveBeenCalledWith( + 'https://developer.adobe.com/console/projects/org-id/projectid/overview' + ) +}) + +test('errors when org is missing locally', async () => { + delete fakeCurrentConfig.org + setConfigMock() + await expect(TheCommand.run([])).rejects.toThrow( + 'This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.' + ) + expect(open).not.toHaveBeenCalled() +}) + +test('errors when project is missing locally', async () => { + fakeCurrentConfig = {} + setConfigMock() + await expect(TheCommand.run([])).rejects.toThrow( + 'This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.' + ) + expect(open).not.toHaveBeenCalled() +}) + +test('errors when no local .aio configuration is found', async () => { + setConfigMock(undefined) + await expect(TheCommand.run([])).rejects.toThrow( + 'This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.' + ) + expect(open).not.toHaveBeenCalled() +}) + +test('global config is never used, even if it defines a complete org/project/workspace', async () => { + // local config only has org+project, no workspace - global has a full (different) config + delete fakeCurrentConfig.workspace + setConfigMock() + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project') { + if (source === 'local') return fakeCurrentConfig + // global/merged fallback: a different, complete project - must never be used + return { + name: 'globalprojectname', + id: 'globalprojectid', + org: { name: 'global org name', id: 'global-org-id' }, + workspace: { name: 'globalworkspacename', id: 'globalworkspaceid' } + } + } + }) + await TheCommand.run([]) + expect(open).toHaveBeenCalledWith( + 'https://developer.adobe.com/console/projects/org-id/projectid/overview' + ) +}) diff --git a/test/commands/app/console/where.test.js b/test/commands/app/console/where.test.js new file mode 100644 index 00000000..6df4e034 --- /dev/null +++ b/test/commands/app/console/where.test.js @@ -0,0 +1,135 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const TheCommand = require('../../../../src/commands/app/console/where') +const BaseCommand = require('../../../../src/BaseCommand') +const { EOL } = require('os') + +jest.mock('@adobe/aio-lib-core-config') +const mockConfig = require('@adobe/aio-lib-core-config') + +let fakeCurrentConfig = {} +/** @private */ +function setConfigMock (localConfig = fakeCurrentConfig) { + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project') { + return source === 'local' ? localConfig : fakeCurrentConfig + } + }) +} + +beforeEach(() => { + jest.clearAllMocks() + fakeCurrentConfig = { + name: 'projectname', + id: 'projectid', + org: { name: 'org name', id: 'org-id' }, + workspace: { name: 'workspacename', id: 'workspaceid' } + } + setConfigMock() +}) + +test('exports', async () => { + expect(typeof TheCommand).toEqual('function') + expect(TheCommand.prototype instanceof BaseCommand).toBeTruthy() + expect(typeof TheCommand.description).toBe('string') +}) + +test('flags', async () => { + expect(TheCommand.flags.json.char).toEqual('j') + expect(TheCommand.flags.json.exclusive).toEqual(['yml']) + expect(TheCommand.flags.yml.char).toEqual('y') + expect(TheCommand.flags.yml.exclusive).toEqual(['json']) +}) + +test('--json and --yml cannot be used together', async () => { + await expect(TheCommand.run(['--json', '--yml'])).rejects.toThrow() +}) + +test('errors when no local .aio configuration is found', async () => { + setConfigMock(undefined) + await expect(TheCommand.run([])).rejects.toThrow( + 'This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.' + ) +}) + +describe('text output', () => { + test('complete config', async () => { + const logSpy = jest.spyOn(TheCommand.prototype, 'log').mockReturnValue() + await TheCommand.run([]) + expect(logSpy).toHaveBeenCalledWith(`This app is set to use:${EOL}` + [ + '1. Org: org name', + '2. Project: projectname', + '3. Workspace: workspacename' + ].join(EOL)) + }) + + test('errors when local .aio config is present but empty', async () => { + fakeCurrentConfig = {} + setConfigMock() + await expect(TheCommand.run([])).rejects.toThrow( + 'This app is not set to use any Org/Project/Workspace yet. Run `aio app use` to configure it.' + ) + }) + + test('shows the no-workspace-selected placeholder when local .aio has no workspace', async () => { + delete fakeCurrentConfig.workspace + setConfigMock() + const logSpy = jest.spyOn(TheCommand.prototype, 'log').mockReturnValue() + await TheCommand.run([]) + expect(logSpy).toHaveBeenCalledWith(`This app is set to use:${EOL}` + [ + '1. Org: org name', + '2. Project: projectname', + '3. Workspace: ' + ].join(EOL)) + }) + + test('global config is never used, even if it defines a complete org/project/workspace', async () => { + delete fakeCurrentConfig.workspace + setConfigMock() + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project') { + if (source === 'local') return fakeCurrentConfig + // global/merged fallback: a different, complete project - must never be used + return { + name: 'globalprojectname', + id: 'globalprojectid', + org: { name: 'global org name', id: 'global-org-id' }, + workspace: { name: 'globalworkspacename', id: 'globalworkspaceid' } + } + } + }) + const logSpy = jest.spyOn(TheCommand.prototype, 'log').mockReturnValue() + await TheCommand.run([]) + expect(logSpy).toHaveBeenCalledWith(`This app is set to use:${EOL}` + [ + '1. Org: org name', + '2. Project: projectname', + '3. Workspace: ' + ].join(EOL)) + }) +}) + +test('--json output', async () => { + const logSpy = jest.spyOn(TheCommand.prototype, 'log').mockReturnValue() + await TheCommand.run(['--json']) + expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ + org: { name: 'org name', id: 'org-id' }, + project: { name: 'projectname', id: 'projectid' }, + workspace: { name: 'workspacename', id: 'workspaceid' } + }, null, 2)) +}) + +test('--yml output', async () => { + const logSpy = jest.spyOn(TheCommand.prototype, 'log').mockReturnValue() + await TheCommand.run(['--yml']) + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('org:')) + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('name: org name')) +}) diff --git a/test/lib/console-helper.test.js b/test/lib/console-helper.test.js new file mode 100644 index 00000000..749ddbf8 --- /dev/null +++ b/test/lib/console-helper.test.js @@ -0,0 +1,156 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { EOL } = require('os') + +jest.mock('@adobe/aio-lib-core-config') +const mockConfig = require('@adobe/aio-lib-core-config') + +const { loadCurrentConfiguration, configString, isCompleteConfig, hasLocalConfiguration } = require('../../src/lib/console-helper') + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('loadCurrentConfiguration', () => { + test('complete project config', () => { + mockConfig.get.mockImplementation(k => { + if (k === 'project') { + return { + name: 'projectname', + id: 'projectid', + org: { name: 'org name', id: 'org-id' }, + workspace: { name: 'workspacename', id: 'workspaceid' } + } + } + }) + expect(loadCurrentConfiguration()).toEqual({ + org: { name: 'org name', id: 'org-id' }, + project: { name: 'projectname', id: 'projectid' }, + workspace: { name: 'workspacename', id: 'workspaceid' } + }) + }) + + test('no project config set', () => { + mockConfig.get.mockReturnValue(undefined) + expect(loadCurrentConfiguration()).toEqual({ + org: {}, + project: { name: undefined, id: undefined }, + workspace: {} + }) + }) + + test('project config without org/workspace', () => { + mockConfig.get.mockReturnValue({ name: 'projectname', id: 'projectid' }) + expect(loadCurrentConfiguration()).toEqual({ + org: {}, + project: { name: 'projectname', id: 'projectid' }, + workspace: {} + }) + }) +}) + +describe('configString', () => { + test('complete config', () => { + const result = configString({ + org: { name: 'my org' }, + project: { name: 'my project' }, + workspace: { name: 'my workspace' } + }) + expect(result).toEqual([ + '1. Org: my org', + '2. Project: my project', + '3. Workspace: my workspace' + ].join(EOL)) + }) + + test('empty config uses placeholders', () => { + const result = configString({}) + expect(result).toEqual([ + '1. Org: ', + '2. Project: ', + '3. Workspace: ' + ].join(EOL)) + }) + + test('indents by the given number of spaces', () => { + const result = configString({ org: { name: 'my org' } }, 4) + expect(result.split(EOL)[0]).toEqual(' 1. Org: my org') + }) +}) + +describe('isCompleteConfig', () => { + test('complete config', () => { + expect(isCompleteConfig({ + org: { id: 'oid', name: 'oname' }, + project: { id: 'pid', name: 'pname' }, + workspace: { id: 'wid', name: 'wname' } + })).toBeTruthy() + }) + + test('missing org', () => { + expect(isCompleteConfig({ + project: { id: 'pid', name: 'pname' }, + workspace: { id: 'wid', name: 'wname' } + })).toBeFalsy() + }) + + test('null config', () => { + expect(isCompleteConfig(null)).toBeFalsy() + }) +}) + +describe('hasLocalConfiguration', () => { + test('local .aio fully defines org, project, and workspace', () => { + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project' && source === 'local') { + return { + id: 'projectid', + name: 'projectname', + org: { id: 'org-id', name: 'org name' }, + workspace: { id: 'workspaceid', name: 'workspacename' } + } + } + }) + expect(hasLocalConfiguration()).toBe(true) + }) + + test('no local .aio file (only global config)', () => { + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project' && source === 'local') { + return undefined + } + }) + expect(hasLocalConfiguration()).toBe(false) + }) + + test('local .aio defines org/project but is missing workspace (workspace is optional)', () => { + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project' && source === 'local') { + return { + id: 'projectid', + name: 'projectname', + org: { id: 'org-id', name: 'org name' } + } + } + }) + expect(hasLocalConfiguration()).toBe(true) + }) + + test('local .aio defines a project but is missing org', () => { + mockConfig.get.mockImplementation((k, source) => { + if (k === 'project' && source === 'local') { + return { id: 'projectid', name: 'projectname' } + } + }) + expect(hasLocalConfiguration()).toBe(false) + }) +})