diff --git a/lib/builtins/deploy-delegates/cfn-deployer/index.js b/lib/builtins/deploy-delegates/cfn-deployer/index.js index c0d264c3..acb52cba 100644 --- a/lib/builtins/deploy-delegates/cfn-deployer/index.js +++ b/lib/builtins/deploy-delegates/cfn-deployer/index.js @@ -3,19 +3,13 @@ const fs = require('fs'); const R = require('ramda'); const awsUtil = require('@src/clients/aws-client/aws-util'); +const stringUtils = require('@src/utils/string-utils'); const CliCFNDeployerError = require('@src/exceptions/cli-cfn-deployer-error'); const Helper = require('./helper'); const SKILL_STACK_PUBLIC_FILE_NAME = 'skill-stack.yaml'; const SKILL_STACK_ASSET_FILE_NAME = 'basic-lambda.yaml'; -const alexaAwsRegionMap = { - default: 'us-east-1', - NA: 'us-east-1', - EU: 'eu-west-1', - FE: 'us-west-2' -}; - module.exports = { bootstrap, invoke @@ -58,16 +52,17 @@ function bootstrap(options, callback) { */ async function invoke(reporter, options, callback) { const { alexaRegion, deployState = {} } = options; - deployState[alexaRegion] = deployState[alexaRegion] || {}; const deployProgress = { isAllStepSuccess: false, isCodeDeployed: false, - deployState: deployState[alexaRegion] + deployState: deployState[alexaRegion] || {} }; try { await _deploy(reporter, options, deployProgress); - deployProgress.resultMessage = _makeSuccessMessage(deployProgress.endpoint.uri, alexaRegion); + deployProgress.resultMessage = deployProgress.isDeploySkipped + ? _makeSkippedMessage(deployProgress.deployRegion, alexaRegion) + : _makeSuccessMessage(deployProgress.endpoint.uri, alexaRegion); callback(null, deployProgress); } catch (err) { deployProgress.resultMessage = _makeErrorMessage(err, alexaRegion); @@ -76,16 +71,23 @@ async function invoke(reporter, options, callback) { } async function _deploy(reporter, options, deployProgress) { - const { profile, doDebug, alexaRegion, skillId, skillName, code, userConfig } = options; + const { profile, doDebug, alexaRegion, skillId, skillName, code, userConfig, deployState = {}, deployRegions } = options; let { stackId } = deployProgress.deployState; const awsProfile = _getAwsProfile(profile); - const awsRegion = _getAwsRegion(alexaRegion, userConfig); + const awsRegion = _getAwsRegion(alexaRegion, deployRegions); const templateBody = _getTemplateBody(alexaRegion, userConfig); const userDefinedParameters = _getUserDefinedParameters(alexaRegion, userConfig); const bucketName = _getS3BucketName(alexaRegion, userConfig, deployProgress.deployState, awsProfile, awsRegion); const bucketKey = _getS3BucketKey(alexaRegion, userConfig, code.codeBuild); - const stackName = `ask-${skillName}-${alexaRegion}-skillStack-${Date.now()}`; + const stackName = _getStackName(skillName, alexaRegion); + const deployRegion = R.keys(deployRegions).find((region) => deployRegions[region] === awsRegion); + + if (deployRegion !== alexaRegion && R.equals(deployState[deployRegion], deployState[alexaRegion])) { + deployProgress.isDeploySkipped = true; + deployProgress.deployRegion = deployRegion; + return; + } const helper = new Helper(profile, doDebug, awsProfile, awsRegion, reporter); @@ -132,11 +134,8 @@ function _getAwsProfile(profile) { return awsProfile; } -function _getAwsRegion(alexaRegion, userConfig) { - let awsRegion = alexaRegion === 'default' ? userConfig.awsRegion - : R.path(['regionalOverrides', alexaRegion, 'awsRegion'], userConfig); - awsRegion = awsRegion || alexaAwsRegionMap[alexaRegion]; - +function _getAwsRegion(alexaRegion, deployRegions) { + const awsRegion = deployRegions[alexaRegion]; if (!awsRegion) { throw new CliCFNDeployerError(`Unsupported Alexa region: ${alexaRegion}. ` + 'Please check your region name or use "regionalOverrides" to specify AWS region.'); @@ -150,13 +149,16 @@ function _getS3BucketName(alexaRegion, userConfig, currentRegionDeployState, aws if (customValue) return customValue; - function generateBucketName() { + // Generates a valid S3 bucket name. + // a bucket name should follow the pattern: ask-projectName-profileName-awsRegion-timeStamp + // a valid bucket name cannot longer than 63 characters, so cli fixes the project name no longer than 22 characters + const generateBucketName = () => { const projectName = path.basename(process.cwd()); - const validProjectName = projectName.toLowerCase().replace(/[^a-z0-9-.]+/g, '').substring(0, 22); - const validProfile = awsProfile.toLowerCase().replace(/[^a-z0-9-.]+/g, '').substring(0, 9); + const validProjectName = stringUtils.filterNonAlphanumeric(projectName.toLowerCase()).substring(0, 22); + const validProfile = stringUtils.filterNonAlphanumeric(awsProfile.toLowerCase()).substring(0, 9); const shortRegionName = awsRegion.replace(/-/g, ''); return `ask-${validProjectName}-${validProfile}-${shortRegionName}-${Date.now()}`; - } + }; return R.path(['s3', 'bucket'], currentRegionDeployState) || generateBucketName(); } @@ -170,6 +172,19 @@ function _getS3BucketKey(alexaRegion, userConfig, codeBuild) { return `endpoint/${path.basename(codeBuild)}`; } +function _getStackName(skillName, alexaRegion) { + // Generates a valid CloudFormation stack name. + // a stack name should follow the pattern: ask-skillName-alexaRegion-skillStack-timeStamp + // a valid stack name cannot longer than 128 characters, so cli fixes the skill name no longer than 64 characters + const generateStackName = () => { + const validSkillName = stringUtils.filterNonAlphanumeric(skillName).substring(0, 64); + const shortRegionName = alexaRegion.replace(/-/g, ''); + return `ask-${validSkillName}-${shortRegionName}-skillStack-${Date.now()}`; + }; + + return generateStackName(); +} + function _getCapabilities(alexaRegion, userConfig) { let capabilities = R.path(['regionalOverrides', alexaRegion, 'cfn', 'capabilities'], userConfig) || R.path(['cfn', 'capabilities'], userConfig); @@ -216,6 +231,10 @@ function _getTemplateBody(alexaRegion, userConfig) { return fs.readFileSync(templatePath, 'utf-8'); } +function _makeSkippedMessage(deployRegion, alexaRegion) { + return `The CloudFormation deploy for Alexa region "${alexaRegion}" is same as "${deployRegion}".`; +} + function _makeSuccessMessage(endpointUri, alexaRegion) { return `The CloudFormation deploy succeeded for Alexa region "${alexaRegion}" with output Lambda ARN: ${endpointUri}.`; } diff --git a/lib/builtins/deploy-delegates/lambda-deployer/index.js b/lib/builtins/deploy-delegates/lambda-deployer/index.js index 5e3569c1..2352e98d 100644 --- a/lib/builtins/deploy-delegates/lambda-deployer/index.js +++ b/lib/builtins/deploy-delegates/lambda-deployer/index.js @@ -4,13 +4,6 @@ const awsUtil = require('@src/clients/aws-client/aws-util'); const stringUtils = require('@src/utils/string-utils'); const helper = require('./helper'); -const alexaAwsRegionMap = { - default: 'us-east-1', - NA: 'us-east-1', - EU: 'eu-west-1', - FE: 'us-west-2' -}; - module.exports = { bootstrap, invoke @@ -36,28 +29,35 @@ function bootstrap(options, callback) { * @param {Function} callback */ function invoke(reporter, options, callback) { - const { profile, ignoreHash, alexaRegion, skillId, skillName, code, userConfig, deployState = {} } = options; + const { profile, ignoreHash, alexaRegion, skillId, skillName, code, userConfig, deployState = {}, deployRegions } = options; + const currentRegionDeployState = deployState[alexaRegion] || {}; const awsProfile = awsUtil.getAWSProfile(profile); if (!stringUtils.isNonBlankString(awsProfile)) { return callback(`Profile [${profile}] doesn't have AWS profile linked to it. Please run "ask configure" to re-configure your porfile.`); } - let currentRegionDeployState = deployState[alexaRegion]; - if (!currentRegionDeployState) { - currentRegionDeployState = {}; - deployState[alexaRegion] = currentRegionDeployState; - } - // parse AWS region to use - let awsRegion = alexaRegion === 'default' ? userConfig.awsRegion : R.path(['regionalOverrides', alexaRegion, 'awsRegion'], userConfig); - awsRegion = awsRegion || alexaAwsRegionMap[alexaRegion]; + const awsRegion = deployRegions[alexaRegion]; if (!stringUtils.isNonBlankString(awsRegion)) { return callback(`Unsupported Alexa region: ${alexaRegion}. Please check your region name or use "regionalOverrides" to specify AWS region.`); } + const deployRegion = R.keys(deployRegions).find((region) => deployRegions[region] === awsRegion); + if (deployRegion !== alexaRegion && R.equals(deployState[deployRegion], deployState[alexaRegion])) { + return callback(null, { + isDeploySkipped: true, + deployRegion, + resultMessage: `The lambda deploy for Alexa region "${alexaRegion}" is same as "${deployRegion}"` + }); + } // load Lambda info from either existing deployState or userConfig's sourceLambda const loadLambdaConfig = { awsProfile, awsRegion, alexaRegion, ignoreHash, deployState: currentRegionDeployState, userConfig }; helper.loadLambdaInformation(reporter, loadLambdaConfig, (loadLambdaErr, lambdaData) => { if (loadLambdaErr) { - return callback(loadLambdaErr); + return callback(null, { + isAllStepSuccess: false, + isCodeDeployed: false, + deployState: currentRegionDeployState, + resultMessage: `The lambda deploy failed for Alexa region "${alexaRegion}": ${loadLambdaErr}` + }); } currentRegionDeployState.lambda = lambdaData.lambda; currentRegionDeployState.iamRole = lambdaData.iamRole; @@ -71,9 +71,14 @@ function invoke(reporter, options, callback) { }; helper.deployIAMRole(reporter, deployIAMConfig, (iamErr, iamRoleArn) => { if (iamErr) { - return callback(iamErr); + return callback(null, { + isAllStepSuccess: false, + isCodeDeployed: false, + deployState: currentRegionDeployState, + resultMessage: `The lambda deploy failed for Alexa region "${alexaRegion}": ${iamErr}` + }); } - deployState[alexaRegion].iamRole = iamRoleArn; + currentRegionDeployState.iamRole = iamRoleArn; // create/update deploy for Lambda const deployLambdaConfig = { profile, @@ -93,19 +98,19 @@ function invoke(reporter, options, callback) { return callback(null, { isAllStepSuccess: false, isCodeDeployed: false, - deployState: deployState[alexaRegion], + deployState: currentRegionDeployState, resultMessage: `The lambda deploy failed for Alexa region "${alexaRegion}": ${lambdaErr}` }); } const { isAllStepSuccess, isCodeDeployed, lambdaResponse = {} } = lambdaResult; - deployState[alexaRegion].lambda = lambdaResponse; + currentRegionDeployState.lambda = lambdaResponse; const { arn } = lambdaResponse; // 2.full successs in Lambda deploy if (isAllStepSuccess) { return callback(null, { isAllStepSuccess, isCodeDeployed, - deployState: deployState[alexaRegion], + deployState: currentRegionDeployState, endpoint: { uri: arn }, resultMessage: `The lambda deploy succeeded for Alexa region "${alexaRegion}" with output Lambda ARN: ${arn}.` }); @@ -114,7 +119,7 @@ function invoke(reporter, options, callback) { return callback(null, { isAllStepSuccess, isCodeDeployed, - deployState: deployState[alexaRegion], + deployState: currentRegionDeployState, resultMessage: `The lambda deploy failed for Alexa region "${alexaRegion}": ${lambdaResult.resultMessage}` }); }); diff --git a/lib/controllers/skill-infrastructure-controller/index.js b/lib/controllers/skill-infrastructure-controller/index.js index d4834ea8..900ac1a2 100644 --- a/lib/controllers/skill-infrastructure-controller/index.js +++ b/lib/controllers/skill-infrastructure-controller/index.js @@ -13,6 +13,13 @@ const profileHelper = require('@src/utils/profile-helper'); const DeployDelegate = require('./deploy-delegate'); +const defaultAlexaAwsRegionMap = { + default: 'us-east-1', + NA: 'us-east-1', + EU: 'eu-west-1', + FE: 'us-west-2' +}; + module.exports = class SkillInfrastructureController { constructor(configuration) { const { profile, doDebug, ignoreHash } = configuration; @@ -99,6 +106,8 @@ module.exports = class SkillInfrastructureController { if (!regionsList || regionsList.length === 0) { return callback('[Warn]: Skip the infrastructure deployment, as the "code" field has not been set in the resources config file.'); } + const userConfig = ResourcesConfig.getInstance().getSkillInfraUserConfig(this.profile); + const deployRegions = this._getAlexaDeployRegions(regionsList, userConfig); // 1.instantiate MultiTasksView const taskConfig = { @@ -110,27 +119,36 @@ module.exports = class SkillInfrastructureController { regionsList.forEach((region) => { const taskTitle = `Deploy Alexa skill infrastructure for region "${region}"`; const taskHandle = (reporter, taskCallback) => { - this._deployInfraByRegion(reporter, dd, region, skillName, taskCallback); + this._deployInfraByRegion(reporter, dd, region, skillName, deployRegions, taskCallback); }; multiTasksView.loadTask(taskHandle, taskTitle, region); }); // 3.start multi-tasks and validate task response multiTasksView.start((taskErr, taskResult) => { - if (taskErr) { + const { error, partialResult } = taskErr || {}; + const result = partialResult || taskResult; + // update skipped deployment task with deploy region result + if (result) { + R.keys(result).filter((alexaRegion) => result[alexaRegion].isDeploySkipped).forEach((alexaRegion) => { + const { deployRegion } = result[alexaRegion]; + result[alexaRegion] = result[deployRegion]; + }); + } + if (error) { // update partial successful deploy results to resources config - if (taskErr.partialResult && !R.isEmpty(R.keys(taskErr.partialResult))) { - this._updateResourcesConfig(taskErr.partialResult); + if (result && !R.isEmpty(R.keys(result))) { + this._updateResourcesConfig(regionsList, result); } - return callback(taskErr.error); + return callback(error); } // 4.validate response and update states based on the results try { - dd.validateDeployDelegateResponse(taskResult); + dd.validateDeployDelegateResponse(result); } catch (responseInvalidErr) { return callback(responseInvalidErr); } - this._updateResourcesConfig(taskResult); - callback(null, taskResult); + this._updateResourcesConfig(regionsList, result); + callback(null, result); }); } @@ -194,10 +212,11 @@ module.exports = class SkillInfrastructureController { * @param {Object} dd injected deploy delegate instance * @param {String} alexaRegion * @param {String} skillName + * @param {Object} deployRegions * @param {Function} callback (error, invokeResult) * callback.error can be a String or { message, context } Object which passes back the partial deploy result */ - _deployInfraByRegion(reporter, dd, alexaRegion, skillName, callback) { + _deployInfraByRegion(reporter, dd, alexaRegion, skillName, deployRegions, callback) { const regionConfig = { profile: this.profile, doDebug: this.doDebug, @@ -210,7 +229,8 @@ module.exports = class SkillInfrastructureController { isCodeModified: null }, userConfig: ResourcesConfig.getInstance().getSkillInfraUserConfig(this.profile), - deployState: ResourcesConfig.getInstance().getSkillInfraDeployState(this.profile) + deployState: ResourcesConfig.getInstance().getSkillInfraDeployState(this.profile), + deployRegions }; // 1.calculate the lastDeployHash for current code folder and compare with the one in record const lastDeployHash = ResourcesConfig.getInstance().getCodeLastDeployHashByRegion(this.profile, regionConfig.alexaRegion); @@ -224,26 +244,39 @@ module.exports = class SkillInfrastructureController { if (invokeErr) { return callback(invokeErr); } - const { isAllStepSuccess, isCodeDeployed } = invokeResult; + const { isAllStepSuccess, isCodeDeployed, isDeploySkipped, resultMessage } = invokeResult; // track the current hash if isCodeDeployed if (isCodeDeployed) { invokeResult.lastDeployHash = currentHash; } - // pass back result based on if isAllStepSuccess, pass result as error if not all steps succeed - callback(isAllStepSuccess ? null : invokeResult, isAllStepSuccess ? invokeResult : undefined); + // skip task if isDeploySkipped + if (isDeploySkipped) { + reporter.skipTask(resultMessage); + } + // pass result message as error message if deploy not success and not skipped + if (!isAllStepSuccess && !isDeploySkipped) { + callback({ message: resultMessage, context: invokeResult }); + } else { + callback(null, invokeResult); + } }); }); } /** * Update the the ask resources config and the deploy state. + * @param {Object} regionsList list of configured alexa regions * @param {Object} rawDeployResult deploy result from invoke: { $region: deploy-delegate's response } */ - _updateResourcesConfig(rawDeployResult) { + _updateResourcesConfig(regionsList, rawDeployResult) { + const curDeployState = ResourcesConfig.getInstance().getSkillInfraDeployState(this.profile) || {}; const newDeployState = {}; - R.keys(rawDeployResult).forEach((alexaRegion) => { - newDeployState[alexaRegion] = rawDeployResult[alexaRegion].deployState; - ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(this.profile, alexaRegion, rawDeployResult[alexaRegion].lastDeployHash); + regionsList.forEach((alexaRegion) => { + const { deployState, lastDeployHash } = rawDeployResult[alexaRegion] || {}; + newDeployState[alexaRegion] = deployState || curDeployState[alexaRegion]; + if (lastDeployHash) { + ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(this.profile, alexaRegion, lastDeployHash); + } }); ResourcesConfig.getInstance().setSkillInfraDeployState(this.profile, newDeployState); ResourcesConfig.getInstance().write(); @@ -272,4 +305,21 @@ module.exports = class SkillInfrastructureController { callback(); }); } + + /** + * Return deploy regions map based on configured alexa code regions + * @param {Array} regionsList list of configured alexa regions + * @param {Object} userConfig + * @return {Object} + */ + _getAlexaDeployRegions(regionsList, userConfig) { + const deployRegions = {}; + regionsList.forEach((alexaRegion) => { + const awsRegion = alexaRegion === 'default' + ? userConfig.awsRegion + : R.path(['regionalOverrides', alexaRegion, 'awsRegion'], userConfig); + deployRegions[alexaRegion] = awsRegion || defaultAlexaAwsRegionMap[alexaRegion]; + }); + return deployRegions; + } }; diff --git a/lib/view/multi-tasks-view.js b/lib/view/multi-tasks-view.js index fd43bd80..55a66ba4 100644 --- a/lib/view/multi-tasks-view.js +++ b/lib/view/multi-tasks-view.js @@ -26,6 +26,9 @@ class ListrReactiveTask { */ get reporter() { return { + skipTask: (reason) => { + this._eventEmitter.emit('skip', reason); + }, updateStatus: (status) => { this._eventEmitter.emit('status', status); } @@ -50,6 +53,7 @@ class ListrReactiveTask { * Mapping is: event observable * status subscriber.next * error subscriber.error + record error.context to task context + * skill task.skip * title task.next * complete subscriber.complete + record result to task context */ @@ -60,6 +64,12 @@ class ListrReactiveTask { }); this._eventEmitter.on('error', (error) => { subscriber.error(error); + if (error.context) { + ctx[this.taskId] = error.context; + } + }); + this._eventEmitter.on('skip', (reason) => { + task.skip(reason); }); this._eventEmitter.on('title', (title) => { task.title = title; @@ -116,7 +126,7 @@ class MultiTasksView { callback(null, context); }).catch((listrError) => { const errorMessage = listrError.errors - .map(e => e.resultMessage || e.message || e).join('\n'); + .map(e => e.message || e).join('\n'); callback({ error: new CliError(errorMessage), partialResult: listrError.context diff --git a/test/unit/builtins/cfn-deployer/index-test.js b/test/unit/builtins/cfn-deployer/index-test.js index 4b044cf0..b008cf00 100644 --- a/test/unit/builtins/cfn-deployer/index-test.js +++ b/test/unit/builtins/cfn-deployer/index-test.js @@ -122,10 +122,13 @@ describe('Builtins test - cfn-deployer index test', () => { default: { stackId, s3: { - bucket: 'someName', - key: 'someKey.zip' + bucket: bucketName, + key: bucketKey } } + }, + deployRegions: { + default: 'us-east-1' } }; getAWSProfileStub = sinon.stub(awsUtil, 'getAWSProfile').returns('some profile'); @@ -136,6 +139,7 @@ describe('Builtins test - cfn-deployer index test', () => { deployStackStub = sinon.stub(Helper.prototype, 'deployStack').resolves({ StackId: stackId }); waitForStackDeployStub = sinon.stub(Helper.prototype, 'waitForStackDeploy'); }); + it('should deploy', (done) => { waitForStackDeployStub.resolves({ endpointUri, stackInfo: { Outputs: [] } }); @@ -205,6 +209,42 @@ describe('Builtins test - cfn-deployer index test', () => { }); }); + it('should skip deploy when region is not primary deploy region', (done) => { + const deployRegion = 'default'; + const skipRegion = 'NA'; + deployOptions.alexaRegion = skipRegion; + deployOptions.deployRegions[skipRegion] = deployOptions.deployRegions[deployRegion]; + deployOptions.deployState[skipRegion] = deployOptions.deployState[deployRegion]; + expectedOutput = { + ...expectedErrorOutput, + isDeploySkipped: true, + deployRegion, + resultMessage: `The CloudFormation deploy for Alexa region "${skipRegion}" is same as "${deployRegion}".` + }; + + Deployer.invoke({}, deployOptions, (err, result) => { + expect(err).eql(null); + expect(result).eql(expectedOutput); + done(); + }); + }); + + it('should not skip deploy when region is not primary deploy region but has different deploy state', (done) => { + const deployRegion = 'default'; + const currentRegion = 'NA'; + waitForStackDeployStub.resolves({ endpointUri, stackInfo: { Outputs: [] } }); + deployOptions.alexaRegion = currentRegion; + deployOptions.deployRegions[currentRegion] = deployOptions.deployRegions[deployRegion]; + deployOptions.deployState[currentRegion] = { ...deployOptions.deployState[deployRegion], stackId: 'different stack id' }; + expectedOutput.resultMessage = `The CloudFormation deploy succeeded for Alexa region "${currentRegion}" with output Lambda ARN: ${endpointUri}.`; + + Deployer.invoke({}, deployOptions, (err, result) => { + expect(err).eql(null); + expect(result).eql(expectedOutput); + done(); + }); + }); + it('should throw error when reserved parameter is used', (done) => { deployOptions.userConfig.cfn.parameters.SkillId = 'reserved parameter value'; diff --git a/test/unit/builtins/lambda-deployer/index-test.js b/test/unit/builtins/lambda-deployer/index-test.js index 0cf419a4..0195ba2c 100644 --- a/test/unit/builtins/lambda-deployer/index-test.js +++ b/test/unit/builtins/lambda-deployer/index-test.js @@ -10,10 +10,15 @@ describe('Builtins test - lambda-deployer index.js test', () => { const TEST_PROFILE = 'default'; // test file uses 'default' profile const TEST_IGNORE_HASH = false; const TEST_ALEXA_REGION_DEFAULT = 'default'; + const TEST_ALEXA_REGION_NA = 'NA'; const TEST_AWS_REGION_DEFAULT = 'us-east-1'; + const TEST_AWS_REGION_NA = 'us-east-1'; const TEST_SKILL_NAME = 'skill_name'; const TEST_IAM_ROLE_ARN = 'IAM role arn'; const NULL_PROFILE = 'null'; + const LAMBDA_ARN = 'lambda_arn'; + const LAST_MODIFIED = 'last_modified'; + const REVISION_ID = '1'; describe('# test class method: bootstrap', () => { afterEach(() => { @@ -49,10 +54,11 @@ describe('Builtins test - lambda-deployer index.js test', () => { skillId: '', skillName: TEST_SKILL_NAME, code: {}, - userConfig: { - awsRegion: TEST_AWS_REGION_DEFAULT - }, - deployState: {} + userConfig: {}, + deployState: {}, + deployRegions: { + [TEST_ALEXA_REGION_DEFAULT]: TEST_AWS_REGION_DEFAULT + } }; const TEST_VALIDATED_DEPLOY_STATE = { lambda: {}, @@ -88,7 +94,8 @@ Please run "ask configure" to re-configure your porfile.`; skillName: TEST_SKILL_NAME, code: {}, userConfig: {}, - deployState: {} + deployState: {}, + deployRegions: {} }; const TEST_ERROR = `Unsupported Alexa region: ${null}. Please check your region name or use "regionalOverrides" to specify AWS region.`; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); @@ -100,29 +107,34 @@ Please run "ask configure" to re-configure your porfile.`; }); }); - it('| alexaRegion is set correctly, validate Lambda deploy state fails, expect an error return', (done) => { + + it('| alexaRegion is set correctly, validate Lambda deploy state fails, expect an error message return', (done) => { // setup const TEST_ERROR = 'loadLambdaInformation error message'; + const TEST_ERROR_MESSAGE_RESPONSE = `The lambda deploy failed for Alexa region "default": ${TEST_ERROR}`; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, TEST_ERROR); // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err) => { + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, res) => { // verify - expect(err).equal(TEST_ERROR); + expect(res.resultMessage).equal(TEST_ERROR_MESSAGE_RESPONSE); + expect(err).equal(null); done(); }); }); - it('| validate Lambda deploy state passes, deploy IAM role fails, expect an error return', (done) => { + it('| validate Lambda deploy state passes, deploy IAM role fails, expect an error message return', (done) => { // setup const TEST_ERROR = 'IAMRole error message'; + const TEST_ERROR_MESSAGE_RESPONSE = `The lambda deploy failed for Alexa region "default": ${TEST_ERROR}`; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); sinon.stub(helper, 'deployIAMRole').callsArgWith(2, TEST_ERROR); // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err) => { + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, res) => { // verify - expect(err).equal(TEST_ERROR); + expect(res.resultMessage).equal(TEST_ERROR_MESSAGE_RESPONSE); + expect(err).equal(null); done(); }); }); @@ -136,47 +148,29 @@ Please run "ask configure" to re-configure your porfile.`; sinon.stub(helper, 'deployIAMRole').callsArgWith(2, null, TEST_IAM_ROLE_ARN); sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, TEST_ERROR); // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, data) => { + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, res) => { // verify - expect(data.resultMessage).equal(TEST_ERROR_MESSAGE_RESPONSE); - expect(data.deployState.iamRole).equal(TEST_IAM_ROLE_ARN); + expect(res.resultMessage).equal(TEST_ERROR_MESSAGE_RESPONSE); + expect(res.deployState.iamRole).equal(TEST_IAM_ROLE_ARN); + expect(err).equal(null); done(); }); }); - it('| deploy IAM role passes, deploy Lambda code configuration fails, expect IAM role arn, revisionId and a message returned', (done) => { + it('| deploy IAM role passes, deploy Lambda config fails, expect all data, except endpoint, and an error message returned', (done) => { // setup const TEST_ERROR = 'LambdaFunction error message'; const TEST_ERROR_MESSAGE_RESPONSE = `The lambda deploy failed for Alexa region "default": ${TEST_ERROR}`; - sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); - sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); - sinon.stub(helper, 'deployIAMRole').callsArgWith(2, null, TEST_IAM_ROLE_ARN); - sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, TEST_ERROR); - // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, data) => { - // verify - expect(data.resultMessage).equal(TEST_ERROR_MESSAGE_RESPONSE); - expect(data.deployState.iamRole).equal(TEST_IAM_ROLE_ARN); - done(); - }); - }); - - it('| deploy IAM role and Lambda Function pass, expect correct data return', (done) => { - // setup - const LAMBDA_ARN = 'lambda_arn'; - const LAST_MODIFIED = 'last_modified'; - const REVISION_ID = '1'; - const TEST_SUCCESS_RESPONSE = `The lambda deploy succeeded for Alexa region "${TEST_ALEXA_REGION_DEFAULT}" \ -with output Lambda ARN: ${LAMBDA_ARN}.`; const LAMDBA_RESPONSE = { arn: LAMBDA_ARN, lastModified: LAST_MODIFIED, revisionId: REVISION_ID }; const TEST_LAMBDA_RESULT = { - isAllStepSuccess: true, + isAllStepSuccess: false, isCodeDeployed: true, - lambdaResponse: LAMDBA_RESPONSE + lambdaResponse: LAMDBA_RESPONSE, + resultMessage: TEST_ERROR }; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); @@ -185,134 +179,102 @@ with output Lambda ARN: ${LAMBDA_ARN}.`; // call lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, res) => { // verify - expect(res.endpoint.uri).equal(LAMBDA_ARN); + expect(res.endpoint).equal(undefined); expect(res.deployState.iamRole).equal(TEST_IAM_ROLE_ARN); expect(res.deployState.lambda).deep.equal(LAMDBA_RESPONSE); - expect(res.resultMessage).equal(TEST_SUCCESS_RESPONSE); + expect(res.resultMessage).equal(TEST_ERROR_MESSAGE_RESPONSE); expect(err).equal(null); done(); }); }); - it('| alexaRegion is default, userConfig awsRegion is set, expect awsRegion is retrieved correctly.', (done) => { + it('| deploy IAM role and Lambda Function pass, expect correct data return', (done) => { // setup - const USER_CONFIG_AWS_REGION = 'sa-east-1'; - const TEST_OPTIONS_WITHOUT_AWS_REGION = { - profile: TEST_PROFILE, - alexaRegion: TEST_ALEXA_REGION_DEFAULT, - skillId: '', - skillName: TEST_SKILL_NAME, - code: {}, - userConfig: { - awsRegion: USER_CONFIG_AWS_REGION - }, - deployState: {} + const TEST_SUCCESS_RESPONSE = `The lambda deploy succeeded for Alexa region "${TEST_ALEXA_REGION_DEFAULT}" \ +with output Lambda ARN: ${LAMBDA_ARN}.`; + const LAMDBA_RESPONSE = { + arn: LAMBDA_ARN, + lastModified: LAST_MODIFIED, + revisionId: REVISION_ID }; const TEST_LAMBDA_RESULT = { isAllStepSuccess: true, isCodeDeployed: true, - lambdaResponse: {} + lambdaResponse: LAMDBA_RESPONSE }; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); sinon.stub(helper, 'deployIAMRole').callsArgWith(2, null, TEST_IAM_ROLE_ARN); sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, null, TEST_LAMBDA_RESULT); // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_AWS_REGION, (err) => { + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, res) => { // verify + expect(res.endpoint.uri).equal(LAMBDA_ARN); + expect(res.deployState.iamRole).equal(TEST_IAM_ROLE_ARN); + expect(res.deployState.lambda).deep.equal(LAMDBA_RESPONSE); + expect(res.resultMessage).equal(TEST_SUCCESS_RESPONSE); expect(err).equal(null); - expect(helper.loadLambdaInformation.args[0][1].awsRegion).equal(USER_CONFIG_AWS_REGION); - expect(helper.deployIAMRole.args[0][1].awsRegion).equal(USER_CONFIG_AWS_REGION); done(); }); }); - it('| alexaRegion is default, userConfig awsRegion is NOT set, expect awsRegion is set based on Alexa and AWS region map.', (done) => { + it('| not primary deployRegion in multi-regions environment, expect deploy skipped message return', (done) => { // setup - const MAPPING_ALEXA_DEFAULT_AWS_REGION = 'us-east-1'; - const TEST_OPTIONS_WITHOUT_AWS_REGION = { - profile: TEST_PROFILE, - alexaRegion: TEST_ALEXA_REGION_DEFAULT, - skillId: '', - skillName: TEST_SKILL_NAME, - code: {}, - userConfig: {}, - deployState: {} + const TEST_OPTIONS_WITH_MULTI_REGIONS = { + ...TEST_OPTIONS, + alexaRegion: TEST_ALEXA_REGION_NA, + deployRegions: { + [TEST_ALEXA_REGION_DEFAULT]: TEST_AWS_REGION_DEFAULT, + [TEST_ALEXA_REGION_NA]: TEST_AWS_REGION_NA + } }; - const LAMDBA_RESULT = {}; + const TEST_SKIPPED_RESPONSE = `The lambda deploy for Alexa region "${TEST_ALEXA_REGION_NA}" is same as "${TEST_ALEXA_REGION_DEFAULT}"`; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); - sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); - sinon.stub(helper, 'deployIAMRole').callsArgWith(2, null, TEST_IAM_ROLE_ARN); - sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, null, LAMDBA_RESULT); // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_AWS_REGION, (err) => { + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITH_MULTI_REGIONS, (err, res) => { // verify + expect(res.isDeploySkipped).equal(true); + expect(res.deployRegion).equal(TEST_ALEXA_REGION_DEFAULT); + expect(res.resultMessage).equal(TEST_SKIPPED_RESPONSE); expect(err).equal(null); - expect(helper.loadLambdaInformation.args[0][1].awsRegion).equal(MAPPING_ALEXA_DEFAULT_AWS_REGION); - expect(helper.deployIAMRole.args[0][1].awsRegion).equal(MAPPING_ALEXA_DEFAULT_AWS_REGION); done(); }); }); - it('| alexaRegion is not default, userConfig regionalOverrides awsRegion is set, expect awsRegion is retrieved correctly.', (done) => { + it('| not primary deployRegion in multi-regions setup but different current deploy state, expect deploy not to be skipped', (done) => { // setup - const TEST_ALEXA_REGION_EU = 'EU'; - const USER_CONFIG_AWS_REGION = 'eu-west-2'; - const TEST_OPTIONS_WITHOUT_EU_REGION = { - profile: TEST_PROFILE, - alexaRegion: TEST_ALEXA_REGION_EU, - skillId: '', - skillName: TEST_SKILL_NAME, - code: {}, - userConfig: { - regionalOverrides: { - EU: { - awsRegion: USER_CONFIG_AWS_REGION - } - } + const TEST_OPTIONS_WITH_MULTI_REGIONS = { + ...TEST_OPTIONS, + alexaRegion: TEST_ALEXA_REGION_NA, + deployState: { + [TEST_ALEXA_REGION_DEFAULT]: 'deploy_state_default', + [TEST_ALEXA_REGION_NA]: 'deploy_state_na' }, - deployState: {} + deployRegions: { + [TEST_ALEXA_REGION_DEFAULT]: TEST_AWS_REGION_DEFAULT, + [TEST_ALEXA_REGION_NA]: TEST_AWS_REGION_NA + } }; - const LAMDBA_RESULT = {}; - sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); - sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); - sinon.stub(helper, 'deployIAMRole').callsArgWith(2, null, TEST_IAM_ROLE_ARN); - sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, null, LAMDBA_RESULT); - // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_EU_REGION, (err) => { - // verify - expect(err).equal(null); - expect(helper.loadLambdaInformation.args[0][1].awsRegion).equal(USER_CONFIG_AWS_REGION); - expect(helper.deployIAMRole.args[0][1].awsRegion).equal(USER_CONFIG_AWS_REGION); - done(); - }); - }); - - it('| alexaRegion is not default, userConfig regionalOverrides awsRegion is NOT set, ' - + 'expect awsRegion is set based on Alexa and AWS region map.', (done) => { - // setup - const TEST_ALEXA_REGION_EU = 'EU'; - const MAPPING_ALEXA_EU_AWS_REGION = 'eu-west-1'; - const TEST_OPTIONS_WITHOUT_AWS_REGION = { - profile: TEST_PROFILE, - alexaRegion: TEST_ALEXA_REGION_EU, - skillId: '', - skillName: TEST_SKILL_NAME, - code: {}, - userConfig: {}, - deployState: {} + const TEST_SUCCESS_RESPONSE = `The lambda deploy succeeded for Alexa region "${TEST_ALEXA_REGION_NA}" \ +with output Lambda ARN: ${LAMBDA_ARN}.`; + const TEST_LAMBDA_RESULT = { + isAllStepSuccess: true, + isCodeDeployed: true, + lambdaResponse: { arn: LAMBDA_ARN } }; - const LAMDBA_RESULT = {}; sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); sinon.stub(helper, 'loadLambdaInformation').callsArgWith(2, null, TEST_VALIDATED_DEPLOY_STATE); sinon.stub(helper, 'deployIAMRole').callsArgWith(2, null, TEST_IAM_ROLE_ARN); - sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, null, LAMDBA_RESULT); + sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, null, TEST_LAMBDA_RESULT); // call - lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_AWS_REGION, (err) => { + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITH_MULTI_REGIONS, (err, res) => { // verify + expect(res.isAllStepSuccess).equal(true); + expect(res.isCodeDeployed).equal(true); + expect(res.isDeploySkipped).equal(undefined); + expect(res.deployRegion).equal(undefined); + expect(res.resultMessage).equal(TEST_SUCCESS_RESPONSE); expect(err).equal(null); - expect(helper.loadLambdaInformation.args[0][1].awsRegion).equal(MAPPING_ALEXA_EU_AWS_REGION); - expect(helper.deployIAMRole.args[0][1].awsRegion).equal(MAPPING_ALEXA_EU_AWS_REGION); done(); }); }); diff --git a/test/unit/controller/skill-infrastructure-controller-test.js b/test/unit/controller/skill-infrastructure-controller-test.js index 5b29d740..2abcf17f 100644 --- a/test/unit/controller/skill-infrastructure-controller-test.js +++ b/test/unit/controller/skill-infrastructure-controller-test.js @@ -193,14 +193,12 @@ describe('Controller test - skill infrastructure controller test', () => { describe('# test class method: deployInfraToAllRegions', () => { const TEST_DD = {}; - let ddStub; const skillInfraController = new SkillInfrastructureController(TEST_CONFIGURATION); beforeEach(() => { new ResourcesConfig(FIXTURE_RESOURCES_CONFIG_FILE_PATH); new Manifest(FIXTURE_MANIFEST_FILE_PATH); - ddStub = sinon.stub(); - TEST_DD.validateDeployDelegateResponse = ddStub; + TEST_DD.validateDeployDelegateResponse = sinon.stub(); }); afterEach(() => { @@ -265,7 +263,7 @@ describe('Controller test - skill infrastructure controller test', () => { // verify expect(res).equal(undefined); expect(err).equal('error'); - expect(SkillInfrastructureController.prototype._updateResourcesConfig.args[0][0]).deep.equal({ NA: 'partial' }); + expect(SkillInfrastructureController.prototype._updateResourcesConfig.args[0][1]).deep.equal({ NA: 'partial' }); done(); }); }); @@ -274,7 +272,7 @@ describe('Controller test - skill infrastructure controller test', () => { // setup sinon.stub(MultiTasksView.prototype, 'loadTask'); sinon.stub(MultiTasksView.prototype, 'start').callsArgWith(0); - ddStub.throws(new Error('error')); + TEST_DD.validateDeployDelegateResponse.throws(new Error('error')); // call skillInfraController.deployInfraToAllRegions(TEST_DD, (err, res) => { // verify @@ -305,6 +303,52 @@ describe('Controller test - skill infrastructure controller test', () => { done(); }); }); + + it('| deploy infra to all regions pass with skip, expect no error called back and skipped result updated', (done) => { + // setup + sinon.stub(SkillInfrastructureController.prototype, '_deployInfraByRegion'); + sinon.stub(SkillInfrastructureController.prototype, '_updateResourcesConfig'); + sinon.stub(MultiTasksView.prototype, 'loadTask'); + sinon.stub(MultiTasksView.prototype, 'start').callsArgWith(0, null, { + default: 'success', + NA: { isDeploySkipped: true, deployRegion: 'default' }, + EU: 'success' + }); + // call + skillInfraController.deployInfraToAllRegions(TEST_DD, (err, res) => { + // verify + expect(res).deep.equal({ default: 'success', NA: 'success', EU: 'success' }); + expect(err).equal(null); + done(); + }); + }); + + it('| deploy infra to all regions fails partially with skip, expect error called back and skipped result updated', (done) => { + // setup + sinon.stub(SkillInfrastructureController.prototype, '_deployInfraByRegion'); + sinon.stub(SkillInfrastructureController.prototype, '_updateResourcesConfig'); + sinon.stub(MultiTasksView.prototype, 'loadTask'); + sinon.stub(MultiTasksView.prototype, 'start').callsArgWith(0, { + error: 'error', + partialResult: { + default: 'partial', + NA: { isDeploySkipped: true, deployRegion: 'default' }, + EU: 'partial' + } + }); + // call + skillInfraController.deployInfraToAllRegions(TEST_DD, (err, res) => { + // verify + expect(res).equal(undefined); + expect(err).equal('error'); + expect(SkillInfrastructureController.prototype._updateResourcesConfig.args[0][1]).deep.equal({ + default: 'partial', + NA: 'partial', + EU: 'partial' + }); + done(); + }); + }); }); describe('# test class method: updateSkillManifestWithDeployResult', () => { @@ -496,11 +540,10 @@ describe('Controller test - skill infrastructure controller test', () => { }); describe('# test class method: _deployInfraByRegion', () => { - let ddStub; - const TEST_REPORTER = {}; - const TEST_DD = {}; + let TEST_REPORTER, TEST_DD; const TEST_REGION = 'default'; const TEST_SKILL_NAME = 'skillName'; + const TEST_DEPLOY_REGIONS = { TEST_REGION: 'deployRegion' }; const TEST_HASH = 'hash'; const skillInfraController = new SkillInfrastructureController(TEST_CONFIGURATION); @@ -511,8 +554,10 @@ describe('Controller test - skill infrastructure controller test', () => { sinon.stub(fse, 'statSync').returns({ isDirectory: () => true }); - ddStub = sinon.stub(); - TEST_DD.invoke = ddStub; + TEST_REPORTER = {}; + TEST_DD = { + invoke: sinon.stub() + }; }); afterEach(() => { @@ -524,7 +569,7 @@ describe('Controller test - skill infrastructure controller test', () => { // setup sinon.stub(hashUtils, 'getHash').callsArgWith(1, 'hash error'); // call - skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, (err, res) => { + skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, TEST_DEPLOY_REGIONS, (err, res) => { // verify expect(res).equal(undefined); expect(err).equal('hash error'); @@ -536,13 +581,13 @@ describe('Controller test - skill infrastructure controller test', () => { // setup sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, TEST_HASH); ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(TEST_PROFILE, TEST_REGION, TEST_HASH); - ddStub.callsArgWith(2, 'invoke error'); + TEST_DD.invoke.callsArgWith(2, 'invoke error'); // call - skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, (err, res) => { + skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, TEST_DEPLOY_REGIONS, (err, res) => { // verify expect(res).equal(undefined); expect(err).equal('invoke error'); - expect(ddStub.args[0][1].code.isCodeModified).equal(false); + expect(TEST_DD.invoke.args[0][1].code.isCodeModified).equal(false); done(); }); }); @@ -550,12 +595,12 @@ describe('Controller test - skill infrastructure controller test', () => { it('| deploy delegate invoke passes with isCodeDeployed true, expect invoke result called back with currentHash', (done) => { // setup sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, TEST_HASH); - ddStub.callsArgWith(2, null, { + TEST_DD.invoke.callsArgWith(2, null, { isCodeDeployed: true, isAllStepSuccess: true }); // call - skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, (err, res) => { + skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, TEST_DEPLOY_REGIONS, (err, res) => { // verify expect(res).deep.equal({ isCodeDeployed: true, @@ -570,14 +615,14 @@ describe('Controller test - skill infrastructure controller test', () => { it('| deploy delegate invoke passes, expect invoke result called back', (done) => { // setup sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, TEST_HASH); - ddStub.callsArgWith(2, null, { + TEST_DD.invoke.callsArgWith(2, null, { isCodeDeployed: false, isAllStepSuccess: true, resultMessage: 'success', deployState: {} }); // call - skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, (err, res) => { + skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, TEST_DEPLOY_REGIONS, (err, res) => { // verify expect(res).deep.equal({ isCodeDeployed: false, @@ -590,25 +635,54 @@ describe('Controller test - skill infrastructure controller test', () => { }); }); + it('| deploy delegate invoke skipped, expect invoke result called back', (done) => { + // setup + sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, TEST_HASH); + TEST_REPORTER.skipTask = sinon.stub(); + TEST_DD.invoke.callsArgWith(2, null, { + isDeploySkipped: true, + resultMessage: 'skipped', + deployRegion: 'deployRegion', + deployState: {} + }); + // call + skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, TEST_DEPLOY_REGIONS, (err, res) => { + // verify + expect(res).deep.equal({ + isDeploySkipped: true, + resultMessage: 'skipped', + deployRegion: 'deployRegion', + deployState: {} + }); + expect(err).equal(null); + expect(TEST_REPORTER.skipTask.calledOnce).equal(true); + expect(TEST_REPORTER.skipTask.args[0][0]).equal('skipped'); + done(); + }); + }); + it('| deploy delegate invoke partial succeed with reasons called back, expect invoke result called back along with the message', (done) => { // setup sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, TEST_HASH); - ddStub.callsArgWith(2, null, { + TEST_DD.invoke.callsArgWith(2, null, { isCodeDeployed: true, isAllStepSuccess: false, resultMessage: 'partial fail', deployState: {} }); // call - skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, (err, res) => { + skillInfraController._deployInfraByRegion(TEST_REPORTER, TEST_DD, TEST_REGION, TEST_SKILL_NAME, TEST_DEPLOY_REGIONS, (err, res) => { // verify expect(res).equal(undefined); expect(err).deep.equal({ - isCodeDeployed: true, - isAllStepSuccess: false, - lastDeployHash: TEST_HASH, - resultMessage: 'partial fail', - deployState: {} + message: 'partial fail', + context: { + isCodeDeployed: true, + isAllStepSuccess: false, + lastDeployHash: TEST_HASH, + resultMessage: 'partial fail', + deployState: {} + } }); done(); }); @@ -616,19 +690,21 @@ describe('Controller test - skill infrastructure controller test', () => { }); describe('# test class method: _updateResourcesConfig', () => { - const TEST_DEPLOY_RESULT = { - default: { - endpoint: { - url: 'TEST_URL' - }, - lastDeployHash: 'TEST_HASH', - deployState: {} - } - }; + let TEST_REGION_LIST, TEST_DEPLOY_RESULT; const skillInfraController = new SkillInfrastructureController(TEST_CONFIGURATION); beforeEach(() => { + TEST_REGION_LIST = ['default']; + TEST_DEPLOY_RESULT = { + default: { + endpoint: { + url: 'TEST_URL' + }, + lastDeployHash: 'TEST_HASH', + deployState: {} + } + }; new ResourcesConfig(FIXTURE_RESOURCES_CONFIG_FILE_PATH); sinon.stub(fse, 'writeFileSync'); }); @@ -639,15 +715,41 @@ describe('Controller test - skill infrastructure controller test', () => { }); it('| update resources config correctly', () => { - // setup - sinon.stub(hashUtils, 'getHash').callsArgWith(1, 'hash error'); // call - skillInfraController._updateResourcesConfig(TEST_DEPLOY_RESULT); + skillInfraController._updateResourcesConfig(TEST_REGION_LIST, TEST_DEPLOY_RESULT); // verify expect(ResourcesConfig.getInstance().getCodeLastDeployHashByRegion(TEST_PROFILE, 'default')).equal('TEST_HASH'); expect(ResourcesConfig.getInstance().getSkillInfraDeployState(TEST_PROFILE)).deep.equal({ default: {} }); expect(fse.writeFileSync.callCount).equal(2); }); + + it('| update resources config using previous state for unreported regions', () => { + // setup + TEST_REGION_LIST = ['default', 'NA']; + ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(TEST_PROFILE, 'NA', 'TEST_HASH'); + ResourcesConfig.getInstance().setSkillInfraDeployState(TEST_PROFILE, { default: {}, NA: {} }); + // call + skillInfraController._updateResourcesConfig(TEST_REGION_LIST, TEST_DEPLOY_RESULT); + // verify + expect(ResourcesConfig.getInstance().getCodeLastDeployHashByRegion(TEST_PROFILE, 'NA')).equal('TEST_HASH'); + expect(ResourcesConfig.getInstance().getSkillInfraDeployState(TEST_PROFILE)).deep.equal({ default: {}, NA: {} }); + expect(fse.writeFileSync.callCount).equal(2); + }); + + it('| update resources config with no previous state for unreported regions', () => { + // setup + TEST_REGION_LIST = ['default', 'NA']; + ResourcesConfig.getInstance().setCodeLastDeployHashByRegion(TEST_PROFILE, 'NA', undefined); + ResourcesConfig.getInstance().setSkillInfraDeployState(TEST_PROFILE, undefined); + // call + skillInfraController._updateResourcesConfig(TEST_REGION_LIST, TEST_DEPLOY_RESULT); + // verify + expect(ResourcesConfig.getInstance().getCodeLastDeployHashByRegion(TEST_PROFILE, 'NA')).equal(undefined); + expect(ResourcesConfig.getInstance().getSkillInfraDeployState(TEST_PROFILE)).deep.equal({ default: {}, NA: undefined }); + expect(fse.writeFileSync.callCount).equal(2); + }); + + }); describe('# test class method: _ensureSkillManifestGotUpdated', () => { @@ -702,4 +804,69 @@ describe('Controller test - skill infrastructure controller test', () => { }); }); }); + + describe('# test class method: _getAlexaDeployRegions', () => { + const skillInfraController = new SkillInfrastructureController(TEST_CONFIGURATION); + + it('| alexaRegion is default, userConfig awsRegion is set, expect awsRegion is retrieved correctly.', (done) => { + // setup + const TEST_ALEXA_REGION = 'default'; + const USER_CONFIG_AWS_REGION = 'sa-east-1'; + const TEST_REGION_LIST = [TEST_ALEXA_REGION]; + const TEST_USER_CONFIG = { + awsRegion: USER_CONFIG_AWS_REGION + }; + // call + const deployRegions = skillInfraController._getAlexaDeployRegions(TEST_REGION_LIST, TEST_USER_CONFIG); + // verify + expect(deployRegions).deep.equal({ [TEST_ALEXA_REGION]: USER_CONFIG_AWS_REGION }); + done(); + }); + + it('| alexaRegion is default, userConfig awsRegion is NOT set, expect awsRegion is set based on Alexa and AWS region map.', (done) => { + // setup + const TEST_ALEXA_REGION = 'default'; + const MAPPING_ALEXA_DEFAULT_AWS_REGION = 'us-east-1'; + const TEST_REGION_LIST = [TEST_ALEXA_REGION]; + const TEST_USER_CONFIG = {}; + // call + const deployRegions = skillInfraController._getAlexaDeployRegions(TEST_REGION_LIST, TEST_USER_CONFIG); + // verify + expect(deployRegions).deep.equal({ [TEST_ALEXA_REGION]: MAPPING_ALEXA_DEFAULT_AWS_REGION }); + done(); + }); + + it('| alexaRegion is not default, userConfig regionalOverrides awsRegion is set, expect awsRegion is retrieved correctly.', (done) => { + // setup + const TEST_ALEXA_REGION_EU = 'EU'; + const USER_CONFIG_AWS_REGION = 'eu-west-2'; + const TEST_REGION_LIST = [TEST_ALEXA_REGION_EU]; + const TEST_USER_CONFIG = { + regionalOverrides: { + EU: { + awsRegion: USER_CONFIG_AWS_REGION + } + } + }; + // call + const deployRegions = skillInfraController._getAlexaDeployRegions(TEST_REGION_LIST, TEST_USER_CONFIG); + // verify + expect(deployRegions).deep.equal({ [TEST_ALEXA_REGION_EU]: USER_CONFIG_AWS_REGION }); + done(); + }); + + it('| alexaRegion is not default, userConfig regionalOverrides awsRegion is NOT set, ' + + 'expect awsRegion is set based on Alexa and AWS region map.', (done) => { + // setup + const TEST_ALEXA_REGION_EU = 'EU'; + const MAPPING_ALEXA_EU_AWS_REGION = 'eu-west-1'; + const TEST_REGION_LIST = [TEST_ALEXA_REGION_EU]; + const TEST_USER_CONFIG = {}; + // call + const deployRegions = skillInfraController._getAlexaDeployRegions(TEST_REGION_LIST, TEST_USER_CONFIG); + // verify + expect(deployRegions).deep.equal({ [TEST_ALEXA_REGION_EU]: MAPPING_ALEXA_EU_AWS_REGION }); + done(); + }); + }); }); diff --git a/test/unit/view/multi-tasks-view-test.js b/test/unit/view/multi-tasks-view-test.js index 0dd4b5a5..4d3f51ae 100644 --- a/test/unit/view/multi-tasks-view-test.js +++ b/test/unit/view/multi-tasks-view-test.js @@ -1,13 +1,12 @@ const { expect } = require('chai'); const sinon = require('sinon'); const Listr = require('listr'); -const events = require('events'); +const { EventEmitter } = require('events'); const { Observable } = require('rxjs'); const CliError = require('@src/exceptions/cli-error'); const MultiTasksView = require('@src/view/multi-tasks-view'); const { ListrReactiveTask } = MultiTasksView; -const { EventEmitter } = events; describe('View test - MultiTasksView test', () => { const TEST_TASK_HANDLE = () => 'taskHandle'; @@ -63,14 +62,31 @@ describe('View test - MultiTasksView test', () => { // setup const multiTasks = new MultiTasksView(TEST_OPTIONS); const newTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); - sinon.stub(Listr.prototype, 'run').rejects({ errors: ['error 1', { resultMessage: 'error 2' }, new Error('error 3')] }); + sinon.stub(Listr.prototype, 'run').rejects({ errors: ['error 1', new Error('error 2')] }); sinon.stub(ListrReactiveTask.prototype, 'execute'); multiTasks._listrTasks.push(newTask); // call multiTasks.start((err, res) => { // verify expect(res).equal(undefined); - expect(err.error).eql(new CliError('error 1\nerror 2\nerror 3')); + expect(err.error).eql(new CliError('error 1\nerror 2')); + expect(ListrReactiveTask.prototype.execute.callCount).equal(1); + done(); + }); + }); + + it('| task start trigger execute and taskRunner run fails with partial context', (done) => { + // setup + const multiTasks = new MultiTasksView(TEST_OPTIONS); + const newTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(Listr.prototype, 'run').rejects({ errors: ['error'], context: { result: 'partial' } }); + sinon.stub(ListrReactiveTask.prototype, 'execute'); + multiTasks._listrTasks.push(newTask); + // call + multiTasks.start((err, res) => { + // verify + expect(res).equal(undefined); + expect(err).deep.equal({ error: new CliError('error'), partialResult: { result: 'partial' } }); expect(ListrReactiveTask.prototype.execute.callCount).equal(1); done(); }); @@ -99,6 +115,10 @@ describe('View test - ListReactiveTask test', () => { const TEST_TASK_HANDLE = () => 'taskHandle'; const TEST_TASK_ID = 'taskId'; + afterEach(() => { + sinon.restore(); + }); + describe('# inspect correctness for constructor', () => { it('| initiate as a ListrReactiveTask class', () => { const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); @@ -110,46 +130,52 @@ describe('View test - ListReactiveTask test', () => { }); describe('# test class method: reporter getter', () => { + it('| getter function returns skipTask method which emit "skip" event', () => { + // setup + const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + const emitStub = sinon.stub(rxTask._eventEmitter, 'emit'); + // call + rxTask.reporter.skipTask('skippedReason'); + // verify + expect(emitStub.args[0][0]).equal('skip'); + expect(emitStub.args[0][1]).equal('skippedReason'); + }); + it('| getter function returns updateStatus method which emit "status" event', () => { // setup - const emitStub = sinon.stub(events.EventEmitter.prototype, 'emit'); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + const emitStub = sinon.stub(rxTask._eventEmitter, 'emit'); // call rxTask.reporter.updateStatus('statusUpdate'); // verify expect(emitStub.args[0][0]).equal('status'); expect(emitStub.args[0][1]).equal('statusUpdate'); - sinon.restore(); }); }); describe('# test class method: execute', () => { it('| execute task handle but callback with error, expect emit error event', () => { // setup - const emitStub = sinon.stub(events.EventEmitter.prototype, 'emit'); - const taskHandleStub = sinon.stub(); - taskHandleStub.callsArgWith(1, 'errorMessage'); + const taskHandleStub = sinon.stub().callsArgWith(1, 'errorMessage'); const rxTask = new ListrReactiveTask(taskHandleStub, TEST_TASK_ID); + const emitStub = sinon.stub(rxTask._eventEmitter, 'emit'); // call rxTask.execute(); // verify expect(emitStub.args[0][0]).equal('error'); expect(emitStub.args[0][1]).equal('errorMessage'); - sinon.restore(); }); - it('| execute task handle but callback with error, expect emit error event', () => { + it('| execute task handle with result, expect emit complete event', () => { // setup - const emitStub = sinon.stub(events.EventEmitter.prototype, 'emit'); - const taskHandleStub = sinon.stub(); - taskHandleStub.callsArgWith(1, null, { result: 'pass' }); + const taskHandleStub = sinon.stub().callsArgWith(1, null, { result: 'pass' }); const rxTask = new ListrReactiveTask(taskHandleStub, TEST_TASK_ID); + const emitStub = sinon.stub(rxTask._eventEmitter, 'emit'); // call rxTask.execute(); // verify expect(emitStub.args[0][0]).equal('complete'); expect(emitStub.args[0][1]).deep.equal({ result: 'pass' }); - sinon.restore(); }); }); @@ -178,8 +204,8 @@ describe('View test - ListReactiveTask test', () => { const subscribeStub = { next: sinon.stub() }; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('status').callsArgWith(1, 'statusUpdate'); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('status').callsArgWith(1, 'statusUpdate'); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub); @@ -192,55 +218,51 @@ describe('View test - ListReactiveTask test', () => { const subscribeStub = { error: sinon.stub() }; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('error').callsArgWith(1, 'error comes'); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('error').callsArgWith(1, 'error'); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub); // verify - expect(subscribeStub.error.args[0][0]).equal('error comes'); + expect(subscribeStub.error.args[0][0]).equal('error'); }); - it('| when "error" event emit with error.message, expect subscriber to call "error"', () => { + it('| when "error" event emit, expect subscriber to call "error" with partial result', () => { // setup - const TEST_ERROR_OBJ = { - resultMessage: 'error' - }; const subscribeStub = { error: sinon.stub() }; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('error').callsArgWith(1, TEST_ERROR_OBJ); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('error').callsArgWith(1, { message: 'error', context: 'partial'}); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub); // verify - expect(subscribeStub.error.args[0][0]).deep.equal(TEST_ERROR_OBJ); + expect(subscribeStub.error.args[0][0]).deep.equal({ message: 'error', context: 'partial'}); + expect(TEST_CONTEXT[TEST_TASK_ID]).equal('partial'); }); - it('| when "error" event emit with error object structure, expect subscriber to call "error" and set context', () => { + it('| when "skip" event emit, expect task to call "skip"', () => { // setup - const TEST_ERROR_OBJ = { - resultMessage: 'error', - deployState: 'state' - }; - const subscribeStub = { - error: sinon.stub() - }; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('error').callsArgWith(1, TEST_ERROR_OBJ); + const subscribeStub = {}; + const TEST_TASK = { + skip: sinon.stub() + } const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('skip').callsArgWith(1, 'skippedReason'); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub); // verify - expect(subscribeStub.error.args[0][0]).deep.equal(TEST_ERROR_OBJ); + expect(TEST_TASK.skip.calledOnce).equal(true); + expect(TEST_TASK.skip.args[0][0]).equal('skippedReason'); }); - it('| when "title" event emit, expect subscriber to call "title"', () => { + it('| when "title" event emit, expect task title to be set', () => { // setup const subscribeStub = {}; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('title').callsArgWith(1, 'new title'); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('title').callsArgWith(1, 'new title'); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub); @@ -253,8 +275,8 @@ describe('View test - ListReactiveTask test', () => { const subscribeStub = { complete: sinon.stub() }; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('complete').callsArgWith(1); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('complete').callsArgWith(1); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub); @@ -267,8 +289,8 @@ describe('View test - ListReactiveTask test', () => { const subscribeStub = { complete: sinon.stub() }; - sinon.stub(events.EventEmitter.prototype, 'on').withArgs('complete').callsArgWith(1, 'done'); const rxTask = new ListrReactiveTask(TEST_TASK_HANDLE, TEST_TASK_ID); + sinon.stub(rxTask._eventEmitter, 'on').withArgs('complete').callsArgWith(1, 'done'); // call const obsv = rxTask.buildObservable()(TEST_CONTEXT, TEST_TASK); obsv._subscribe(subscribeStub);