From afdf77c960c990f7daa445532789aebb9dc15a53 Mon Sep 17 00:00:00 2001 From: Piotr Grzesik Date: Tue, 20 Jul 2021 14:32:14 +0200 Subject: [PATCH] refactor(CLI Onboarding): Move `dashboard-set-org` from plugin --- lib/cli/interactive-setup/dashboard-login.js | 3 - .../interactive-setup/dashboard-set-org.js | 383 +++++ lib/cli/interactive-setup/index.js | 2 +- lib/cli/interactive-setup/utils.js | 48 + .../.serverlessrc | 31 + .../serverless.yml | 5 + .../aws-loggedin-noapp-service/.serverlessrc | 31 + .../aws-loggedin-noapp-service/serverless.yml | 4 + .../.serverlessrc | 31 + .../serverless.yml | 5 + .../.serverlessrc | 31 + .../serverless.yml | 5 + .../dashboard-set-org.test.js | 1271 +++++++++++++++++ 13 files changed, 1846 insertions(+), 4 deletions(-) create mode 100644 lib/cli/interactive-setup/dashboard-set-org.js create mode 100644 test/fixtures/programmatic/aws-loggedin-monitored-service/.serverlessrc create mode 100644 test/fixtures/programmatic/aws-loggedin-monitored-service/serverless.yml create mode 100644 test/fixtures/programmatic/aws-loggedin-noapp-service/.serverlessrc create mode 100644 test/fixtures/programmatic/aws-loggedin-noapp-service/serverless.yml create mode 100644 test/fixtures/programmatic/aws-loggedin-wrongapp-service/.serverlessrc create mode 100644 test/fixtures/programmatic/aws-loggedin-wrongapp-service/serverless.yml create mode 100644 test/fixtures/programmatic/aws-loggedin-wrongorg-service/.serverlessrc create mode 100644 test/fixtures/programmatic/aws-loggedin-wrongorg-service/serverless.yml create mode 100644 test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js diff --git a/lib/cli/interactive-setup/dashboard-login.js b/lib/cli/interactive-setup/dashboard-login.js index 458922f04ff..68389644c1b 100644 --- a/lib/cli/interactive-setup/dashboard-login.js +++ b/lib/cli/interactive-setup/dashboard-login.js @@ -4,7 +4,6 @@ const _ = require('lodash'); const { ServerlessSDK } = require('@serverless/platform-client'); const login = require('@serverless/dashboard-plugin/lib/login'); const configUtils = require('@serverless/utils/config'); -const { StepHistory } = require('@serverless/utils/telemetry'); const loginOrRegisterQuestion = async (inquirer) => ( @@ -66,8 +65,6 @@ module.exports = { return !isLoggedIn; }, async run(context) { - // TODO: Remove check for `StepHistory` after releasing new major version - if (!_.get(context.stepHistory, 'set')) context.stepHistory = new StepHistory(); process.stdout.write('You are not logged in or you do not have a Serverless account.\n\n'); return steps.loginOrRegister(context); }, diff --git a/lib/cli/interactive-setup/dashboard-set-org.js b/lib/cli/interactive-setup/dashboard-set-org.js new file mode 100644 index 00000000000..495b016792b --- /dev/null +++ b/lib/cli/interactive-setup/dashboard-set-org.js @@ -0,0 +1,383 @@ +'use strict'; + +const _ = require('lodash'); +const chalk = require('chalk'); +const accountUtils = require('@serverless/utils/account'); +const configUtils = require('@serverless/utils/config'); +const { ServerlessSDK } = require('@serverless/platform-client'); +const { writeOrgAndApp } = require('./utils'); +const { + getPlatformClientWithAccessKey, + getOrCreateAccessKeyForOrg, +} = require('@serverless/dashboard-plugin/lib/clientUtils'); + +const isValidAppName = RegExp.prototype.test.bind(/^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/); + +const orgUpdateConfirm = async (inquirer, stepHistory) => { + process.stdout.write( + "Service has monitoring enabled, but it is configured with the 'org' to which you do not have access\n\n" + ); + const shouldUpdateOrg = ( + await inquirer.prompt({ + message: 'Would you like to update it?', + type: 'confirm', + name: 'shouldUpdateOrg', + }) + ).shouldUpdateOrg; + stepHistory.set('shouldUpdateOrg', shouldUpdateOrg); + return shouldUpdateOrg; +}; +const appUpdateConfirm = async (inquirer, appName, orgName, stepHistory) => { + process.stdout.write( + "Service seems to have monitoring enabled, but configured app doesn't seem to exist in an organization.\n\n" + ); + + const appUpdateType = ( + await inquirer.prompt({ + message: 'What would you like to do?', + type: 'list', + name: 'appUpdateType', + choices: [ + { name: `Create '${appName}' app in '${orgName}' org`, value: 'create' }, + { + name: 'Switch to one of the existing apps (or create new one with different name)', + value: 'chooseExisting', + }, + { name: "Skip, I'll sort this out manually", value: 'skip' }, + ], + }) + ).appUpdateType; + stepHistory.set('appUpdateType', appUpdateType); + return appUpdateType; +}; + +const orgsChoice = async (inquirer, orgNames, stepHistory) => { + const orgName = ( + await inquirer.prompt({ + message: 'What org do you want to add this to?', + type: 'list', + name: 'orgName', + choices: [...orgNames, { name: '[Skip]', value: '_skip_' }], + }) + ).orgName; + stepHistory.set('orgName', orgName.startsWith('_') ? orgName : '_user_provided_'); + return orgName; +}; + +const appNameChoice = async (inquirer, appNames, stepHistory) => { + const appName = ( + await inquirer.prompt({ + message: 'What application do you want to add this to?', + type: 'list', + name: 'appName', + choices: Array.from(appNames).concat({ name: '[create a new app]', value: '_create_' }), + }) + ).appName; + stepHistory.set('appName', appName.startsWith('_') ? appName : '_user_provided_'); + return appName; +}; + +const appNameInput = async (inquirer, appNames, stepHistory) => { + const appName = ( + await inquirer.prompt({ + message: 'What do you want to name this application?', + type: 'input', + name: 'newAppName', + validate: (input) => { + input = input.trim(); + if (!isValidAppName(input)) { + return ( + 'App name is not valid.\n' + + ' - It should only contain lowercase alphanumeric and hyphens.\n' + + ' - It should start and end with an alphanumeric character.\n' + + " - Shouldn't exceed 128 characters" + ); + } + if (appNames.includes(input)) return 'App of this name already exists'; + return true; + }, + }) + ).newAppName.trim(); + stepHistory.set('newAppName', '_user_provided_'); + return appName; +}; + +const steps = { + resolveOrgNames: async (user) => { + if (process.env.SERVERLESS_ACCESS_KEY) { + const sdk = new ServerlessSDK({ accessKey: process.env.SERVERLESS_ACCESS_KEY }); + const { orgName } = await sdk.accessKeys.get(); + return new Set([orgName]); + } + + let orgs = new Set(); + if (!user.idToken) { + // User registered over CLI hence idToken is not stored. + // Still to resolve organizations from platform idToken is needed. + // Handling it gently by assuming that orgs listed in config file + // make a valid representation + for (const org of Object.keys(user.accessKeys)) orgs.add(org); + } else { + const sdk = new ServerlessSDK(); + await accountUtils.refreshToken(sdk); + user = configUtils.getLoggedInUser(); + sdk.config({ accessKey: user.idToken }); + orgs = new Set( + (await sdk.organizations.list({ username: user.username })).map((org) => org.tenantName) + ); + } + return orgs; + }, + setOrgAndApp: async ( + context, + { + orgNames, + orgName, + apps, + appName, + newAppName, + isDashboardMonitoringOverridenByCli, + isDashboardAppPreconfigured, + } + ) => { + const { inquirer, history, stepHistory } = context; + if (!orgName) { + orgName = await (async () => { + // We only want to automatically select the single available org in situations where user explicitly + // logged in/registered during the process and created new service, for existing services we want to always ask + // that question. It will also be always asked if `SERVERLESS_ACCESS_KEY` was provided + if ( + orgNames.size === 1 && + history && + history.has('dashboardLogin') && + history.has('service') + ) { + return orgNames.values().next().value; + } + return orgsChoice(inquirer, orgNames, stepHistory); + })(); + } + + if (orgName === '_skip_') { + return; + } + + const sdk = await getPlatformClientWithAccessKey(orgName); + if (!newAppName && !appName) { + if (!apps) apps = await sdk.apps.list({ orgName }); + + const appNames = apps.map((app) => app.appName); + + if (!apps.length) { + newAppName = context.configuration.service; + } else { + appName = await appNameChoice(inquirer, appNames, stepHistory); + if (appName === '_create_') { + newAppName = await appNameInput(inquirer, appNames, stepHistory); + } + } + } + if (newAppName) { + ({ appName } = await sdk.apps.create({ orgName, app: { name: newAppName } })); + } + if (isDashboardMonitoringOverridenByCli && isDashboardAppPreconfigured) { + const { shouldOverrideDashboardConfig } = await inquirer.prompt({ + message: + 'Are you sure you want to update monitoring settings ' + + `to ${chalk.bold(`app: ${appName}, org: ${orgName}`)}`, + type: 'confirm', + name: 'shouldOverrideDashboardConfig', + }); + stepHistory.set('shouldOverrideDashboardConfig', shouldOverrideDashboardConfig); + if (!shouldOverrideDashboardConfig) { + delete context.configuration.app; + delete context.configuration.org; + return; + } + } + process.stdout.write( + `\n${chalk.green('Your project has been setup with org ')}${chalk.white.bold( + orgName + )}${chalk.green(' and app ')}${chalk.white.bold(appName)}\n` + ); + await writeOrgAndApp(orgName, appName, context); + return; + }, +}; + +module.exports = { + async isApplicable(context) { + const { configuration, options, serviceDir } = context; + if (!serviceDir) { + context.inapplicabilityReasonCode = 'NOT_IN_SERVICE_DIRECTORY'; + return false; + } + + if ( + _.get(configuration, 'provider') !== 'aws' && + _.get(configuration, 'provider.name') !== 'aws' + ) { + context.inapplicabilityReasonCode = 'NON_AWS_PROVIDER'; + return false; + } + const sdk = new ServerlessSDK(); + const { supportedRegions, supportedRuntimes } = await sdk.metadata.get(); + if (!supportedRuntimes.includes(_.get(configuration.provider, 'runtime') || 'nodejs12.x')) { + context.inapplicabilityReasonCode = 'UNSUPPORTED_RUNTIME'; + return false; + } + if ( + !supportedRegions.includes(options.region || configuration.provider.region || 'us-east-1') + ) { + context.inapplicabilityReasonCode = 'UNSUPPORTED_REGION'; + return false; + } + const usesServerlessAccessKey = Boolean(process.env.SERVERLESS_ACCESS_KEY); + + let user = configUtils.getLoggedInUser(); + if (!user && !usesServerlessAccessKey) { + context.inapplicabilityReasonCode = 'NOT_LOGGED_IN'; + return false; + } + + const orgNames = await steps.resolveOrgNames(user); + if (!orgNames.size) { + context.inapplicabilityReasonCode = 'NO_ORGS_AVAILABLE'; + return false; + } + if (!usesServerlessAccessKey) { + user = configUtils.getLoggedInUser(); // Refreshed, as new token might have been generated + } + + const orgName = options.org || configuration.org; + const appName = options.app || configuration.app; + + const isDashboardMonitoringPreconfigured = Boolean(configuration.org); + const isDashboardAppPreconfigured = Boolean(configuration.app); + const isDashboardMonitoringOverridenByCli = + isDashboardMonitoringPreconfigured && + ((options.org && options.org !== configuration.org) || + (options.app && options.app !== configuration.app)); + if (orgName && orgNames.has(orgName)) { + if (!isValidAppName(appName)) { + return { + user, + orgName, + isDashboardMonitoringPreconfigured, + isDashboardAppPreconfigured, + isDashboardMonitoringOverridenByCli, + }; + } + + const accessKey = await getOrCreateAccessKeyForOrg(orgName); + sdk.config({ accessKey }); + const apps = await sdk.apps.list({ orgName }); + + if (options.org || options.app) { + if (apps.some((app) => app.appName === appName)) { + if ( + isDashboardMonitoringPreconfigured && + isDashboardAppPreconfigured && + !isDashboardMonitoringOverridenByCli + ) { + context.inapplicabilityReasonCode = 'MONITORING_NOT_OVERRIDEN_BY_CLI'; + return false; + } + return { + user, + orgName, + appName, + isDashboardMonitoringPreconfigured, + isDashboardAppPreconfigured, + isDashboardMonitoringOverridenByCli, + }; + } + if (options.app) { + process.stdout.write( + chalk.red( + "\nPassed value for `--app` doesn't seem to correspond to chosen organization.\n" + ) + ); + } + return { + user, + orgName, + isDashboardMonitoringPreconfigured, + isDashboardAppPreconfigured, + isDashboardMonitoringOverridenByCli, + }; + } else if (apps.some((app) => app.appName === appName)) { + context.inapplicabilityReasonCode = 'HAS_MONITORING_SETUP'; + return false; + } + return { + user, + orgName, + apps, + newAppName: appName, + isDashboardMonitoringPreconfigured, + isDashboardAppPreconfigured, + isDashboardMonitoringOverridenByCli, + }; + } else if (orgName) { + if (options.org) { + process.stdout.write( + chalk.red( + "\nPassed value for `--org` doesn't seem to correspond to account with which you're logged in with.\n" + ) + ); + } else { + process.stdout.write( + chalk.red(`\nConfigured org '${orgName}' is not available in your account.\n`) + ); + } + } + return { + user, + orgNames, + isDashboardMonitoringPreconfigured, + isDashboardAppPreconfigured, + isDashboardMonitoringOverridenByCli, + }; + }, + async run(context, stepData) { + const { inquirer, configuration, options, stepHistory } = context; + if (!stepData.orgName) delete configuration.org; + if (!stepData.appName && !stepData.newAppName) delete configuration.app; + if (!options.org && !options.app) { + if (stepData.isDashboardMonitoringPreconfigured) { + if (!stepData.orgName) { + if (!(await orgUpdateConfirm(inquirer, stepHistory))) return; + } else if (stepData.newAppName) { + const appUpdateTypeChoice = await appUpdateConfirm( + inquirer, + stepData.newAppName, + stepData.orgName, + stepHistory + ); + switch (appUpdateTypeChoice) { + case 'create': + break; + case 'chooseExisting': + delete stepData.newAppName; + break; + case 'skip': + return; + default: + throw new Error('Unexpected app update type'); + } + } + } + } + await steps.setOrgAndApp(context, stepData); + }, + steps, + configuredQuestions: [ + 'orgName', + 'appName', + 'newAppName', + 'shouldUpdateOrg', + 'appUpdateType', + 'shouldOverrideDashboardConfig', + ], +}; diff --git a/lib/cli/interactive-setup/index.js b/lib/cli/interactive-setup/index.js index 70af04f815f..f28c52283aa 100644 --- a/lib/cli/interactive-setup/index.js +++ b/lib/cli/interactive-setup/index.js @@ -7,7 +7,7 @@ const { resolveInitialContext } = require('./utils'); const steps = { service: require('./service'), dashboardLogin: require('./dashboard-login'), - dashboardSetOrg: require('@serverless/dashboard-plugin/lib/cli/interactive-setup/dashboard-set-org'), + dashboardSetOrg: require('./dashboard-set-org'), awsCredentials: require('./aws-credentials'), deploy: require('./deploy'), }; diff --git a/lib/cli/interactive-setup/utils.js b/lib/cli/interactive-setup/utils.js index 8d392b02947..df4b1c99bc1 100644 --- a/lib/cli/interactive-setup/utils.js +++ b/lib/cli/interactive-setup/utils.js @@ -1,10 +1,18 @@ 'use strict'; +const path = require('path'); const inquirer = require('@serverless/utils/inquirer'); const resolveProviderCredentials = require('@serverless/dashboard-plugin/lib/resolveProviderCredentials'); const isAuthenticated = require('@serverless/dashboard-plugin/lib/isAuthenticated'); const hasLocalCredentials = require('../../aws/has-local-credentials'); +const fsp = require('fs').promises; + +const yamlExtensions = new Set(['.yml', '.yaml']); + +const appPattern = /^(?:#\s*)?app\s*:.+/m; +const orgPattern = /^(?:#\s*)?(?:tenant|org)\s*:.+/m; + const ServerlessError = require('../../serverless-error'); const resolveStage = require('../../utils/resolve-stage'); const resolveRegion = require('../../utils/resolve-region'); @@ -46,4 +54,44 @@ module.exports = { isDashboardEnabled: Boolean(configuration && configuration.org && configuration.app), }; }, + writeOrgAndApp: async ( + orgName, + appName, + { configurationFilename, serviceDir, configuration } + ) => { + const configurationFilePath = path.resolve(serviceDir, configurationFilename); + let ymlString = await (async () => { + if (!yamlExtensions.has(path.extname(configurationFilename))) return null; // Non YAML config + try { + return await fsp.readFile(configurationFilePath); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } + })(); + + if (!ymlString) { + process.stdout.write( + 'Add the following settings to your serverless configuration file:\n\n' + + `org: ${orgName}\napp: ${appName}\n` + ); + return; + } + ymlString = ymlString.toString(); + const appMatch = ymlString.match(appPattern); + if (appMatch) { + ymlString = ymlString.replace(appMatch[0], `app: ${appName}`); + } else { + ymlString = `app: ${appName}\n${ymlString}`; + } + const orgMatch = ymlString.match(orgPattern); + if (orgMatch) { + ymlString = ymlString.replace(orgMatch[0], `org: ${orgName}`); + } else { + ymlString = `org: ${orgName}\n${ymlString}`; + } + await fsp.writeFile(configurationFilePath, ymlString); + configuration.org = orgName; + configuration.app = appName; + }, }; diff --git a/test/fixtures/programmatic/aws-loggedin-monitored-service/.serverlessrc b/test/fixtures/programmatic/aws-loggedin-monitored-service/.serverlessrc new file mode 100644 index 00000000000..c28f097c827 --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-monitored-service/.serverlessrc @@ -0,0 +1,31 @@ +{ + "frameworkId": "00000000-0000-0000-0000-000000000000", + "meta": { + "created_at": 1560000000, + "updated_at": 1560000000 + }, + "userId": "testinteractivecli", + "users": { + "testinteractivecli": { + "userId": "testinteractivecli", + "name": "Testing Interactive Cli", + "email": "test-interactive-cli@interactive.cli", + "username": "testinteractivecli", + "dashboard": { + "refreshToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "accessToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "idToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik56azVNREl5TVRnNFJqWTBORGswT0VJM1JrRXpORGN4UmtVMU1FWXdNemczT1VKQlFqRTBNZyJ9.eyJuaWNrbmFtZSI6InRlc3QtaW50ZXJhY3RpdmUtY2xpIiwibmFtZSI6IlRlc3RpbmcgSW50ZXJhY3RpdmUgQ2xpIiwicGljdHVyZSI6Imh0dHBzOi8vaW50ZXJhdGNpdmUuY2xpL3Rlc3RpbmcucG5nIiwidXBkYXRlZF9hdCI6IjIwMTktMDktMTZUMTU6MTg6NDMuOTk5WiIsImVtYWlsIjoidGVzdGluZ0BpbnRlcmFjdGl2ZS5jbGkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXJsZXNzaW5jLmF1dGgwLmNvbS8iLCJzdWIiOiJ0ZXN0LWludGVyYWN0aXZlLWNsaSIsImF1ZCI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYIiwiaWF0IjoxNTYwMDAwMDAwLCJleHAiOjMwMDAwMDAwMDB9.GcNQtWSxv9CHTABw-HIjYSvRxTEapDUDqIIWRGmz01XmShQxRGOHRuUg1NKU4w9MpOlB6txHKs8UWd2eZkzw_Z4QmIuLyAVhVklpWP2-xeysPLUyqVTgqAg8kgIUAwdKjmrdpQqHhGd-Q1BIX62-E-qKKx8prmADSw_hgmuvlMuSCa1ajCnfyUXycQxDmbFrvjd24lJER0FSpB2nWWW3KxZ_UBX-TuVmiEtRXg9GYeSv6oIU78PrIhYgJ0QjERRF1yAYamIXNRs-KZ7Z4YiFNC4uKzFH1524pZkS4Q0-pweIvBrrsjekz-vEYcbaVG1zAxDu_yNrYPk5phCy8MHTrQ", + "expiresAt": 3000000000000, + "username": "testinteractivecli", + "accessKeys": { + "testinteractivecli": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + } + }, + "enterprise": { + "versionSDK": "2.1.1", + "timeLastLogin": 1560000000, + "timeLastLogout": 1560000000 + } + } + } +} diff --git a/test/fixtures/programmatic/aws-loggedin-monitored-service/serverless.yml b/test/fixtures/programmatic/aws-loggedin-monitored-service/serverless.yml new file mode 100644 index 00000000000..9b6fa32d41f --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-monitored-service/serverless.yml @@ -0,0 +1,5 @@ +service: 'some-aws-service' +provider: 'aws' + +org: 'testinteractivecli' +app: 'some-aws-service-app' diff --git a/test/fixtures/programmatic/aws-loggedin-noapp-service/.serverlessrc b/test/fixtures/programmatic/aws-loggedin-noapp-service/.serverlessrc new file mode 100644 index 00000000000..c28f097c827 --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-noapp-service/.serverlessrc @@ -0,0 +1,31 @@ +{ + "frameworkId": "00000000-0000-0000-0000-000000000000", + "meta": { + "created_at": 1560000000, + "updated_at": 1560000000 + }, + "userId": "testinteractivecli", + "users": { + "testinteractivecli": { + "userId": "testinteractivecli", + "name": "Testing Interactive Cli", + "email": "test-interactive-cli@interactive.cli", + "username": "testinteractivecli", + "dashboard": { + "refreshToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "accessToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "idToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik56azVNREl5TVRnNFJqWTBORGswT0VJM1JrRXpORGN4UmtVMU1FWXdNemczT1VKQlFqRTBNZyJ9.eyJuaWNrbmFtZSI6InRlc3QtaW50ZXJhY3RpdmUtY2xpIiwibmFtZSI6IlRlc3RpbmcgSW50ZXJhY3RpdmUgQ2xpIiwicGljdHVyZSI6Imh0dHBzOi8vaW50ZXJhdGNpdmUuY2xpL3Rlc3RpbmcucG5nIiwidXBkYXRlZF9hdCI6IjIwMTktMDktMTZUMTU6MTg6NDMuOTk5WiIsImVtYWlsIjoidGVzdGluZ0BpbnRlcmFjdGl2ZS5jbGkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXJsZXNzaW5jLmF1dGgwLmNvbS8iLCJzdWIiOiJ0ZXN0LWludGVyYWN0aXZlLWNsaSIsImF1ZCI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYIiwiaWF0IjoxNTYwMDAwMDAwLCJleHAiOjMwMDAwMDAwMDB9.GcNQtWSxv9CHTABw-HIjYSvRxTEapDUDqIIWRGmz01XmShQxRGOHRuUg1NKU4w9MpOlB6txHKs8UWd2eZkzw_Z4QmIuLyAVhVklpWP2-xeysPLUyqVTgqAg8kgIUAwdKjmrdpQqHhGd-Q1BIX62-E-qKKx8prmADSw_hgmuvlMuSCa1ajCnfyUXycQxDmbFrvjd24lJER0FSpB2nWWW3KxZ_UBX-TuVmiEtRXg9GYeSv6oIU78PrIhYgJ0QjERRF1yAYamIXNRs-KZ7Z4YiFNC4uKzFH1524pZkS4Q0-pweIvBrrsjekz-vEYcbaVG1zAxDu_yNrYPk5phCy8MHTrQ", + "expiresAt": 3000000000000, + "username": "testinteractivecli", + "accessKeys": { + "testinteractivecli": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + } + }, + "enterprise": { + "versionSDK": "2.1.1", + "timeLastLogin": 1560000000, + "timeLastLogout": 1560000000 + } + } + } +} diff --git a/test/fixtures/programmatic/aws-loggedin-noapp-service/serverless.yml b/test/fixtures/programmatic/aws-loggedin-noapp-service/serverless.yml new file mode 100644 index 00000000000..7600e51819d --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-noapp-service/serverless.yml @@ -0,0 +1,4 @@ +service: 'some-aws-service' +provider: 'aws' + +org: 'testinteractivecli' diff --git a/test/fixtures/programmatic/aws-loggedin-wrongapp-service/.serverlessrc b/test/fixtures/programmatic/aws-loggedin-wrongapp-service/.serverlessrc new file mode 100644 index 00000000000..c28f097c827 --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-wrongapp-service/.serverlessrc @@ -0,0 +1,31 @@ +{ + "frameworkId": "00000000-0000-0000-0000-000000000000", + "meta": { + "created_at": 1560000000, + "updated_at": 1560000000 + }, + "userId": "testinteractivecli", + "users": { + "testinteractivecli": { + "userId": "testinteractivecli", + "name": "Testing Interactive Cli", + "email": "test-interactive-cli@interactive.cli", + "username": "testinteractivecli", + "dashboard": { + "refreshToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "accessToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "idToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik56azVNREl5TVRnNFJqWTBORGswT0VJM1JrRXpORGN4UmtVMU1FWXdNemczT1VKQlFqRTBNZyJ9.eyJuaWNrbmFtZSI6InRlc3QtaW50ZXJhY3RpdmUtY2xpIiwibmFtZSI6IlRlc3RpbmcgSW50ZXJhY3RpdmUgQ2xpIiwicGljdHVyZSI6Imh0dHBzOi8vaW50ZXJhdGNpdmUuY2xpL3Rlc3RpbmcucG5nIiwidXBkYXRlZF9hdCI6IjIwMTktMDktMTZUMTU6MTg6NDMuOTk5WiIsImVtYWlsIjoidGVzdGluZ0BpbnRlcmFjdGl2ZS5jbGkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXJsZXNzaW5jLmF1dGgwLmNvbS8iLCJzdWIiOiJ0ZXN0LWludGVyYWN0aXZlLWNsaSIsImF1ZCI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYIiwiaWF0IjoxNTYwMDAwMDAwLCJleHAiOjMwMDAwMDAwMDB9.GcNQtWSxv9CHTABw-HIjYSvRxTEapDUDqIIWRGmz01XmShQxRGOHRuUg1NKU4w9MpOlB6txHKs8UWd2eZkzw_Z4QmIuLyAVhVklpWP2-xeysPLUyqVTgqAg8kgIUAwdKjmrdpQqHhGd-Q1BIX62-E-qKKx8prmADSw_hgmuvlMuSCa1ajCnfyUXycQxDmbFrvjd24lJER0FSpB2nWWW3KxZ_UBX-TuVmiEtRXg9GYeSv6oIU78PrIhYgJ0QjERRF1yAYamIXNRs-KZ7Z4YiFNC4uKzFH1524pZkS4Q0-pweIvBrrsjekz-vEYcbaVG1zAxDu_yNrYPk5phCy8MHTrQ", + "expiresAt": 3000000000000, + "username": "testinteractivecli", + "accessKeys": { + "testinteractivecli": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + } + }, + "enterprise": { + "versionSDK": "2.1.1", + "timeLastLogin": 1560000000, + "timeLastLogout": 1560000000 + } + } + } +} diff --git a/test/fixtures/programmatic/aws-loggedin-wrongapp-service/serverless.yml b/test/fixtures/programmatic/aws-loggedin-wrongapp-service/serverless.yml new file mode 100644 index 00000000000..560c7b02e7b --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-wrongapp-service/serverless.yml @@ -0,0 +1,5 @@ +service: 'some-aws-service' +provider: 'aws' + +org: 'testinteractivecli' +app: 'not-created-app' diff --git a/test/fixtures/programmatic/aws-loggedin-wrongorg-service/.serverlessrc b/test/fixtures/programmatic/aws-loggedin-wrongorg-service/.serverlessrc new file mode 100644 index 00000000000..c28f097c827 --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-wrongorg-service/.serverlessrc @@ -0,0 +1,31 @@ +{ + "frameworkId": "00000000-0000-0000-0000-000000000000", + "meta": { + "created_at": 1560000000, + "updated_at": 1560000000 + }, + "userId": "testinteractivecli", + "users": { + "testinteractivecli": { + "userId": "testinteractivecli", + "name": "Testing Interactive Cli", + "email": "test-interactive-cli@interactive.cli", + "username": "testinteractivecli", + "dashboard": { + "refreshToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "accessToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "idToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik56azVNREl5TVRnNFJqWTBORGswT0VJM1JrRXpORGN4UmtVMU1FWXdNemczT1VKQlFqRTBNZyJ9.eyJuaWNrbmFtZSI6InRlc3QtaW50ZXJhY3RpdmUtY2xpIiwibmFtZSI6IlRlc3RpbmcgSW50ZXJhY3RpdmUgQ2xpIiwicGljdHVyZSI6Imh0dHBzOi8vaW50ZXJhdGNpdmUuY2xpL3Rlc3RpbmcucG5nIiwidXBkYXRlZF9hdCI6IjIwMTktMDktMTZUMTU6MTg6NDMuOTk5WiIsImVtYWlsIjoidGVzdGluZ0BpbnRlcmFjdGl2ZS5jbGkiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXJsZXNzaW5jLmF1dGgwLmNvbS8iLCJzdWIiOiJ0ZXN0LWludGVyYWN0aXZlLWNsaSIsImF1ZCI6IlhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYIiwiaWF0IjoxNTYwMDAwMDAwLCJleHAiOjMwMDAwMDAwMDB9.GcNQtWSxv9CHTABw-HIjYSvRxTEapDUDqIIWRGmz01XmShQxRGOHRuUg1NKU4w9MpOlB6txHKs8UWd2eZkzw_Z4QmIuLyAVhVklpWP2-xeysPLUyqVTgqAg8kgIUAwdKjmrdpQqHhGd-Q1BIX62-E-qKKx8prmADSw_hgmuvlMuSCa1ajCnfyUXycQxDmbFrvjd24lJER0FSpB2nWWW3KxZ_UBX-TuVmiEtRXg9GYeSv6oIU78PrIhYgJ0QjERRF1yAYamIXNRs-KZ7Z4YiFNC4uKzFH1524pZkS4Q0-pweIvBrrsjekz-vEYcbaVG1zAxDu_yNrYPk5phCy8MHTrQ", + "expiresAt": 3000000000000, + "username": "testinteractivecli", + "accessKeys": { + "testinteractivecli": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + } + }, + "enterprise": { + "versionSDK": "2.1.1", + "timeLastLogin": 1560000000, + "timeLastLogout": 1560000000 + } + } + } +} diff --git a/test/fixtures/programmatic/aws-loggedin-wrongorg-service/serverless.yml b/test/fixtures/programmatic/aws-loggedin-wrongorg-service/serverless.yml new file mode 100644 index 00000000000..146e5545d06 --- /dev/null +++ b/test/fixtures/programmatic/aws-loggedin-wrongorg-service/serverless.yml @@ -0,0 +1,5 @@ +service: 'some-aws-service' +provider: 'aws' + +org: 'some-other' +app: 'some-aws-service-app' diff --git a/test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js b/test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js new file mode 100644 index 00000000000..e50ce5b1335 --- /dev/null +++ b/test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js @@ -0,0 +1,1271 @@ +'use strict'; + +const chai = require('chai'); +const { join } = require('path'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); +const fsp = require('fs').promises; +const yaml = require('js-yaml'); +const overrideCwd = require('process-utils/override-cwd'); +const overrideStdoutWrite = require('process-utils/override-stdout-write'); +const overrideEnv = require('process-utils/override-env'); +const configureInquirerStub = require('@serverless/test/configure-inquirer-stub'); +const stripAnsi = require('strip-ansi'); +const { StepHistory } = require('@serverless/utils/telemetry'); +const inquirer = require('@serverless/utils/inquirer'); + +const fixtures = require('../../../../fixtures/programmatic'); + +const { expect } = chai; + +chai.use(require('chai-as-promised')); + +describe('test/unit/lib/cli/interactive-setup/dashboard-set-org.test.js', function () { + this.timeout(1000 * 60 * 3); + + let step; + let mockOrganizationsList = [ + { tenantName: 'testinteractivecli' }, + { tenantName: 'otherorg' }, + { tenantName: 'orgwithoutapps' }, + ]; + + before(async () => { + const ServerlessSDKMock = class ServerlessSDK { + constructor() { + this.metadata = { + get: async () => { + return { + awsAccountId: '377024778620', + supportedRuntimes: [ + 'nodejs10.x', + 'nodejs12.x', + 'python2.7', + 'python3.6', + 'python3.7', + ], + supportedRegions: [ + 'us-east-1', + 'us-east-2', + 'us-west-2', + 'eu-central-1', + 'eu-west-1', + 'eu-west-2', + 'ap-northeast-1', + 'ap-southeast-1', + 'ap-southeast-2', + ], + }; + }, + }; + + this.apps = { + create: async ({ app: { name } }) => ({ appName: name }), + list: async ({ orgName }) => { + if (orgName === 'orgwithoutapps') { + return []; + } + + return [ + { appName: 'some-aws-service-app' }, + { appName: 'other-app' }, + { appName: 'app-from-flag' }, + ]; + }, + }; + + this.organizations = { + list: async () => { + return mockOrganizationsList; + }, + }; + + this.accessKeys = { + get: async () => { + return { + orgName: 'fromaccesskey', + }; + }, + }; + } + + async refreshToken() { + return {}; + } + + config() {} + }; + + step = proxyquire('../../../../../lib/cli/interactive-setup/dashboard-set-org', { + '@serverless/platform-client': { + ServerlessSDK: ServerlessSDKMock, + }, + '@serverless/dashboard-plugin/lib/clientUtils': { + getPlatformClientWithAccessKey: async () => new ServerlessSDKMock(), + getOrCreateAccessKeyForOrg: async () => 'accessKey', + }, + }); + }); + + // TODO: VERIFY THESE CLEANUPS + after(() => { + sinon.restore(); + }); + + afterEach(async () => { + sinon.reset(); + }); + + it('Should be ineffective, when not at service path', async () => { + const context = {}; + expect(await step.isApplicable(context)).to.be.false; + expect(context.inapplicabilityReasonCode).to.equal('NOT_IN_SERVICE_DIRECTORY'); + }); + + it('Should be ineffective, when not at AWS service path', async () => { + const context = { + serviceDir: process.cwd(), + configuration: {}, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + }; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('NON_AWS_PROVIDER'); + }); + + it('Should be ineffective, when not at supported runtime service path', async () => { + const context = { + serviceDir: process.cwd(), + configuration: { service: 'some-aws-service', provider: { name: 'aws', runtime: 'java8' } }, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + }; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('UNSUPPORTED_RUNTIME'); + }); + + it('Should be ineffective, when not logged in', async () => { + const context = { + serviceDir: process.cwd(), + configuration: { + service: 'some-aws-service', + provider: { name: 'aws', runtime: 'nodejs12.x' }, + }, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + }; + expect(await step.isApplicable(context)).to.equal(false); + expect(context.inapplicabilityReasonCode).to.equal('NOT_LOGGED_IN'); + }); + + it('Should be ineffective, when no orgs are resolved', async () => { + const freshStep = proxyquire('../../../../../lib/cli/interactive-setup/dashboard-set-org', { + '@serverless/platform-client': { + ServerlessSDK: class ServerlessSDK { + constructor() { + this.metadata = { + get: async () => { + return { + awsAccountId: '377024778620', + supportedRuntimes: ['nodejs10.x', 'nodejs12.x'], + supportedRegions: ['us-east-1'], + }; + }, + }; + this.organizations = { + list: async () => [], + }; + } + + config() {} + }, + }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + }; + await overrideCwd(serviceDir, async () => { + expect(await freshStep.isApplicable(context)).to.be.false; + }); + expect(context.inapplicabilityReasonCode).to.equal('NO_ORGS_AVAILABLE'); + }); + + it('Should be ineffective, when project has monitoring setup with recognized org and app', async () => { + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-monitored-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + }; + await overrideCwd(serviceDir, async () => { + expect(await step.isApplicable(context)).to.be.false; + }); + expect(await overrideCwd(serviceDir, async () => await step.isApplicable(context))).to.equal( + false + ); + expect(context.inapplicabilityReasonCode).to.equal('HAS_MONITORING_SETUP'); + }); + + it('Should reject an invalid app name', async () => { + configureInquirerStub(inquirer, { + input: { newAppName: 'invalid app name /* Ć */' }, + list: { orgName: 'testinteractivecli', appName: '_create_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + await expect( + overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }) + ).to.eventually.be.rejected.and.have.property('code', 'INVALID_ANSWER'); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_create_'], + ]) + ); + }); + + it('Should recognize an invalid org and allow to opt out', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldUpdateOrg: false }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-wrongorg-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + expect(context.configuration).to.not.have.property('org'); + expect(context.configuration).to.not.have.property('app'); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['shouldUpdateOrg', false]])); + }); + + it('Should recognize an invalid app and allow to opt out', async () => { + configureInquirerStub(inquirer, { + list: { appUpdateType: 'skip' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-wrongapp-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('not-created-app'); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['appUpdateType', 'skip']])); + }); + + describe('Monitoring setup', () => { + it('Should setup monitoring for chosen org and app', async () => { + configureInquirerStub(inquirer, { + list: { orgName: 'testinteractivecli', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_user_provided_'], + ]) + ); + }); + + it('Should setup monitoring for chosen app and org based on access key', async () => { + configureInquirerStub(inquirer, { + list: { orgName: 'fromaccesskey', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + let stdoutData = ''; + await overrideEnv({ variables: { SERVERLESS_ACCESS_KEY: 'validkey' } }, async () => { + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('fromaccesskey'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('fromaccesskey'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org fromaccesskey and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_user_provided_'], + ]) + ); + }); + + it('Should allow to skip monitoring when org is resolved from access key', async () => { + configureInquirerStub(inquirer, { + list: { orgName: '_skip_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + let stdoutData = ''; + await overrideEnv({ variables: { SERVERLESS_ACCESS_KEY: 'validkey' } }, async () => { + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.be.undefined; + expect(serviceConfig.app).to.be.undefined; + expect(context.configuration.org).to.be.undefined; + expect(context.configuration.app).to.be.undefined; + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['orgName', '_skip_']])); + }); + + it('Should allow to skip setting monitoring when selecting org', async () => { + configureInquirerStub(inquirer, { + list: { orgName: '_skip_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.be.undefined; + expect(serviceConfig.app).to.be.undefined; + expect(context.configuration.org).to.be.undefined; + expect(context.configuration.app).to.be.undefined; + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['orgName', '_skip_']])); + }); + }); + + describe('Monitoring setup when only one org available', () => { + before(() => { + mockOrganizationsList = [{ tenantName: 'orgwithoutapps' }]; + }); + + after(() => { + mockOrganizationsList = [ + { tenantName: 'testinteractivecli' }, + { tenantName: 'otherorg' }, + { tenantName: 'orgwithoutapps' }, + ]; + }); + + it('Should not automatically pre choose single available org if login/register step was not presented', async () => { + configureInquirerStub(inquirer, { + list: { orgName: '_skip_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.be.undefined; + expect(serviceConfig.app).to.be.undefined; + expect(context.configuration.org).to.be.undefined; + expect(context.configuration.app).to.be.undefined; + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['orgName', '_skip_']])); + }); + + it('Should not automatically pre choose single available org if context history is not available', async () => { + configureInquirerStub(inquirer, { + list: { orgName: '_skip_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + stepHistory: new StepHistory(), + }; + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.be.undefined; + expect(serviceConfig.app).to.be.undefined; + expect(context.configuration.org).to.be.undefined; + expect(context.configuration.app).to.be.undefined; + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['orgName', '_skip_']])); + }); + + it('Should not automatically pre choose single available org if login/register step was presented but service step was not', async () => { + configureInquirerStub(inquirer, { + list: { orgName: '_skip_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + history: new Map([['dashboardLogin', []]]), + stepHistory: new StepHistory(), + }; + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.be.undefined; + expect(serviceConfig.app).to.be.undefined; + expect(context.configuration.org).to.be.undefined; + expect(context.configuration.app).to.be.undefined; + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['orgName', '_skip_']])); + }); + + it('Should setup monitoring with the only available org if login/register and service steps were presented', async () => { + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: {}, + inquirer, + history: new Map([ + ['dashboardLogin', []], + ['service', []], + ]), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('orgwithoutapps'); + expect(serviceConfig.app).to.equal(configuration.service); + expect(context.configuration.org).to.equal('orgwithoutapps'); + expect(context.configuration.app).to.equal(configuration.service); + expect(stripAnsi(stdoutData)).to.include( + `Your project has been setup with org orgwithoutapps and app ${configuration.service}` + ); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map()); + }); + }); + + describe('Monitoring setup from CLI flags', () => { + it('Should setup monitoring for chosen org and app', async () => { + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'testinteractivecli', app: 'other-app' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map()); + }); + + it('Should setup monitoring for chosen org and app even if already configured', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldOverrideDashboardConfig: true }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-monitored-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'otherorg', app: 'app-from-flag' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('otherorg'); + expect(serviceConfig.app).to.equal('app-from-flag'); + expect(context.configuration.org).to.equal('otherorg'); + expect(context.configuration.app).to.equal('app-from-flag'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org otherorg and app app-from-flag' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['shouldOverrideDashboardConfig', true]]) + ); + }); + + it('Should not setup monitoring for chosen org and app even if already configured if rejected', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldOverrideDashboardConfig: false }, + }); + + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-monitored-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'otherorg', app: 'app-from-flag' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + + await overrideCwd(serviceDir, async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + }); + expect(context.configuration).to.not.have.property('org'); + expect(context.configuration).to.not.have.property('app'); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['shouldOverrideDashboardConfig', false]]) + ); + }); + + it('Should ask for org if passed in one is invalid', async () => { + configureInquirerStub(inquirer, { + list: { orgName: 'testinteractivecli', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'invalid-testinteractivecli', app: 'irrelevant' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_user_provided_'], + ]) + ); + }); + + it('Should ask for org if passed in one is invalid and there is a valid on in config', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldOverrideDashboardConfig: true }, + list: { orgName: 'otherorg', appName: 'other-app' }, + }); + + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-monitored-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'invalid-testinteractivecli', app: 'irrelevant' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('otherorg'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('otherorg'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org otherorg and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_user_provided_'], + ['shouldOverrideDashboardConfig', true], + ]) + ); + }); + + it('Should ask for app if passed in one is invalid and there is a valid on in config', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldOverrideDashboardConfig: true }, + list: { orgName: 'testinteractivecli', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-monitored-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'invalid-testinteractivecli', app: 'irrelevant' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_user_provided_'], + ['shouldOverrideDashboardConfig', true], + ]) + ); + }); + + it('Should ask for app if passed in one is invalid', async () => { + configureInquirerStub(inquirer, { + list: { orgName: 'testinteractivecli', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + options: { org: 'testinteractivecli', app: 'invalid' }, + inquirer, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['appName', '_user_provided_']]) + ); + }); + + it('Should create new app when requested, and setup monitoring with it', async () => { + configureInquirerStub(inquirer, { + input: { newAppName: 'frominput' }, + list: { orgName: 'testinteractivecli', appName: '_create_' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: {}, + history: new Map(), + stepHistory: new StepHistory(), + }; + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('frominput'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('frominput'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app frominput' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['orgName', '_user_provided_'], + ['appName', '_create_'], + ['newAppName', '_user_provided_'], + ]) + ); + }); + }); + + describe('Monitoring setup when invalid org', () => { + it('Should provide a way to setup monitoring with an invalid org setting', async () => { + configureInquirerStub(inquirer, { + confirm: { shouldUpdateOrg: true }, + list: { orgName: 'testinteractivecli', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-wrongorg-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: {}, + history: new Map(), + stepHistory: new StepHistory(), + }; + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['shouldUpdateOrg', true], + ['orgName', '_user_provided_'], + ['appName', '_user_provided_'], + ]) + ); + }); + }); + + describe('Monitoring setup when no app', () => { + it('Should allow to setup app', async () => { + configureInquirerStub(inquirer, { + list: { appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-noapp-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: {}, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['appName', '_user_provided_']]) + ); + }); + }); + + describe('Monitoring setup when no app with --app flag', () => { + it('Should allow to setup app', async () => { + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-noapp-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: { app: 'app-from-flag' }, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('app-from-flag'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('app-from-flag'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app app-from-flag' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map()); + }); + + it('Should create a default app if no apps exist', async () => { + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-noapp-service', + { configExt: { org: 'orgwithoutapps' } } + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: {}, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('orgwithoutapps'); + expect(serviceConfig.app).to.equal(configuration.service); + expect(context.configuration.org).to.equal('orgwithoutapps'); + expect(context.configuration.app).to.equal(configuration.service); + expect(stripAnsi(stdoutData)).to.include( + `Your project has been setup with org orgwithoutapps and app ${configuration.service}` + ); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map()); + }); + + it('Should allow to setup app when app is invalid', async () => { + configureInquirerStub(inquirer, { + list: { appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-noapp-service' + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: { app: 'invalid-app-from-flag' }, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([['appName', '_user_provided_']]) + ); + }); + }); + + describe('Monitoring setup when invalid app', () => { + it('Should recognize an invalid app and allow to create it', async () => { + configureInquirerStub(inquirer, { + list: { appUpdateType: 'create' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service', + { + configExt: { + org: 'testinteractivecli', + app: 'not-created-app', + }, + } + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: {}, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('not-created-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('not-created-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app not-created-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['appUpdateType', 'create']])); + }); + + it('Should recognize an invalid app and allow to replace it with existing one', async () => { + configureInquirerStub(inquirer, { + list: { appUpdateType: 'chooseExisting', appName: 'other-app' }, + }); + const { servicePath: serviceDir, serviceConfig: configuration } = await fixtures.setup( + 'aws-loggedin-service', + { + configExt: { + org: 'testinteractivecli', + app: 'not-created-app', + }, + } + ); + const context = { + serviceDir, + configuration, + configurationFilename: 'serverless.yml', + inquirer, + options: {}, + history: new Map(), + stepHistory: new StepHistory(), + }; + + let stdoutData = ''; + await overrideCwd(serviceDir, async () => { + await overrideStdoutWrite( + (data) => (stdoutData += data), + async () => { + const stepData = await step.isApplicable(context); + if (!stepData) throw new Error('Step resolved as not applicable'); + await step.run(context, stepData); + } + ); + }); + const serviceConfig = yaml.load( + String(await fsp.readFile(join(serviceDir, 'serverless.yml'))) + ); + expect(serviceConfig.org).to.equal('testinteractivecli'); + expect(serviceConfig.app).to.equal('other-app'); + expect(context.configuration.org).to.equal('testinteractivecli'); + expect(context.configuration.app).to.equal('other-app'); + expect(stripAnsi(stdoutData)).to.include( + 'Your project has been setup with org testinteractivecli and app other-app' + ); + expect(context.stepHistory.valuesMap()).to.deep.equal( + new Map([ + ['appUpdateType', 'chooseExisting'], + ['appName', '_user_provided_'], + ]) + ); + }); + }); +});