From 808be3ac7816e7637baad55ac3c98641d6b883f2 Mon Sep 17 00:00:00 2001 From: Chih-Ying Chang Date: Thu, 5 Dec 2019 14:25:44 -0800 Subject: [PATCH] feat: add lambda deployer --- .../deploy-delegates/cfn-deployer/index.js | 8 +- .../lambda-deployer/helper.js | 224 ++++++++++ .../deploy-delegates/lambda-deployer/index.js | 100 +++++ lib/clients/aws-client/aws-util.js | 4 +- lib/clients/aws-client/iam-client.js | 90 ++++ lib/clients/aws-client/lambda-client.js | 161 +++++++ lib/commands/init/aws-setup-helper.js | 10 +- .../deploy-delegate.js | 5 + lib/utils/constants.js | 38 +- package.json | 1 + .../builtins/lambda-deployer/helper-test.js | 421 ++++++++++++++++++ .../builtins/lambda-deployer/index-test.js | 171 +++++++ .../clients/aws-client/iam-client-test.js | 146 ++++++ .../clients/aws-client/lambda-client-test.js | 228 ++++++++++ test/unit/run-test.js | 5 + 15 files changed, 1594 insertions(+), 18 deletions(-) create mode 100644 lib/builtins/deploy-delegates/lambda-deployer/helper.js create mode 100644 lib/builtins/deploy-delegates/lambda-deployer/index.js create mode 100644 lib/clients/aws-client/iam-client.js create mode 100644 lib/clients/aws-client/lambda-client.js create mode 100644 test/unit/builtins/lambda-deployer/helper-test.js create mode 100644 test/unit/builtins/lambda-deployer/index-test.js create mode 100644 test/unit/clients/aws-client/iam-client-test.js create mode 100644 test/unit/clients/aws-client/lambda-client-test.js diff --git a/lib/builtins/deploy-delegates/cfn-deployer/index.js b/lib/builtins/deploy-delegates/cfn-deployer/index.js index 9eb10723..c267e4a0 100644 --- a/lib/builtins/deploy-delegates/cfn-deployer/index.js +++ b/lib/builtins/deploy-delegates/cfn-deployer/index.js @@ -21,20 +21,18 @@ module.exports = { function bootstrap(options, callback) { const { profile, userConfig, workspacePath } = options; const templateLocation = path.join(workspacePath, SKILL_STACK_PUBLIC_FILE_NAME); + let updatedUserConfig; try { const templateContent = fs.readFileSync(path.join(__dirname, 'assets', SKILL_STACK_ASSET_FILE_NAME), 'utf-8'); const awsProfile = awsUtil.getAWSProfile(profile); const awsDefaultRegion = awsUtil.getCLICompatibleDefaultRegion(awsProfile); fs.writeFileSync(templateLocation, templateContent); userConfig.templatePath = `.${path.sep}${path.join('infrastructure', path.basename(workspacePath), SKILL_STACK_PUBLIC_FILE_NAME)}`; - if (awsDefaultRegion) { - R.set(R.lensPath(['regionalOverrides', 'default', 'awsRegion']), awsDefaultRegion, userConfig); - } + updatedUserConfig = R.set(R.lensPath(['awsRegion']), awsDefaultRegion, userConfig); } catch (e) { return callback(e.message); } - - callback(null, { userConfig }); + callback(null, { userConfig: updatedUserConfig }); } /** diff --git a/lib/builtins/deploy-delegates/lambda-deployer/helper.js b/lib/builtins/deploy-delegates/lambda-deployer/helper.js new file mode 100644 index 00000000..bc2f6976 --- /dev/null +++ b/lib/builtins/deploy-delegates/lambda-deployer/helper.js @@ -0,0 +1,224 @@ +const async = require('async'); +const fs = require('fs'); +const R = require('ramda'); + +const CONSTANTS = require('@src/utils/constants'); +const IAMClient = require('@src/clients/aws-client/iam-client'); +const LambdaClient = require('@src/clients/aws-client/lambda-client'); +const Manifest = require('@src/model/manifest'); +const retryUtils = require('@src/utils/retry-utility'); +const stringUtils = require('@src/utils/string-utils'); + +module.exports = { + validateLambdaDeployState, + deployIAMRole, + deployLambdaFunction +}; + +function validateLambdaDeployState(reporter, awsProfile, awsRegion, currentRegionDeployState, callback) { + const lambdaData = currentRegionDeployState.lambda; + if (R.isNil(lambdaData) || R.isEmpty(lambdaData) || R.isNil(lambdaData.arn)) { + return callback(null, { updatedDeployState: currentRegionDeployState }); + } + reporter.updateStatus('Validating the deploy state of existing Lambda function...'); + let updatedDeployState; + const lambdaClient = new LambdaClient({ awsProfile, awsRegion }); + const lambdaArn = lambdaData.arn; + lambdaClient.getFunction(lambdaArn, (err, data) => { + if (err) { + return callback(err); + } + // 1. check IAM role arn + const localIAMRole = currentRegionDeployState.iamRole; + const remoteIAMRole = data.Configuration.Role; + if (stringUtils.isNonBlankString(localIAMRole) && !R.equals(localIAMRole, remoteIAMRole)) { + return callback(`The IAM role for Lambda ARN (${lambdaArn}) should be ${remoteIAMRole}, but found ${localIAMRole}. \ +Please solve this IAM role mismatch and re-deploy again.`); + } + updatedDeployState = R.set(R.lensPath(['iamRole']), remoteIAMRole, currentRegionDeployState); + // 2. check revision id + const localRevisionId = lambdaData.revisionId; + const remoteRevisionId = data.Configuration.RevisionId; + if (stringUtils.isNonBlankString(localRevisionId) && !R.equals(localRevisionId, remoteRevisionId)) { + return callback(`The current revisionId (The revision ID for Lambda ARN (${lambdaArn}) should be ${remoteRevisionId}, \ +but found ${localRevisionId}. Please solve this revision mismatch and re-deploy again.`); + } + updatedDeployState = R.set(R.lensPath(['lambda', 'revisionId']), remoteRevisionId, currentRegionDeployState); + // 3. add lastModified + const lastModified = data.Configuration.LastModified; + updatedDeployState = R.set(R.lensPath(['lambda', 'lastModified']), lastModified, currentRegionDeployState); + callback(null, { updatedDeployState }); + }); +} + +function deployIAMRole(reporter, awsProfile, alexaRegion, skillName, awsRegion, deployState, callback) { + const iamClient = new IAMClient({ awsProfile, awsRegion }); + const roleArn = deployState.iamRole; + if (R.isNil(roleArn) || R.isEmpty(roleArn)) { + reporter.updateStatus('No IAM role exists. Creating an IAM role...'); + _createIAMRole(reporter, iamClient, skillName, (roleErr, iamRoleArn) => { + if (roleErr) { + return callback(roleErr); + } + callback(null, iamRoleArn); + }); + } else { + iamClient.getIAMRole(roleArn, (roleErr, roleData) => { + if (roleErr) { + if (roleErr.code === 'NoSuchEntity') { + callback(`The IAM role is not found. Please check if your IAM role from region ${alexaRegion} is valid.`); + } else { + callback(roleErr); + } + } else { + reporter.updateStatus(`Current IAM role : "${roleArn}"...`); + callback(null, roleData.Role.Arn); + } + }); + } +} + +function _createIAMRole(reporter, iamClient, skillName, callback) { + iamClient.createBasicLambdaRole(skillName, (roleErr, roleData) => { + if (roleErr) { + return callback(roleErr); + } + const roleArn = roleData.Role.Arn; + reporter.updateStatus(`Create Role (arn: ${roleArn}) in progress...`); + iamClient.attachBasicLambdaRolePolicy(roleArn, (policyErr) => { + if (policyErr) { + return callback(policyErr); + } + callback(null, roleData.Role.Arn); + }); + }); +} + +function deployLambdaFunction(reporter, options, callback) { + const { profile, awsProfile, alexaRegion, awsRegion, skillId, skillName, code, iamRoleArn, userConfig, currentRegionDeployState } = options; + const lambdaClient = new LambdaClient({ awsProfile, awsRegion }); + const zipFilePath = code.codeBuild; + if (R.isNil(currentRegionDeployState.lambda) || R.isEmpty(currentRegionDeployState.lambda)) { + reporter.updateStatus('No Lambda information exists. Creating a lambda function...'); + const createLambdaOptions = { + profile, + alexaRegion, + skillId, + skillName, + iamRoleArn, + zipFilePath, + userConfig + }; + _createLambdaFunction(reporter, lambdaClient, createLambdaOptions, (lambdaErr, lambdaResources) => { + if (lambdaErr) { + return callback(lambdaErr); + } + callback(null, lambdaResources); + }); + } else { + reporter.updateStatus(`Updating current Lambda function with ARN ${currentRegionDeployState.lambda.arn}...`); + const updateLambdaOptions = { + zipFilePath, + userConfig, + currentRegionDeployState + }; + _updateLambdaFunction(reporter, lambdaClient, updateLambdaOptions, (lambdaErr, lambdaResources) => { + if (lambdaErr) { + return callback(lambdaErr); + } + callback(null, lambdaResources); + }); + } +} + +function _createLambdaFunction(reporter, lambdaClient, options, callback) { + const { profile, alexaRegion, skillId, skillName, iamRoleArn, zipFilePath, userConfig } = options; + const zipFile = fs.readFileSync(zipFilePath); + const { runtime, handler } = userConfig; + const retryConfig = { + base: CONSTANTS.CONFIGURATION.RETRY.CREATE_LAMBDA_FUNCTION.BASE, + factor: CONSTANTS.CONFIGURATION.RETRY.CREATE_LAMBDA_FUNCTION.FACTOR, + maxRetry: CONSTANTS.CONFIGURATION.RETRY.CREATE_LAMBDA_FUNCTION.MAXRETRY + }; + const RETRY_MESSAGE = 'The role defined for the function cannot be assumed by Lambda.'; + const retryCall = (loopCallback) => { + // 1. Create a Lambda function + lambdaClient.createLambdaFunction(skillName, profile, alexaRegion, iamRoleArn, zipFile, runtime, handler, (lambdaErr, lambdaData) => { + if (lambdaErr) { + // There may be a (small) window of time after creating IAM role and adding policies, the role will trigger the error + // if creating lambda function during this timming. Thus, use retry to bypass this issue. + if (lambdaErr.message === RETRY_MESSAGE) { + return loopCallback(null, RETRY_MESSAGE); + } + return loopCallback(lambdaErr); + } + loopCallback(null, lambdaData); + }); + }; + const shouldRetryCondition = retryResponse => retryResponse === RETRY_MESSAGE; + retryUtils.retry(retryConfig, retryCall, shouldRetryCondition, (retryErr, lambdaData) => { + if (retryErr) { + return callback(retryErr); + } + const arn = lambdaData.FunctionArn; + // 2. Grant permissions to use a Lambda function + _addEventPermissions(lambdaClient, skillId, arn, (addErr) => { + if (addErr) { + return callback(addErr); + } + // 3. Get the latest revisionId from getFunction API + lambdaClient.getFunction(arn, (revisionErr, revisionData) => { + if (revisionErr) { + return callback(revisionErr); + } + callback(null, { + arn, + lastModified: lambdaData.LastModified, + revisionId: revisionData.Configuration.RevisionId + }); + }); + }); + }); +} + +function _addEventPermissions(lambdaClient, skillId, functionArn, callback) { + // TODO: to move Manifest outside the deployer logic + const domainInfo = Manifest.getInstance().getApis(); + const domainList = R.keys(domainInfo); + async.forEach(domainList, (domain, addCallback) => { + lambdaClient.addAlexaPermissionByDomain(domain, skillId, functionArn, (err) => { + if (err) { + return addCallback(err); + } + addCallback(); + }); + }, (error) => { + callback(error); + }); +} + +function _updateLambdaFunction(reporter, lambdaClient, options, callback) { + const { zipFilePath, userConfig, currentRegionDeployState } = options; + const zipFile = fs.readFileSync(zipFilePath); + const functionName = currentRegionDeployState.lambda.arn; + let { revisionId } = currentRegionDeployState.lambda; + lambdaClient.updateFunctionCode(zipFile, functionName, revisionId, (codeErr, codeData) => { + if (codeErr) { + return callback(codeErr); + } + reporter.updateStatus(`Update a lambda function (${functionName}) in progress...`); + const { runtime } = userConfig; + const { handler } = userConfig; + revisionId = codeData.RevisionId; + lambdaClient.updateFunctionConfiguration(functionName, runtime, handler, revisionId, (configErr, configData) => { + if (configErr) { + return callback(configErr); + } + callback(null, { + arn: configData.FunctionArn, + lastModified: configData.LastModified, + revisionId: configData.RevisionId + }); + }); + }); +} diff --git a/lib/builtins/deploy-delegates/lambda-deployer/index.js b/lib/builtins/deploy-delegates/lambda-deployer/index.js new file mode 100644 index 00000000..bc19fc90 --- /dev/null +++ b/lib/builtins/deploy-delegates/lambda-deployer/index.js @@ -0,0 +1,100 @@ +const R = require('ramda'); + +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: 'ap-northeast-1' +}; + +module.exports = { + bootstrap, + invoke +}; + +/** + * Bootstrap ask-cli resources config with initial configuration. + * @param {Object} options + * @param {Function} callback + */ +function bootstrap(options, callback) { + const { profile, userConfig } = options; + const awsProfile = awsUtil.getAWSProfile(profile); + const awsDefaultRegion = awsUtil.getCLICompatibleDefaultRegion(awsProfile); + const updatedUserConfig = R.set(R.lensPath(['awsRegion']), awsDefaultRegion, userConfig); + callback(null, { userConfig: updatedUserConfig }); +} + +/** + * Invoke the actual deploy logic for skill's infrastructure + * @param {Object} reporter upstream CLI status reporter + * @param {Object} options + * @param {Function} callback + */ +function invoke(reporter, options, callback) { + const { profile, alexaRegion, skillId, skillName, code, userConfig, deployState } = options; + const awsProfile = awsUtil.getAWSProfile(profile); + if (!stringUtils.isNonBlankString(awsProfile)) { + return callback(`Profile [${profile}] doesn't have AWS profile linked to it. Please run "ask init" to re-configure your porfile.`); + } + let currentRegionDeployState = deployState[alexaRegion]; + if (!currentRegionDeployState) { + currentRegionDeployState = {}; + deployState[alexaRegion] = currentRegionDeployState; + } + const awsRegion = R.view(R.lensPath(['regionalOverrides', alexaRegion, 'awsRegion']), userConfig) || alexaAwsRegionMap[alexaRegion]; + if (!stringUtils.isNonBlankString(awsRegion)) { + return callback(`Unsupported Alexa region: ${alexaRegion}. Please check your region name or use "regionalOverrides" to specify AWS region.`); + } + + helper.validateLambdaDeployState(reporter, awsProfile, awsRegion, currentRegionDeployState, (validationErr, validationData) => { + if (validationErr) { + return callback(validationErr); + } + currentRegionDeployState = validationData.updatedDeployState; + helper.deployIAMRole(reporter, awsProfile, alexaRegion, skillName, awsRegion, currentRegionDeployState, (iamErr, iamRoleArn) => { + if (iamErr) { + return callback(iamErr); + } + deployState[alexaRegion].iamRole = iamRoleArn; + const deployLambdaConfig = { + profile, + awsProfile, + alexaRegion, + awsRegion, + skillId, + skillName, + code, + iamRoleArn, + userConfig, + currentRegionDeployState + }; + helper.deployLambdaFunction(reporter, deployLambdaConfig, (lambdaErr, lambdaResult) => { + if (lambdaErr) { + const errMessage = `The lambda deploy failed for Alexa region "${alexaRegion}": ${lambdaErr}`; + const reasons = [errMessage]; + return callback(null, { + reasons, + message: errMessage, + deployState: deployState[alexaRegion] + }); + } + const { arn, lastModified, revisionId } = lambdaResult; + deployState[alexaRegion].lambda = { + arn, + lastModified, + revisionId + }; + return callback(null, { + endpoint: { uri: arn }, + message: `The lambda deploy succeeded for Alexa region "${alexaRegion}" with output Lambda ARN: ${arn}.`, + deployState: deployState[alexaRegion] + }); + }); + }); + }); +} diff --git a/lib/clients/aws-client/aws-util.js b/lib/clients/aws-client/aws-util.js index 1da11342..d023473e 100644 --- a/lib/clients/aws-client/aws-util.js +++ b/lib/clients/aws-client/aws-util.js @@ -58,7 +58,7 @@ class SharedIniFile { } /** - * function used to retrieve default aws region + * function used to retrieve default aws region or return a global default aws region if a set one is not found. * @param {string} runtimeProfile aws profile name */ function getCLICompatibleDefaultRegion(awsProfile) { @@ -78,5 +78,5 @@ function getCLICompatibleDefaultRegion(awsProfile) { const section = configFile.getProfile(profile); region = section && section.region; } - return region; + return region || CONSTANT.AWS_SKILL_INFRASTRUCTURE_DEFAULT_REGION; } diff --git a/lib/clients/aws-client/iam-client.js b/lib/clients/aws-client/iam-client.js new file mode 100644 index 00000000..fd0f31f4 --- /dev/null +++ b/lib/clients/aws-client/iam-client.js @@ -0,0 +1,90 @@ +const aws = require('aws-sdk'); + +const CONSTANTS = require('@src/utils/constants'); +const stringUtils = require('@src/utils/string-utils'); + +/** + * Class for AWS IAM Client + */ +module.exports = class IAMClient { + constructor(configuration) { + const { awsProfile, awsRegion } = configuration; + if (!stringUtils.isNonBlankString(awsProfile) || !stringUtils.isNonBlankString(awsRegion)) { + throw new Error('Invalid awsProfile or Invalid awsRegion'); + } + aws.config.credentials = new aws.SharedIniFileCredentials({ + profile: awsProfile + }); + this.awsRegion = awsRegion; + aws.config.region = this.awsRegion; + this.client = new aws.IAM(); + } + + /** + * Wrapper of iam sdk api + * Retrieves information about the specified role + * @param {string} roleArn The arn of the IAM role to get information about. + * @param {callback} callback { error, response } + */ + getIAMRole(roleArn, callback) { + const params = { + RoleName: this._extractIAMRoleName(roleArn) + }; + this.client.getRole(params, (err, response) => { + callback(err, !err ? response : null); + }); + } + + /** + * Wrapper of iam sdk api + * Creates a new role for AWS account. + * @param {string} skillName The name of the skill to generate a IAM role name. + * @param {callback} callback { error, response } + */ + createBasicLambdaRole(skillName, callback) { + const roleName = this._generateIAMRoleName(skillName); + const policy = CONSTANTS.AWS.IAM.ROLE.LAMBDA_BASIC_ROLE.POLICY; + const params = { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify(policy) + }; + this.client.createRole(params, (err, response) => { + callback(err, !err ? response : null); + }); + } + + /** + * Wrapper of iam sdk api + * Attaches the specified managed policy to the specified IAM role. + * @param {string} roleArn The Amazon Resource Name (ARN) specifying the group. + * @param {callback} callback { error, response } + */ + attachBasicLambdaRolePolicy(roleArn, callback) { + const params = { + PolicyArn: CONSTANTS.AWS.IAM.ROLE.LAMBDA_BASIC_ROLE.POLICY_ARN, + RoleName: this._extractIAMRoleName(roleArn) + }; + this.client.attachRolePolicy(params, (err, response) => { + callback(err, !err ? response : null); + }); + } + + /** + * Extracts IAM Role from an existing iam role arn. + * @param {string} roleArn The Amazon Resource Name (ARN) specifying the group. + */ + _extractIAMRoleName(roleArn) { + return roleArn.split('role/').pop(); + } + + /** + * Generates a valid IAM Role function name. + * a IAM Role function name should follow the pattern: ask-lambda-skillName-timeStamp + * @param {string} skillName + */ + _generateIAMRoleName(skillName) { + const roleNamePrefix = process.env.ASK_DEPLOY_ROLE_PREFIX || 'ask-lambda'; + const validSkillName = skillName.replace(/_/g, '-'); + return `${roleNamePrefix}-${validSkillName}-${Date.now()}`; + } +}; diff --git a/lib/clients/aws-client/lambda-client.js b/lib/clients/aws-client/lambda-client.js new file mode 100644 index 00000000..964b0be5 --- /dev/null +++ b/lib/clients/aws-client/lambda-client.js @@ -0,0 +1,161 @@ +const aws = require('aws-sdk'); + +const stringUtils = require('@src/utils/string-utils'); + +/** + * Class for Lambda Client + */ +module.exports = class LambdaClient { + constructor(configuration) { + const { awsProfile, awsRegion } = configuration; + if (!stringUtils.isNonBlankString(awsProfile) || !stringUtils.isNonBlankString(awsRegion)) { + throw new Error('Invalid awsProfile or Invalid awsRegion'); + } + aws.config.credentials = new aws.SharedIniFileCredentials({ + profile: awsProfile + }); + this.awsRegion = awsRegion; + aws.config.region = this.awsRegion; + this.client = new aws.Lambda(); + } + + /** + * Wrapper of aws sdk api + * Creates a Lambda function + * @param {string} functionName The name of the Lambda function. + * @param {string} role The Amazon Resource Name (ARN) of the function's execution role. + * @param {Buffer} zipFile The base64-encoded contents of the deployment package. + * @param {string} runtime The identifier of the function's runtime. + * @param {string} handler The name of the method within your code that Lambda calls to execute your function. + * @param {callback} callback { error, response } + */ + createLambdaFunction(skillName, profile, alexaRegion, role, zipFile, runtime, handler, callback) { + const params = { + Code: { + ZipFile: zipFile + }, + Role: role, + Runtime: runtime, + Handler: handler, + FunctionName: this._generateFunctionName(skillName, profile, alexaRegion) + }; + this.client.createFunction(params, (err, data) => { + callback(err, !err ? data : null); + }); + } + + /** + * Wrapper of aws sdk api + * Grants an AWS service or another account permission to use a function. + * @param {string} domain The type of skill. + * @param {string} skillId The skill ID is used as a token that must be supplied by the invoker. + * @param {string} functionArn The name of the Lambda function. + * @param {callback} callback { error, response } + */ + addAlexaPermissionByDomain(domain, skillId, functionArn, callback) { + const params = this._getDomainPermission(domain); + params.FunctionName = functionArn; + params.EventSourceToken = skillId; + this.client.addPermission(params, (err, data) => { + callback(err, !err ? data : null); + }); + } + + /** + * Wrapper of aws sdk api + * Returns information about the function or function version + * @param {string} functionArn The name of the Lambda function + * @param {callback} callback { error, response } + */ + getFunction(functionArn, callback) { + const params = { + FunctionName: functionArn + }; + this.client.getFunction(params, (err, data) => { + callback(err, !err ? data : null); + }); + } + + /** + * Wrapper of aws sdk api + * Updates a Lambda function's code. + * @param {Buffer} zipFile The base64-encoded contents of the deployment package. + * @param {string} functionName The name of the Lambda function. + * @param {string} revisionId Only update the function if the revision ID matches the ID that's specified. + * @param {callback} callback { error, response } + */ + updateFunctionCode(zipFile, functionName, revisionId, callback) { + const codeUpdateParams = { + ZipFile: zipFile, + FunctionName: functionName, + RevisionId: revisionId + }; + this.client.updateFunctionCode(codeUpdateParams, (err, data) => { + callback(err, !err ? data : null); + }); + } + + /** + * Wrapper of aws sdk api + * Modifies the version-specific settings of a Lambda function. + * @param {string} functionName The name of the Lambda function. + * @param {string} runtime The identifier of the function's runtime. + * @param {string} handler The name of the method within your code that Lambda calls to execute your function. + * @param {string} revisionId Only update the function if the revision ID matches the ID that's specified. + * @param {callback} callback { error, response } + */ + updateFunctionConfiguration(functionName, runtime, handler, revisionId, callback) { + const configurationUpdateParams = { + FunctionName: functionName, + Runtime: runtime, + Handler: handler, + RevisionId: revisionId + }; + this.client.updateFunctionConfiguration(configurationUpdateParams, (err, data) => { + callback(err, !err ? data : null); + }); + } + + /** + * Gets a permission configuration by skill domain. + * @param {string} domain skill domain + */ + _getDomainPermission(domain) { + let permission; + switch (domain) { + case 'smartHome': + case 'video': + permission = { + Action: 'lambda:InvokeFunction', + StatementId: Date.now().toString(), + Principal: 'alexa-connectedhome.amazon.com' + }; + break; + case 'custom': + case 'houseHoldList': + case 'music': + permission = { + Action: 'lambda:InvokeFunction', + StatementId: Date.now().toString(), + Principal: 'alexa-appkit.amazon.com' + }; + break; + default: + } + return permission; + } + + /** + * Generates a valid Lambda function name. + * a lambda function name should follow the pattern: ask-skillName-profileName-alexaRegion-timeStamp + * a valid function name cannot longer than 64 characters, so cli fixes the project name no longer than 22 characters + * @param {String} skillName + * @param {String} awsProfile + */ + _generateFunctionName(skillName, profile, alexaRegion) { + const validSkillName = stringUtils.filterNonAlphanumeric(skillName.toLowerCase()).substring(0, 22); + const validProfile = stringUtils.filterNonAlphanumeric(profile.toLowerCase()); + const shortRegionName = alexaRegion.replace(/-/g, ''); + return `ask-${validSkillName}-${validProfile}-${shortRegionName}-${Date.now()}`; + } +}; diff --git a/lib/commands/init/aws-setup-helper.js b/lib/commands/init/aws-setup-helper.js index 77afdb51..5b9ee886 100644 --- a/lib/commands/init/aws-setup-helper.js +++ b/lib/commands/init/aws-setup-helper.js @@ -53,13 +53,13 @@ function openIamCreateUserPage(isBrowser, userName, callback) { userNames: userName, permissionType: 'policies', policies: [ - CONSTANTS.AWS.IAM.POLICY_ARN.IAM_FULL, - CONSTANTS.AWS.IAM.POLICY_ARN.CFN_FULL, - CONSTANTS.AWS.IAM.POLICY_ARN.S3_FULL, - CONSTANTS.AWS.IAM.POLICY_ARN.LAMBDA_FULL + CONSTANTS.AWS.IAM.USER.POLICY_ARN.IAM_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.CFN_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.S3_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.LAMBDA_FULL ] }; - const awsIamUrl = `${CONSTANTS.AWS.IAM.NEW_USER_BASE_URL}${querystring.stringify(params)}`; + const awsIamUrl = `${CONSTANTS.AWS.IAM.USER.NEW_USER_BASE_URL}${querystring.stringify(params)}`; console.log(messages.AWS_CREATE_PROFILE_TITLE); if (isBrowser) { setTimeout(() => { diff --git a/lib/controllers/skill-infrastructure-controller/deploy-delegate.js b/lib/controllers/skill-infrastructure-controller/deploy-delegate.js index f9ae73fd..14785bdb 100644 --- a/lib/controllers/skill-infrastructure-controller/deploy-delegate.js +++ b/lib/controllers/skill-infrastructure-controller/deploy-delegate.js @@ -7,6 +7,11 @@ const BUILT_IN_DEPLOY_DELEGATES = { + 'and using AWS CloudFormation to configure all the skill needed AWS resources. ' + 'Will keep polling the CloudFormation status and update deploy progress in real time. ' + 'Starting from a basic skill-template.yaml with AWS Lambda related resources.' + }, + '@ask-cli/lambda-deployer': { + location: 'lambda-deployer', + description: 'Deploy skill infrastructure by creating an AWS IAM Role with basic permissions to access AWS Lambda, ' + + 'uploading the local skill code and updating the configuration to an AWS Lambda function.' } }; diff --git a/lib/utils/constants.js b/lib/utils/constants.js index 7d474fa4..029f0cbf 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -52,6 +52,11 @@ module.exports.CONFIGURATION = { MAX_RETRY: 30, MIN_TIME_OUT: 1000, FACTOR: 1.2 + }, + CREATE_LAMBDA_FUNCTION: { + BASE: 5000, + FACTOR: 1.5, + MAXRETRY: 3 } }, S3: { @@ -106,14 +111,35 @@ module.exports.TEMPLATES = { } }; +module.exports.AWS_SKILL_INFRASTRUCTURE_DEFAULT_REGION = 'us-east-1'; + module.exports.AWS = { IAM: { - NEW_USER_BASE_URL: 'https://console.aws.amazon.com/iam/home?region=undefined#/users$new?', - POLICY_ARN: { - IAM_FULL: 'arn:aws:iam::aws:policy/IAMFullAccess', - CFN_FULL: 'arn:aws:iam::aws:policy/AWSCloudFormationFullAccess', - S3_FULL: 'arn:aws:iam::aws:policy/AmazonS3FullAccess', - LAMBDA_FULL: 'arn:aws:iam::aws:policy/AWSLambdaFullAccess' + USER: { + NEW_USER_BASE_URL: 'https://console.aws.amazon.com/iam/home?region=undefined#/users$new?', + POLICY_ARN: { + IAM_FULL: 'arn:aws:iam::aws:policy/IAMFullAccess', + CFN_FULL: 'arn:aws:iam::aws:policy/AWSCloudFormationFullAccess', + S3_FULL: 'arn:aws:iam::aws:policy/AmazonS3FullAccess', + LAMBDA_FULL: 'arn:aws:iam::aws:policy/AWSLambdaFullAccess' + } + }, + ROLE: { + LAMBDA_BASIC_ROLE: { + POLICY_ARN: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + POLICY: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com' + }, + Action: 'sts:AssumeRole' + } + ] + } + } } } }; diff --git a/package.json b/package.json index bd087037..5624d630 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "lib/utils/zip-utils.js", "lib/utils/hash-utils.js", "lib/utils/retry-utility.js", + "lib/builtins/*", "lib/clients/*", "lib/model/*", "lib/view/*", diff --git a/test/unit/builtins/lambda-deployer/helper-test.js b/test/unit/builtins/lambda-deployer/helper-test.js new file mode 100644 index 00000000..8d9f6727 --- /dev/null +++ b/test/unit/builtins/lambda-deployer/helper-test.js @@ -0,0 +1,421 @@ +const aws = require('aws-sdk'); +const { expect } = require('chai'); +const fs = require('fs'); +const path = require('path'); +const sinon = require('sinon'); + +const helper = require('@src/builtins/deploy-delegates/lambda-deployer/helper'); +const IAMClient = require('@src/clients/aws-client/iam-client'); +const LambdaClient = require('@src/clients/aws-client/lambda-client'); +const Manifest = require('@src/model/manifest'); + +describe('Builtins test - lambda-deployer helper.js test', () => { + const FIXTURE_MANIFEST_FILE_PATH = path.join(process.cwd(), 'test', 'unit', 'fixture', 'model', 'manifest.json'); + const TEST_PROFILE = 'default'; // test file uses 'default' profile + const TEST_AWS_PROFILE = 'default'; + const TEST_AWS_REGION = 'us-east-1'; + const TEST_ALEXA_REGION = 'default'; + const TEST_SKILL_NAME = 'skill_name'; + const TEST_SKILL_ID = 'skill_id'; + const TEST_IAM_ROLE_ARN = 'iam_role_arn'; + const TEST_LAMBDA_ARN = 'iam_lambda_arn'; + const TEST_NO_SUCH_ENTITY_ERROR = { + code: 'NoSuchEntity' + }; + const REPORTER = { + updateStatus: () => {} + }; + const TEST_ROLE_DATA = { + Role: { + Arn: TEST_IAM_ROLE_ARN + } + }; + + describe('# test class method: validateLambdaDeployState', () => { + const TEST_LOCAL_IAM_ROLE = 'local_iam_role'; + const TEST_LOCAL_REVISION_ID = 'local_revision_id'; + const TEST_LOCAL_DEPLOY_STATE = { + iamRole: TEST_LOCAL_IAM_ROLE, + lambda: { + arn: TEST_LAMBDA_ARN, + revisionId: TEST_LOCAL_REVISION_ID + } + }; + const TEST_REMOTE_IAM_ROLE = 'remote_iam_role'; + const TEST_REMOTE_REVISION_ID = 'remote_revision_id'; + + afterEach(() => { + sinon.restore(); + }); + + it('| no lambda arn found, expect original deploystate return', (done) => { + // setup + const TEST_DEPLOY_STATE_NO_LAMBDA = {}; + // call + helper.validateLambdaDeployState(REPORTER, TEST_AWS_PROFILE, TEST_AWS_REGION, TEST_DEPLOY_STATE_NO_LAMBDA, (err, data) => { + // verify + expect(data.updatedDeployState).equal(TEST_DEPLOY_STATE_NO_LAMBDA); + done(); + }); + }); + + it('| an existing lambda arn found, getFunction request fails, expect an error return', (done) => { + // setup + const TEST_GET_FUNCTION_ERROR = 'get_function_error'; + sinon.stub(LambdaClient.prototype, 'getFunction').callsArgWith(1, TEST_GET_FUNCTION_ERROR); + // call + helper.validateLambdaDeployState(REPORTER, TEST_AWS_PROFILE, TEST_AWS_REGION, TEST_LOCAL_DEPLOY_STATE, (err) => { + // verify + expect(err).equal(TEST_GET_FUNCTION_ERROR); + done(); + }); + }); + + it('| an existing lambda arn found, getFunction request passes, the local iam role does not equal with the remote iam role, ' + + 'expect an error return', (done) => { + // setup + const TEST_REMOTE_DEPLOY_STATE = { + Configuration: { + Role: TEST_REMOTE_IAM_ROLE, + RevisionId: TEST_REMOTE_REVISION_ID + } + }; + const TEST_IAM_ROLE_ERROR = `The IAM role for Lambda ARN (${TEST_LAMBDA_ARN}) should be ${TEST_REMOTE_IAM_ROLE}, \ +but found ${TEST_LOCAL_IAM_ROLE}. Please solve this IAM role mismatch and re-deploy again.`; + sinon.stub(LambdaClient.prototype, 'getFunction').callsArgWith(1, null, TEST_REMOTE_DEPLOY_STATE); + // call + helper.validateLambdaDeployState(REPORTER, TEST_AWS_PROFILE, TEST_AWS_REGION, TEST_LOCAL_DEPLOY_STATE, (err) => { + // verify + expect(err).equal(TEST_IAM_ROLE_ERROR); + done(); + }); + }); + + it('| an existing lambda arn found, getFunction request passes, the local revisionId does not equal with the remote revisionId,' + + 'expect an error return', (done) => { + // setup + const TEST_REMOTE_DEPLOY_STATE = { + Configuration: { + Role: TEST_LOCAL_IAM_ROLE, + RevisionId: TEST_REMOTE_REVISION_ID + } + }; + const TEST_REVISION_ID_ERROR = `The current revisionId (The revision ID for Lambda ARN (${TEST_LAMBDA_ARN}) should be \ +${TEST_REMOTE_REVISION_ID}, but found ${TEST_LOCAL_REVISION_ID}. Please solve this revision mismatch and re-deploy again.`; + sinon.stub(LambdaClient.prototype, 'getFunction').callsArgWith(1, null, TEST_REMOTE_DEPLOY_STATE); + // call + helper.validateLambdaDeployState(REPORTER, TEST_AWS_PROFILE, TEST_AWS_REGION, TEST_LOCAL_DEPLOY_STATE, (err) => { + // verify + expect(err).equal(TEST_REVISION_ID_ERROR); + done(); + }); + }); + + it('| an existing lambda arn found, getFunction request passes, expect updated deploy state return', (done) => { + // setup + const TEST_REMOTE_DEPLOY_STATE = { + Configuration: { + Role: TEST_LOCAL_IAM_ROLE, + RevisionId: TEST_LOCAL_REVISION_ID + } + }; + sinon.stub(LambdaClient.prototype, 'getFunction').callsArgWith(1, null, TEST_REMOTE_DEPLOY_STATE); + // call + helper.validateLambdaDeployState(REPORTER, TEST_AWS_PROFILE, TEST_AWS_REGION, TEST_LOCAL_DEPLOY_STATE, (err, data) => { + // verify + expect(data.updatedDeployState.iamRole).equal(TEST_LOCAL_IAM_ROLE); + expect(data.updatedDeployState.lambda.revisionId).equal(TEST_LOCAL_REVISION_ID); + done(); + }); + }); + }); + + describe('# test class method: deployIAMRole', () => { + const TEST_DEPLOYSTATE_NO_ROLE = { + }; + const TEST_DEPLOYSTATE = { + iamRole: TEST_IAM_ROLE_ARN + }; + + afterEach(() => { + sinon.restore(); + }); + + it('| an existing IAM role found, get IAM Role fails, expect an error return', (done) => { + // setup + const TEST_ERROR = 'getRole error message'; + sinon.stub(IAMClient.prototype, 'getIAMRole').callsArgWith(1, TEST_ERROR); + // call + helper.deployIAMRole(REPORTER, TEST_AWS_PROFILE, TEST_ALEXA_REGION, TEST_SKILL_NAME, TEST_AWS_REGION, TEST_DEPLOYSTATE, (err) => { + // verify + expect(err).equal(TEST_ERROR); + done(); + }); + }); + + it('| an existing IAM role found, get IAM Role throw "NoSuchEntity" error, expect an error return', (done) => { + // setup + const TEST_GET_ROLE_ERROR = `The IAM role is not found. Please check if your IAM role from region ${TEST_ALEXA_REGION} is valid.`; + sinon.stub(IAMClient.prototype, 'getIAMRole').callsArgWith(1, TEST_NO_SUCH_ENTITY_ERROR); + // call + helper.deployIAMRole(REPORTER, TEST_AWS_PROFILE, TEST_ALEXA_REGION, TEST_SKILL_NAME, TEST_AWS_REGION, TEST_DEPLOYSTATE, (err) => { + // verify + expect(err).equal(TEST_GET_ROLE_ERROR); + done(); + }); + }); + + it('| an existing IAM role found, get IAM Role passes, expect correct data return', (done) => { + // setup + sinon.stub(IAMClient.prototype, 'getIAMRole').callsArgWith(1, null, TEST_ROLE_DATA); + // call + helper.deployIAMRole(REPORTER, TEST_AWS_PROFILE, TEST_ALEXA_REGION, TEST_SKILL_NAME, TEST_AWS_REGION, TEST_DEPLOYSTATE, (err, res) => { + // verify + expect(res).equal(TEST_IAM_ROLE_ARN); + done(); + }); + }); + + it('| no IAM role found, create IAM role fails, expect an error return', (done) => { + // setup + const TEST_CREATE_ROLE_ERROR = 'createIAMRole error message'; + sinon.stub(IAMClient.prototype, 'createBasicLambdaRole').callsArgWith(1, TEST_CREATE_ROLE_ERROR); + // call + helper.deployIAMRole(REPORTER, TEST_AWS_PROFILE, TEST_ALEXA_REGION, TEST_SKILL_NAME, TEST_AWS_REGION, TEST_DEPLOYSTATE_NO_ROLE, (err) => { + // verify + expect(err).equal(TEST_CREATE_ROLE_ERROR); + done(); + }); + }); + + it('| no IAM role found, create IAM role passes, attach role policy fails, expect an error return', (done) => { + // setup + const TEST_POLICY_ERROR = 'attachRolePolicy error message'; + sinon.stub(IAMClient.prototype, 'createBasicLambdaRole').callsArgWith(1, null, TEST_ROLE_DATA); + sinon.stub(IAMClient.prototype, 'attachBasicLambdaRolePolicy').callsArgWith(1, TEST_POLICY_ERROR); + // call + helper.deployIAMRole(REPORTER, TEST_AWS_PROFILE, TEST_ALEXA_REGION, TEST_SKILL_NAME, TEST_AWS_REGION, TEST_DEPLOYSTATE_NO_ROLE, (err) => { + // verify + expect(err).equal(TEST_POLICY_ERROR); + done(); + }); + }); + + it('| no IAM role found, create IAM role and attach role policy passes, expect a IAM role arn return.', (done) => { + // setup + sinon.stub(IAMClient.prototype, 'createBasicLambdaRole').callsArgWith(1, null, TEST_ROLE_DATA); + sinon.stub(IAMClient.prototype, 'attachBasicLambdaRolePolicy').callsArgWith(1, null, TEST_IAM_ROLE_ARN); + // call + helper.deployIAMRole(REPORTER, TEST_AWS_PROFILE, TEST_ALEXA_REGION, TEST_SKILL_NAME, TEST_AWS_REGION, TEST_DEPLOYSTATE_NO_ROLE, + (err, res) => { + // verify + expect(res).equal(TEST_IAM_ROLE_ARN); + done(); + }); + }); + }); + + describe('# test class method: deployLambdaFunction', () => { + const TEST_ZIP_FILE_PATH = 'zip_file_path'; + const TEST_ZIP_FILE = 'zip_file'; + const TEST_CODE_BUILD = 'code_build_path'; + const TEST_LAST_MODIFIED = 'last_modified_time'; + const TEST_REVISION_ID = 'revision_id'; + const TEST_UPDATED_REVISION_ID = 'revision id'; + const TEST_FUNCTION_ARN = 'lambda function arn'; + const TEST_RUNTIME = 'runtime'; + const TEST_HANDLER = 'handler'; + const TEST_CREATE_OPTIONS = { + profile: TEST_PROFILE, + awsProfile: TEST_AWS_REGION, + alexaRegion: TEST_ALEXA_REGION, + awsRegion: TEST_AWS_REGION, + skillId: TEST_SKILL_ID, + skillName: TEST_SKILL_NAME, + code: { codeBuild: TEST_CODE_BUILD }, + iamRoleArn: TEST_IAM_ROLE_ARN, + zipFilePath: TEST_ZIP_FILE_PATH, + userConfig: { awsRegion: TEST_AWS_REGION }, + currentRegionDeployState: {} + }; + const TEST_UPDATE_OPTIONS = { + profile: TEST_PROFILE, + awsProfile: TEST_PROFILE, + alexaRegion: TEST_ALEXA_REGION, + awsRegion: TEST_AWS_REGION, + skillId: TEST_SKILL_ID, + skillName: TEST_SKILL_NAME, + code: { codeBuild: TEST_CODE_BUILD }, + iamRoleArn: TEST_IAM_ROLE_ARN, + zipFilePath: TEST_ZIP_FILE_PATH, + userConfig: { awsRegion: TEST_AWS_REGION, runtime: TEST_RUNTIME, handler: TEST_HANDLER }, + currentRegionDeployState: { lambda: { + arn: TEST_LAMBDA_ARN, + lastModified: TEST_LAST_MODIFIED, + revisionId: TEST_REVISION_ID + } } + }; + + beforeEach(() => { + new Manifest(FIXTURE_MANIFEST_FILE_PATH); + }); + + afterEach(() => { + Manifest.dispose(); + sinon.restore(); + }); + + it('| no Lambda found, create Lambda function fails, expect an error return.', (done) => { + // setup + const TEST_CREATE_FUNCTION_ERROR = 'createLambdaFunction error'; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'createLambdaFunction').callsArgWith(7, TEST_CREATE_FUNCTION_ERROR); + // call + helper.deployLambdaFunction(REPORTER, TEST_CREATE_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_CREATE_FUNCTION_ERROR); + done(); + }); + }); + + it('| no Lambda found, create Lambda function fails and get retry message, retry passes after one retry,' + + ' add Alexa Permission fails, expect an error return.', (done) => { + // setup + const RETRY_MESSAGE = 'The role defined for the function cannot be assumed by Lambda.'; + const TEST_CREATE_FUNCTION_ERROR = { + message: RETRY_MESSAGE + }; + const TEST_LAMBDA_DATA = { + FunctionArn: TEST_FUNCTION_ARN + }; + const TEST_ADD_PERMISSION_ERROR = 'addAlexaPermissionByDomain error'; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + const stubTestFunc = sinon.stub(LambdaClient.prototype, 'createLambdaFunction'); + stubTestFunc.onCall(0).callsArgWith(7, TEST_CREATE_FUNCTION_ERROR); + stubTestFunc.onCall(1).callsArgWith(7, null, TEST_LAMBDA_DATA); + sinon.stub(LambdaClient.prototype, 'addAlexaPermissionByDomain').callsArgWith(3, TEST_ADD_PERMISSION_ERROR); + // call + helper.deployLambdaFunction(REPORTER, TEST_CREATE_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_ADD_PERMISSION_ERROR); + done(); + }); + }).timeout(10000); + + it('| no Lambda found, create Lambda function passes, add Alexa Permission fails, expect an error return.', (done) => { + // setup + const TEST_LAMBDA_DATA = { + FunctionArn: TEST_FUNCTION_ARN + }; + const TEST_ADD_PERMISSION_ERROR = 'addAlexaPermissionByDomain error'; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'createLambdaFunction').callsArgWith(7, null, TEST_LAMBDA_DATA); + sinon.stub(LambdaClient.prototype, 'addAlexaPermissionByDomain').callsArgWith(3, TEST_ADD_PERMISSION_ERROR); + // call + helper.deployLambdaFunction(REPORTER, TEST_CREATE_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_ADD_PERMISSION_ERROR); + done(); + }); + }); + + it('| no Lambda found, create Lambda function and add Alexa Permission pass, get Function revisionId fails,' + + ' expect an error return.', (done) => { + // setup + const TEST_LAMBDA_DATA = { + FunctionArn: TEST_FUNCTION_ARN + }; + const TEST_REVISION_ID_ERROR = 'getFunctionRevisionId error'; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'createLambdaFunction').callsArgWith(7, null, TEST_LAMBDA_DATA); + sinon.stub(LambdaClient.prototype, 'addAlexaPermissionByDomain').callsArgWith(3, null); + sinon.stub(LambdaClient.prototype, 'getFunction').callsArgWith(1, TEST_REVISION_ID_ERROR); + + // call + helper.deployLambdaFunction(REPORTER, TEST_CREATE_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_REVISION_ID_ERROR); + done(); + }); + }); + + it('| no Lambda found, create Lambda function, add Alexa Permission and get Function revisionId pass, expect Lambda data return.', (done) => { + // setup + const TEST_LAMBDA_DATA = { + FunctionArn: TEST_FUNCTION_ARN + }; + const TEST_GET_FUNCTION_RESPONSE = { + Configuration: { + RevisionId: TEST_UPDATED_REVISION_ID + } + }; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'createLambdaFunction').callsArgWith(7, null, TEST_LAMBDA_DATA); + sinon.stub(LambdaClient.prototype, 'addAlexaPermissionByDomain').callsArgWith(3, null); + sinon.stub(LambdaClient.prototype, 'getFunction').callsArgWith(1, null, TEST_GET_FUNCTION_RESPONSE); + + // call + helper.deployLambdaFunction(REPORTER, TEST_CREATE_OPTIONS, (err, res) => { + // verify + expect(res.arn).equal(TEST_FUNCTION_ARN); + expect(res.revisionId).equal(TEST_UPDATED_REVISION_ID); + done(); + }); + }); + + it('| an existing Lambda found, update function code fails, expect an error return.', (done) => { + // setup + const TEST_UPDATE_CODE_ERROR = 'updateFunctionCode error'; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'updateFunctionCode').callsArgWith(3, TEST_UPDATE_CODE_ERROR); + // call + helper.deployLambdaFunction(REPORTER, TEST_UPDATE_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_UPDATE_CODE_ERROR); + done(); + }); + }); + + it('| an existing Lambda found, update function code passes, update function configuration fails, expect an error return.', (done) => { + // setup + const TEST_UPDATE_CONGIF_ERROR = 'updateFunctionConfiguration error'; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'updateFunctionCode').callsArgWith(3, null, { RevisionId: TEST_REVISION_ID }); + sinon.stub(LambdaClient.prototype, 'updateFunctionConfiguration').callsArgWith(4, TEST_UPDATE_CONGIF_ERROR); + // call + helper.deployLambdaFunction(REPORTER, TEST_UPDATE_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_UPDATE_CONGIF_ERROR); + done(); + }); + }); + + it('| an existing Lambda found, update function code and update function configuration pass, expect updated lambda data return.', (done) => { + // setup + const TEST_UPDATE_CONFIG_DATA = { + FunctionArn: TEST_LAMBDA_ARN, + LastModified: TEST_LAST_MODIFIED, + RevisionId: TEST_REVISION_ID + }; + sinon.stub(fs, 'readFileSync').withArgs(TEST_ZIP_FILE_PATH).returns(TEST_ZIP_FILE); + sinon.stub(aws, 'Lambda'); + sinon.stub(LambdaClient.prototype, 'updateFunctionCode').callsArgWith(3, null, { RevisionId: TEST_REVISION_ID }); + sinon.stub(LambdaClient.prototype, 'updateFunctionConfiguration').callsArgWith(4, null, TEST_UPDATE_CONFIG_DATA); + // call + helper.deployLambdaFunction(REPORTER, TEST_UPDATE_OPTIONS, (err, data) => { + // verify + expect(data.arn).equal(TEST_LAMBDA_ARN); + expect(data.lastModified).equal(TEST_LAST_MODIFIED); + expect(data.revisionId).equal(TEST_REVISION_ID); + done(); + }); + }); + }); +}); diff --git a/test/unit/builtins/lambda-deployer/index-test.js b/test/unit/builtins/lambda-deployer/index-test.js new file mode 100644 index 00000000..41954e66 --- /dev/null +++ b/test/unit/builtins/lambda-deployer/index-test.js @@ -0,0 +1,171 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const awsUtil = require('@src/clients/aws-client/aws-util'); +const CONSTANTS = require('@src/utils/constants'); +const helper = require('@src/builtins/deploy-delegates/lambda-deployer/helper'); +const lambdaDeployer = require('@src/builtins/deploy-delegates/lambda-deployer/index'); + + +describe('Builtins test - lambda-deployer index.js test', () => { + const TEST_PROFILE = 'default'; // test file uses 'default' profile + const TEST_ALEXA_REGION = 'default'; + const TEST_SKILL_NAME = 'skill_name'; + + describe('# test class method: bootstrap', () => { + afterEach(() => { + sinon.restore(); + }); + + it('| awsProfile and awsDefaultRegion are set, expect config is updated correctly', (done) => { + // set up + const TEST_OPTIONS = { + profile: TEST_PROFILE, + userConfig: {} + }; + sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); + sinon.stub(awsUtil, 'getCLICompatibleDefaultRegion').withArgs(TEST_PROFILE).returns(CONSTANTS.AWS_SKILL_INFRASTRUCTURE_DEFAULT_REGION); + // call + lambdaDeployer.bootstrap(TEST_OPTIONS, (err, res) => { + // verify + expect(err).equal(null); + expect(res).deep.equal({ + userConfig: { awsRegion: CONSTANTS.AWS_SKILL_INFRASTRUCTURE_DEFAULT_REGION } + }); + done(); + }); + }); + }); + + describe('# test case method: invoke', () => { + const REPORTER = {}; + const NULL_PROFILE = null; + const TEST_OPTIONS = { + profile: TEST_PROFILE, + alexaRegion: TEST_ALEXA_REGION, + skillId: '', + skillName: TEST_SKILL_NAME, + code: {}, + userConfig: {}, + deployState: {} + }; + const TEST_VALIDATED_DEPLOY_STATE = { + updatedDeployState: {} + }; + + afterEach(() => { + sinon.restore(); + }); + + it('| profile is not set, expect an error return', (done) => { + // setup + const TEST_OPTIONS_WITHOUT_PROFILE = { + profile: NULL_PROFILE, + }; + const TEST_ERROR = `Profile [${NULL_PROFILE}] doesn't have AWS profile linked to it. Please run "ask init" to re-configure your porfile.`; + sinon.stub(awsUtil, 'getAWSProfile').withArgs(NULL_PROFILE).returns(NULL_PROFILE); + // call + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_PROFILE, (err) => { + // verify + expect(err).equal(TEST_ERROR); + done(); + }); + }); + + it('| profile is set, awsRegion is blank, expect an error return', (done) => { + // setup + const TEST_OPTIONS_WITHOUT_REGION = { + profile: TEST_PROFILE, + alexaRegion: null, + skillId: '', + skillName: TEST_SKILL_NAME, + code: {}, + userConfig: {}, + deployState: {} + }; + 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); + // call + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_REGION, (err) => { + // verify + expect(err).equal(TEST_ERROR); + done(); + }); + }); + + it('| profile is set, validate Lambda deploy state fails, expect an error return', (done) => { + // setup + const TEST_ERROR = 'validateLambdaDeployState error message'; + sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); + sinon.stub(helper, 'validateLambdaDeployState').callsArgWith(4, TEST_ERROR); + // call + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_ERROR); + done(); + }); + }); + + it('| profile is set, validate Lambda deploy state passes, deploy IAM role fails, expect an error return', (done) => { + // setup + const TEST_ERROR = 'IAMRole error message'; + sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); + sinon.stub(helper, 'validateLambdaDeployState').callsArgWith(4, null, TEST_VALIDATED_DEPLOY_STATE); + sinon.stub(helper, 'deployIAMRole').callsArgWith(6, TEST_ERROR); + // call + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err) => { + // verify + expect(err).equal(TEST_ERROR); + done(); + }); + }); + + it('| deploy IAM role passes, deploy Lambda Function fails, expect IAM role arn and an error message return', (done) => { + // setup + const IAM_ROLE_ARN = 'IAM role arn'; + 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, 'validateLambdaDeployState').callsArgWith(4, null, TEST_VALIDATED_DEPLOY_STATE); + sinon.stub(helper, 'deployIAMRole').callsArgWith(6, null, IAM_ROLE_ARN); + sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, TEST_ERROR); + // call + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, data) => { + // verify + expect(data.reasons).to.be.an.instanceOf(Array); + expect(data.reasons.length).equal(1); + expect(data.message).equal(TEST_ERROR_MESSAGE_RESPONSE); + expect(data.deployState.iamRole).equal(IAM_ROLE_ARN); + done(); + }); + }); + + it('| deploy IAM role and Lambda Function pass, expect correct data return', (done) => { + // setup + const IAM_ROLE_ARN = 'iam_role_arn'; + const LAMBDA_ARN = 'lambda_arn'; + const LAST_MODIFIED = 'last_modified'; + const REVISIONID = '1'; + const TEST_SUCCESS_RESPONSE = `The lambda deploy succeeded for Alexa region "${TEST_ALEXA_REGION}" \ +with output Lambda ARN: ${LAMBDA_ARN}.`; + const LAMDBA_RESUALT = { + arn: LAMBDA_ARN, + lastModified: LAST_MODIFIED, + revisionId: REVISIONID + }; + sinon.stub(awsUtil, 'getAWSProfile').withArgs(TEST_PROFILE).returns(TEST_PROFILE); + sinon.stub(helper, 'validateLambdaDeployState').callsArgWith(4, null, TEST_VALIDATED_DEPLOY_STATE); + sinon.stub(helper, 'deployIAMRole').callsArgWith(6, null, IAM_ROLE_ARN); + sinon.stub(helper, 'deployLambdaFunction').callsArgWith(2, null, LAMDBA_RESUALT); + // call + lambdaDeployer.invoke(REPORTER, TEST_OPTIONS, (err, res) => { + // verify + expect(res.endpoint.uri).equal(LAMBDA_ARN); + expect(res.deployState.iamRole).equal(IAM_ROLE_ARN); + expect(res.message).equal(TEST_SUCCESS_RESPONSE); + expect(err).equal(null); + done(); + }); + }); + }); +}); diff --git a/test/unit/clients/aws-client/iam-client-test.js b/test/unit/clients/aws-client/iam-client-test.js new file mode 100644 index 00000000..2ce6aa57 --- /dev/null +++ b/test/unit/clients/aws-client/iam-client-test.js @@ -0,0 +1,146 @@ +const aws = require('aws-sdk'); +const { expect } = require('chai'); +const sinon = require('sinon'); + +const IAMClient = require('@src/clients/aws-client/iam-client'); + +describe('Clients test - iam client test', () => { + const TEST_AWS_PROFILE = 'TEST_AWS_PROFILE'; + const TEST_AWS_REGION = 'TEST_AWS_REGION'; + const TEST_CONFIGURATION = { + awsProfile: TEST_AWS_PROFILE, + awsRegion: TEST_AWS_REGION + }; + const TEST_ROLE_ARN = 'iam_role_arn'; + const TEST_SKILL_NAME = 'skill_name'; + + afterEach(() => { + sinon.restore(); + }); + + describe('# constructor tests', () => { + it('| inspect correctness for constructor when awsRegion is set in configuration.', () => { + const iamClient = new IAMClient(TEST_CONFIGURATION); + expect(iamClient).to.be.instanceOf(IAMClient); + expect(iamClient.awsRegion).equal(TEST_AWS_REGION); + expect(aws.config.region).equal(TEST_AWS_REGION); + expect(aws.config.credentials).deep.equal(new aws.SharedIniFileCredentials({ profile: TEST_AWS_PROFILE })); + }); + + it('| inspect an error for constructor when awsRegion is null in configuration.', () => { + const configuration = { + awsProfile: TEST_AWS_PROFILE, + awsRegion: null + }; + try { + new IAMClient(configuration); + } catch (e) { + expect(e.message).equal('Invalid awsProfile or Invalid awsRegion'); + } + }); + it('| inspect an error for constructor when awsRegion is blank in configuration.', () => { + const configuration = { + awsProfile: ' ', + awsRegion: TEST_AWS_REGION + }; + try { + new IAMClient(configuration); + } catch (e) { + expect(e.message).equal('Invalid awsProfile or Invalid awsRegion'); + } + }); + }); + + describe('# function getIAMRole tests', () => { + it('| iamClient get IAM role request fails, expect an error return.', (done) => { + // setup + const iamClient = new IAMClient(TEST_CONFIGURATION); + const TEST_GET_ROLE_ERR = 'GET_ROLE_ERROR'; + sinon.stub(iamClient.client, 'getRole').callsArgWith(1, TEST_GET_ROLE_ERR); + // call + iamClient.getIAMRole(TEST_ROLE_ARN, (err) => { + // verify + expect(err).equal(TEST_GET_ROLE_ERR); + done(); + }); + }); + + it('| iamClient get IAM role request passes, expect role data return.', (done) => { + // setup + const iamClient = new IAMClient(TEST_CONFIGURATION); + const TEST_GET_ROLE_RESPONSE = { + Role: { + Arn: TEST_ROLE_ARN + } + }; + sinon.stub(iamClient.client, 'getRole').callsArgWith(1, null, TEST_GET_ROLE_RESPONSE); + // call + iamClient.getIAMRole(TEST_ROLE_ARN, (err, data) => { + // verify + expect(data.Role.Arn).equal(TEST_ROLE_ARN); + expect(err).equal(null); + done(); + }); + }); + }); + + describe('# function createBasicLambdaRole tests', () => { + it('| iamClient create basic Lambda role request fails, expect an error return.', (done) => { + // setup + const iamClient = new IAMClient(TEST_CONFIGURATION); + const TEST_CREATE_ROLE_ERR = 'CREATE_ROLE_ERROR'; + sinon.stub(iamClient.client, 'createRole').callsArgWith(1, TEST_CREATE_ROLE_ERR); + // call + iamClient.createBasicLambdaRole(TEST_SKILL_NAME, (err) => { + // verify + expect(err).equal(TEST_CREATE_ROLE_ERR); + done(); + }); + }); + + it('| iamClient create basic Lambda role request passes, expect role data return.', (done) => { + // setup + const iamClient = new IAMClient(TEST_CONFIGURATION); + const TEST_CREATE_ROLE_RESPONSE = { + Role: { + Arn: TEST_ROLE_ARN + } + }; + sinon.stub(iamClient.client, 'createRole').callsArgWith(1, null, TEST_CREATE_ROLE_RESPONSE); + // call + iamClient.createBasicLambdaRole(TEST_SKILL_NAME, (err, data) => { + // verify + expect(data.Role.Arn).equal(TEST_ROLE_ARN); + expect(err).equal(null); + done(); + }); + }); + }); + + describe('# function attachBasicLambdaRolePolicy tests', () => { + it('| iamClient attach basic Lambda role policy request fails, expect an error return.', (done) => { + // setup + const iamClient = new IAMClient(TEST_CONFIGURATION); + const TEST_ATTACH_POLICY_ERR = 'ATTACH_POLICY_ERROR'; + sinon.stub(iamClient.client, 'attachRolePolicy').callsArgWith(1, TEST_ATTACH_POLICY_ERR); + // call + iamClient.attachBasicLambdaRolePolicy(TEST_ROLE_ARN, (err) => { + // verify + expect(err).equal(TEST_ATTACH_POLICY_ERR); + done(); + }); + }); + + it('| iamClient attach basic Lambda role policy request passes, expect null error return.', (done) => { + // setup + const iamClient = new IAMClient(TEST_CONFIGURATION); + sinon.stub(iamClient.client, 'attachRolePolicy').callsArgWith(1, null); + // call + iamClient.attachBasicLambdaRolePolicy(TEST_ROLE_ARN, (err) => { + // verify + expect(err).equal(null); + done(); + }); + }); + }); +}); diff --git a/test/unit/clients/aws-client/lambda-client-test.js b/test/unit/clients/aws-client/lambda-client-test.js new file mode 100644 index 00000000..f48117ce --- /dev/null +++ b/test/unit/clients/aws-client/lambda-client-test.js @@ -0,0 +1,228 @@ +const aws = require('aws-sdk'); +const { expect } = require('chai'); +const sinon = require('sinon'); + +const LambdaClient = require('@src/clients/aws-client/lambda-client'); + +describe('Clients test - lambda client test', () => { + const TEST_AWS_PROFILE = 'TEST_AWS_PROFILE'; + const TEST_AWS_REGION = 'TEST_AWS_REGION'; + const TEST_ALEXA_REGION = 'TEST_ALEXA_REGION'; + const TEST_CONFIGURATION = { + awsProfile: TEST_AWS_PROFILE, + awsRegion: TEST_AWS_REGION + }; + const TEST_SKILL_NAME = 'skill_name'; + const TEST_FUNCTION_ARN = 'function_arn'; + const TEST_SKILL_ID = 'skill_id'; + const TEST_REVISION_ID = 'revision_id'; + const TEST_ZIPFILD = 'zipfile_path'; + const TEST_RUNTIME = 'runtime'; + const TEST_HANDLE = 'handler'; + + afterEach(() => { + sinon.restore(); + }); + + describe('# constructor tests', () => { + it('| inspect correctness for constructor when awsRegion is set in configuration.', () => { + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + expect(lambdaClient).to.be.instanceOf(LambdaClient); + expect(lambdaClient.awsRegion).equal(TEST_AWS_REGION); + expect(aws.config.region).equal(TEST_AWS_REGION); + expect(aws.config.credentials).deep.equal(new aws.SharedIniFileCredentials({ profile: TEST_AWS_PROFILE })); + }); + + it('| inspect an error for constructor when awsRegion is null in configuration.', () => { + const configuration = { + awsProfile: TEST_AWS_PROFILE, + awsRegion: null + }; + try { + new LambdaClient(configuration); + } catch (e) { + expect(e.message).equal('Invalid awsProfile or Invalid awsRegion'); + } + }); + it('| inspect an error for constructor when awsRegion is blank in configuration.', () => { + const configuration = { + awsProfile: ' ', + awsRegion: TEST_AWS_REGION + }; + try { + new LambdaClient(configuration); + } catch (e) { + expect(e.message).equal('Invalid awsProfile or Invalid awsRegion'); + } + }); + }); + + describe('# function createLambdaFunction tests', () => { + const TEST_IAM_ROLE = 'iam_role'; + + it('| iamClient create Lambda function request fails, expect an error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_CREATE_FUNCTION_ERR = 'create_function_error'; + sinon.stub(lambdaClient.client, 'createFunction').callsArgWith(1, TEST_CREATE_FUNCTION_ERR); + // call + lambdaClient.createLambdaFunction(TEST_SKILL_NAME, TEST_AWS_PROFILE, TEST_ALEXA_REGION, + TEST_IAM_ROLE, TEST_ZIPFILD, TEST_RUNTIME, TEST_HANDLE, (err) => { + // verify + expect(err).equal(TEST_CREATE_FUNCTION_ERR); + done(); + }); + }); + + it('| iamClient create Lambda function request passes, expect role data return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_CREATE_FUNCTION_RESPONSE = { + FunctionArn: TEST_FUNCTION_ARN + }; + sinon.stub(lambdaClient.client, 'createFunction').callsArgWith(1, null, TEST_CREATE_FUNCTION_RESPONSE); + // call + lambdaClient.createLambdaFunction(TEST_SKILL_NAME, TEST_AWS_PROFILE, TEST_ALEXA_REGION, + TEST_IAM_ROLE, TEST_ZIPFILD, TEST_RUNTIME, TEST_HANDLE, (err, data) => { + // verify + expect(data.FunctionArn).equal(TEST_FUNCTION_ARN); + done(); + }); + }); + }); + + describe('# function addAlexaPermissionByDomain tests', () => { + it('| iamClient add Alexa permission by custom domain request fails, expect an error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_DOMAIN = 'custom'; + const TEST_ADD_PERMISSION_ERR = 'ADD_PERMISSION_ERROR'; + sinon.stub(lambdaClient.client, 'addPermission').callsArgWith(1, TEST_ADD_PERMISSION_ERR); + // call + lambdaClient.addAlexaPermissionByDomain(TEST_DOMAIN, TEST_SKILL_ID, TEST_FUNCTION_ARN, (err) => { + // verify + expect(err).equal(TEST_ADD_PERMISSION_ERR); + done(); + }); + }); + + it('| iamClient add Alexa permission by smartHome domain request fails, expect an error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_DOMAIN = 'smartHome'; + const TEST_ADD_PERMISSION_ERR = 'ADD_PERMISSION_ERROR'; + sinon.stub(lambdaClient.client, 'addPermission').callsArgWith(1, TEST_ADD_PERMISSION_ERR); + // call + lambdaClient.addAlexaPermissionByDomain(TEST_DOMAIN, TEST_SKILL_ID, TEST_FUNCTION_ARN, (err) => { + // verify + expect(err).equal(TEST_ADD_PERMISSION_ERR); + done(); + }); + }); + + it('| iamClient add Alexa permission by domain request passes, expect null error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_DOMAIN = 'video'; + sinon.stub(lambdaClient.client, 'addPermission').callsArgWith(1, null); + // call + lambdaClient.addAlexaPermissionByDomain(TEST_DOMAIN, TEST_SKILL_ID, TEST_FUNCTION_ARN, (err) => { + // verify + expect(err).equal(null); + done(); + }); + }); + }); + + describe('# function getFunction tests', () => { + it('| iamClient get Function request fails, expect an error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_GET_FUNCTION_ERR = 'GET_FUNCTION_ERROR'; + sinon.stub(lambdaClient.client, 'getFunction').callsArgWith(1, TEST_GET_FUNCTION_ERR); + // call + lambdaClient.getFunction(TEST_FUNCTION_ARN, (err) => { + // verify + expect(err).equal(TEST_GET_FUNCTION_ERR); + done(); + }); + }); + + it('| iamClient get Function request passes, expect a revision id return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_GET_FUNCTION_RESPONSE = { + Configuration: { + RevisionId: TEST_REVISION_ID + } + }; + sinon.stub(lambdaClient.client, 'getFunction').callsArgWith(1, null, TEST_GET_FUNCTION_RESPONSE); + // call + lambdaClient.getFunction(TEST_FUNCTION_ARN, (err, data) => { + // verify + expect(data.Configuration.RevisionId).equal(TEST_REVISION_ID); + done(); + }); + }); + }); + + describe('# function updateFunctionCode tests', () => { + it('| iamClient update function code request fails, expect an error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_UPDATE_CODE_ERR = 'UPDATE_CODE_ERROR'; + sinon.stub(lambdaClient.client, 'updateFunctionCode').callsArgWith(1, TEST_UPDATE_CODE_ERR); + // call + lambdaClient.updateFunctionCode(TEST_ZIPFILD, TEST_FUNCTION_ARN, TEST_REVISION_ID, (err) => { + // verify + expect(err).equal(TEST_UPDATE_CODE_ERR); + done(); + }); + }); + + it('| iamClient update function code request passes, expect function data return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_UPDATE_CODE_RESPONSE = { + FunctionArn: TEST_FUNCTION_ARN + }; + sinon.stub(lambdaClient.client, 'updateFunctionCode').callsArgWith(1, null, TEST_UPDATE_CODE_RESPONSE); + // call + lambdaClient.updateFunctionCode(TEST_ZIPFILD, TEST_FUNCTION_ARN, TEST_REVISION_ID, (err, data) => { + // verify + expect(data.FunctionArn).equal(TEST_FUNCTION_ARN); + done(); + }); + }); + }); + + describe('# function update function configuration tests', () => { + it('| iamClient update function configuration request fails, expect an error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_UPDATE_CONFIG_ERR = 'UPDATE_CONFIG_ERROR'; + sinon.stub(lambdaClient.client, 'updateFunctionConfiguration').callsArgWith(1, TEST_UPDATE_CONFIG_ERR); + // call + lambdaClient.updateFunctionConfiguration(TEST_FUNCTION_ARN, TEST_RUNTIME, TEST_HANDLE, TEST_REVISION_ID, (err) => { + // verify + expect(err).equal(TEST_UPDATE_CONFIG_ERR); + done(); + }); + }); + + it('| iamClient update function configuration request passes, expect null error return.', (done) => { + // setup + const lambdaClient = new LambdaClient(TEST_CONFIGURATION); + const TEST_UPDATE_CONFIG_RESPONSE = { + RevisionId: TEST_REVISION_ID + }; + sinon.stub(lambdaClient.client, 'updateFunctionConfiguration').callsArgWith(1, null, TEST_UPDATE_CONFIG_RESPONSE); + // call + lambdaClient.updateFunctionConfiguration(TEST_FUNCTION_ARN, TEST_RUNTIME, TEST_HANDLE, TEST_REVISION_ID, (err, data) => { + // verify + expect(data.RevisionId).equal(TEST_REVISION_ID); + done(); + }); + }); + }); +}); diff --git a/test/unit/run-test.js b/test/unit/run-test.js index 0c18738d..5ec0ddd7 100644 --- a/test/unit/run-test.js +++ b/test/unit/run-test.js @@ -7,6 +7,9 @@ require('module-alias/register'); */ [ // UNIT TEST + // builtins + '@test/unit/builtins/lambda-deployer/index-test.js', + '@test/unit/builtins/lambda-deployer/helper-test.js', // commands '@test/unit/commands/option-validator-test', '@test/unit/commands/abstract-command-test', @@ -33,6 +36,8 @@ require('module-alias/register'); '@test/unit/clients/aws-client/s3-client-test', '@test/unit/clients/aws-client/cloudformation-client-test', '@test/unit/clients/aws-client/aws-util-test', + '@test/unit/clients/aws-client/iam-client-test', + '@test/unit/clients/aws-client/lambda-client-test', // model '@test/unit/model/abstract-config-file-test', '@test/unit/model/app-config-test',