diff --git a/index.js b/index.js deleted file mode 100644 index 18e684fd..00000000 --- a/index.js +++ /dev/null @@ -1,516 +0,0 @@ -'use strict'; -const BbPromise = require('bluebird'); -const path = require('path'); -const _ = require('lodash'); -const chalk = require('chalk'); - -class ServerlessStepFunctions { - constructor(serverless, options) { - this.serverless = serverless; - this.options = options || {}; - this.provider = this.serverless.getProvider('aws'); - this.service = this.serverless.service.service; - this.region = this.provider.getRegion(); - this.stage = this.provider.getStage(); - this.awsStateLanguage = {}; - this.functionArns = {}; - this.iamPolicyStatement = `{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "lambda:InvokeFunction" - ], - "Resource": "*" - } - ] - } - `; - - this.assumeRolePolicyDocument = `{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "states.${this.region}.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] - } - `; - - this.commands = { - deploy: { - commands: { - stepf: { - usage: 'Deploy the State Machine of Step functions', - lifecycleEvents: [ - 'deploy', - ], - options: { - state: { - usage: 'Name of the State Machine', - shortcut: 't', - required: true, - }, - stage: { - usage: 'Stage of the service', - shortcut: 's', - }, - region: { - usage: 'Region of the service', - shortcut: 'r', - }, - }, - }, - tasks: { - usage: 'Deploy the Tasks of Step functions', - lifecycleEvents: [ - 'deploy', - ], - options: { - state: { - usage: 'Name of the Tasks', - shortcut: 't', - required: true, - }, - stage: { - usage: 'Stage of the service', - shortcut: 's', - }, - region: { - usage: 'Region of the service', - shortcut: 'r', - }, - }, - }, - }, - }, - remove: { - commands: { - stepf: { - usage: 'Remove Step functions', - lifecycleEvents: [ - 'remove', - ], - options: { - state: { - usage: 'Name of the State Machine', - shortcut: 't', - required: true, - }, - stage: { - usage: 'Stage of the service', - shortcut: 's', - }, - region: { - usage: 'Region of the service', - shortcut: 'r', - }, - }, - }, - tasks: { - usage: 'Remove the Tasks of Step functions', - lifecycleEvents: [ - 'deploy', - ], - options: { - state: { - usage: 'Name of the Tasks', - shortcut: 't', - required: true, - }, - stage: { - usage: 'Stage of the service', - shortcut: 's', - }, - region: { - usage: 'Region of the service', - shortcut: 'r', - }, - }, - }, - }, - }, - invoke: { - commands: { - stepf: { - usage: 'Invoke Step functions', - lifecycleEvents: [ - 'invoke', - ], - options: { - state: { - usage: 'Name of the State Machine', - shortcut: 't', - required: true, - }, - data: { - usage: 'String data to be passed as an event to your step function', - shortcut: 'd', - }, - path: { - usage: - 'The path to a json file with input data to be passed to the invoked step function', - shortcut: 'p', - }, - stage: { - usage: 'Stage of the service', - shortcut: 's', - }, - region: { - usage: 'Region of the service', - shortcut: 'r', - }, - }, - }, - }, - }, - }; - - this.hooks = { - 'deploy:stepf:deploy': () => BbPromise.bind(this) - .then(this.stateMachineDeploy), - 'remove:stepf:remove': () => BbPromise.bind(this) - .then(this.stateMachineRemove), - 'invoke:stepf:invoke': () => BbPromise.bind(this) - .then(this.stateMachineInvoke), - 'deploy:tasks:deploy': () => BbPromise.bind(this) - .then(this.tasksDeploy), - 'remove:tasks:remove': () => BbPromise.bind(this) - .then(this.tasksRemove), - }; - } - - stateMachineDeploy() { - this.serverless.cli.log(`Start to deploy ${this.options.state} step function...`); - return BbPromise.bind(this) - .then(this.yamlParse) - .then(this.getStateMachineArn) - .then(this.getFunctionArns) - .then(this.compile) - .then(this.getIamRole) - .then(this.deleteStateMachine) - .then(this.createStateMachine); - } - - stateMachineRemove() { - return BbPromise.bind(this) - .then(this.deleteIamRole) - .then(this.getStateMachineArn) - .then(this.deleteStateMachine) - .then(() => { - this.serverless.cli.log(`Remove ${this.options.state}`); - return BbPromise.resolve(); - }); - } - - stateMachineInvoke() { - return BbPromise.bind(this) - .then(this.parseInputdate) - .then(this.getStateMachineArn) - .then(this.startExecution) - .then(this.describeExecution); - } - - tasksDeploy() { - // todo - } - - tasksRemove() { - // todo - } - - getIamRoleName() { - let name = `${this.service}-${this.region}-${this.stage}-${this.options.state}-`; - name += 'ssf-exerole'; - return name.substr(0, 64); - } - - getIamPolicyName() { - let name = `${this.service}-${this.region}-${this.stage}-${this.options.state}-`; - name += 'ssf-exepolicy'; - return name.substr(0, 64); - } - - getStateMachineName() { - return `${this.service}-${this.stage}-${this.options.state}`; - } - - getIamRole() { - return this.provider.request('IAM', - 'getRole', - { - RoleName: this.getIamRoleName(), - }, - this.options.stage, - this.options.region) - .then((result) => { - this.iamRoleArn = result.Role.Arn; - return BbPromise.resolve(); - }).catch((error) => { - if (error.statusCode === 404) { - return this.createIamRole(); - } - throw new this.serverless.classes.Error(error.message); - }); - } - - getFunctionArns() { - return this.provider.request('STS', - 'getCallerIdentity', - {}, - this.options.stage, - this.options.region) - .then((result) => { - _.forEach(this.serverless.service.functions, (value, key) => { - this.functionArns[key] - = `arn:aws:lambda:${this.region}:${result.Account}:function:${value.name}`; - }); - return BbPromise.resolve(); - }); - } - - createIamRole() { - return this.provider.request('IAM', - 'createRole', - { - AssumeRolePolicyDocument: this.assumeRolePolicyDocument, - RoleName: this.getIamRoleName(), - }, - this.options.stage, - this.options.region) - .then((result) => { - this.iamRoleArn = result.Role.Arn; - return this.provider.request('IAM', - 'createPolicy', - { - PolicyDocument: this.iamPolicyStatement, - PolicyName: this.getIamPolicyName(), - }, - this.options.stage, - this.options.region); - }) - .then((result) => this.provider.request('IAM', - 'attachRolePolicy', - { - PolicyArn: result.Policy.Arn, - RoleName: this.getIamRoleName(), - }, - this.options.stage, - this.options.region) - ) - .then(() => BbPromise.resolve()); - } - - deleteIamRole() { - let policyArn; - return this.provider.request('STS', - 'getCallerIdentity', - {}, - this.options.stage, - this.options.region) - .then((result) => { - policyArn = `arn:aws:iam::${result.Account}:policy/${this.getIamPolicyName()}`; - - return this.provider.request('IAM', - 'detachRolePolicy', - { - PolicyArn: policyArn, - RoleName: this.getIamRoleName(), - }, - this.options.stage, - this.options.region); - }) - .then(() => this.provider.request('IAM', - 'deletePolicy', - { - PolicyArn: policyArn, - }, - this.options.stage, - this.options.region) - ) - .then(() => this.provider.request('IAM', - 'deleteRole', - { - RoleName: this.getIamRoleName(), - }, - this.options.stage, - this.options.region) - ) - .then(() => BbPromise.resolve()); - } - - getStateMachineArn() { - return this.provider.request('STS', - 'getCallerIdentity', - {}, - this.options.stage, - this.options.region) - .then((result) => { - this.stateMachineArn = - `arn:aws:states:${this.region}:${result.Account}:stateMachine:${this.getStateMachineName()}`; - return BbPromise.resolve(); - }); - } - - deleteStateMachine() { - return this.provider.request('StepFunctions', - 'deleteStateMachine', - { - stateMachineArn: this.stateMachineArn, - }, - this.options.stage, - this.options.region) - .then(() => BbPromise.resolve()); - } - - createStateMachine() { - return this.provider.request('StepFunctions', - 'createStateMachine', - { - definition: this.awsStateLanguage[this.options.state], - name: this.getStateMachineName(), - roleArn: this.iamRoleArn, - }, - this.options.stage, - this.options.region) - .then(() => { - this.serverless.cli.consoleLog(''); - this.serverless.cli.log(`Finish to deploy ${this.getStateMachineName()} step function`); - return BbPromise.resolve(); - }).catch((error) => { - if (error.message.match(/State Machine is being deleted/)) { - this.serverless.cli.printDot(); - setTimeout(this.createStateMachine.bind(this), 5000); - } else { - throw new this.serverless.classes.Error(error.message); - } - }); - } - - parseInputdate() { - if (!this.options.data && this.options.path) { - const absolutePath = path.isAbsolute(this.options.path) ? - this.options.path : - path.join(this.serverless.config.servicePath, this.options.path); - if (!this.serverless.utils.fileExistsSync(absolutePath)) { - throw new this.serverless.classes.Error('The file you provided does not exist.'); - } - this.options.data = JSON.stringify(this.serverless.utils.readFileSync(absolutePath)); - } - return BbPromise.resolve(); - } - - startExecution() { - this.serverless.cli.log(`Start function ${this.options.state}...`); - return this.provider.request('StepFunctions', - 'startExecution', - { - stateMachineArn: this.stateMachineArn, - input: this.options.data, - }, - this.options.stage, - this.options.region) - .then((result) => { - this.executionArn = result.executionArn; - return BbPromise.resolve(); - }).catch((error) => { - throw new this.serverless.classes.Error(error.message); - }); - } - - describeExecution() { - return this.provider.request('StepFunctions', - 'describeExecution', - { - executionArn: this.executionArn, - }, - this.options.stage, - this.options.region) - .then((result) => { - if (result.status === 'RUNNING') { - this.serverless.cli.printDot(); - setTimeout(this.describeExecution.bind(this), 5000); - } else { - this.serverless.cli.consoleLog(''); - this.serverless.cli.consoleLog(''); - const msg = 'Execution Result -----------------------------------------'; - this.serverless.cli.consoleLog(chalk.yellow(msg)); - this.serverless.cli.consoleLog(''); - this.serverless.cli.consoleLog(result); - - if (result.status === 'FAILED') { - return this.getExecutionHistory(); - } - } - return BbPromise.resolve(); - }); - } - - getExecutionHistory() { - return this.provider.request('StepFunctions', - 'getExecutionHistory', - { - executionArn: this.executionArn, - }, - this.options.stage, - this.options.region) - .then((result) => { - this.serverless.cli.consoleLog(''); - const msg = 'Error Log ------------------------------------------------'; - this.serverless.cli.consoleLog(chalk.yellow(msg)); - this.serverless.cli.consoleLog(''); - this.serverless.cli.consoleLog(result.events[result.events.length - 1] - .executionFailedEventDetails); - return BbPromise.resolve(); - }); - } - - yamlParse() { - const servicePath = this.serverless.config.servicePath; - - if (!servicePath) { - return BbPromise.resolve(); - } - - const serverlessYmlPath = path.join(servicePath, 'serverless.yml'); - return this.serverless.yamlParser - .parse(serverlessYmlPath) - .then((serverlessFileParam) => { - this.serverless.service.stepFunctions = serverlessFileParam.stepFunctions.stateMachine; - this.serverless.variables.populateService(this.serverless.pluginManager.cliOptions); - return BbPromise.resolve(); - }); - } - - compile() { - if (!this.serverless.service.stepFunctions) { - const errorMessage = [ - 'stepFunctions statement does not exists in serverless.yml', - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - - if (typeof this.serverless.service.stepFunctions[this.options.state] === 'undefined') { - const errorMessage = [ - `Step function "${this.options.state}" is not exists`, - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - - this.awsStateLanguage[this.options.state] = - JSON.stringify(this.serverless.service.stepFunctions[this.options.state]); - - _.forEach(this.functionArns, (value, key) => { - const regExp = new RegExp(`"Resource":"${key}"`, 'g'); - this.awsStateLanguage[this.options.state] = - this.awsStateLanguage[this.options.state].replace(regExp, `"Resource":"${value}"`); - }); - return BbPromise.resolve(); - } -} -module.exports = ServerlessStepFunctions; diff --git a/index.test.js b/index.test.js deleted file mode 100644 index 298b1e98..00000000 --- a/index.test.js +++ /dev/null @@ -1,635 +0,0 @@ -'use strict'; - -const expect = require('chai').expect; -const BbPromise = require('bluebird'); -const sinon = require('sinon'); -const Serverless = require('serverless/lib/Serverless'); -const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); -const ServerlessStepFunctions = require('./index'); - -describe('ServerlessStepFunctions', () => { - let serverless; - let provider; - let serverlessStepFunctions; - - beforeEach(() => { - serverless = new Serverless(); - serverless.servicePath = true; - serverless.service.service = 'step-functions'; - serverless.service.functions = { - first: { - handler: true, - name: 'first', - }, - }; - const options = { - stage: 'dev', - region: 'us-east-1', - function: 'first', - functionObj: { - name: 'first', - }, - state: 'stateMachine', - data: 'inputData', - }; - - serverless.init(); - serverless.setProvider('aws', new AwsProvider(serverless)); - provider = serverless.getProvider('aws'); - serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); - }); - - describe('#constructor()', () => { - it('should have hooks', () => expect(serverlessStepFunctions.hooks).to.be.not.empty); - - it('should set the provider variable to an instance of AwsProvider', () => - expect(serverlessStepFunctions.provider).to.be.instanceof(AwsProvider)); - - it('should have access to the serverless instance', () => { - expect(serverlessStepFunctions.serverless).to.deep.equal(serverless); - }); - - it('should set the region variable', () => - expect(serverlessStepFunctions.region).to.be.equal(provider.getRegion())); - - it('should set the stage variable', () => - expect(serverlessStepFunctions.stage).to.be.equal(provider.getStage())); - - it('should set the assumeRolePolicyDocument variable', () => - expect(serverlessStepFunctions.assumeRolePolicyDocument).to.be - .equal(`{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "states.us-east-1.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] - } - `)); - - it('should run deploy:stepf:deploy promise chain in order', () => { - const deployStub = sinon - .stub(serverlessStepFunctions, 'stateMachineDeploy').returns(BbPromise.resolve()); - return serverlessStepFunctions.hooks['deploy:stepf:deploy']() - .then(() => { - expect(deployStub.calledOnce).to.be.equal(true); - serverlessStepFunctions.stateMachineDeploy.restore(); - }); - }); - - it('should run remove:stepf:remove promise chain in order', () => { - const removeStub = sinon - .stub(serverlessStepFunctions, 'stateMachineRemove').returns(BbPromise.resolve()); - return serverlessStepFunctions.hooks['remove:stepf:remove']() - .then(() => { - expect(removeStub.calledOnce).to.be.equal(true); - serverlessStepFunctions.stateMachineRemove.restore(); - }); - }); - - it('should run invoke:stepf:invoke promise chain in order', () => { - const invokeStub = sinon - .stub(serverlessStepFunctions, 'stateMachineInvoke').returns(BbPromise.resolve()); - return serverlessStepFunctions.hooks['invoke:stepf:invoke']() - .then(() => { - expect(invokeStub.calledOnce).to.be.equal(true); - serverlessStepFunctions.stateMachineInvoke.restore(); - }); - }); - - it('should set an empty options object if no options are given', () => { - const serverlessStepFunctionsWithEmptyOptions = new ServerlessStepFunctions(serverless); - expect(serverlessStepFunctionsWithEmptyOptions.options).to.deep.equal({}); - }); - }); - - describe('#stateMachineDeploy()', () => { - it('should run promise chain in order', () => { - const yamlParseStub = sinon - .stub(serverlessStepFunctions, 'yamlParse').returns(BbPromise.resolve()); - const getStateMachineArnStub = sinon - .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); - const getFunctionArnsStub = sinon - .stub(serverlessStepFunctions, 'getFunctionArns').returns(BbPromise.resolve()); - const compileStub = sinon - .stub(serverlessStepFunctions, 'compile').returns(BbPromise.resolve()); - const getIamRoleStub = sinon - .stub(serverlessStepFunctions, 'getIamRole').returns(BbPromise.resolve()); - const deleteStateMachineStub = sinon - .stub(serverlessStepFunctions, 'deleteStateMachine').returns(BbPromise.resolve()); - const createStateMachineStub = sinon - .stub(serverlessStepFunctions, 'createStateMachine').returns(BbPromise.resolve()); - - return serverlessStepFunctions.stateMachineDeploy() - .then(() => { - expect(yamlParseStub.calledOnce).to.be.equal(true); - expect(getStateMachineArnStub.calledAfter(yamlParseStub)).to.be.equal(true); - expect(getFunctionArnsStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); - expect(compileStub.calledAfter(getFunctionArnsStub)).to.be.equal(true); - expect(getIamRoleStub.calledAfter(compileStub)).to.be.equal(true); - expect(deleteStateMachineStub.calledAfter(getIamRoleStub)).to.be.equal(true); - expect(createStateMachineStub.calledAfter(deleteStateMachineStub)).to.be.equal(true); - - serverlessStepFunctions.yamlParse.restore(); - serverlessStepFunctions.getStateMachineArn.restore(); - serverlessStepFunctions.getFunctionArns.restore(); - serverlessStepFunctions.compile.restore(); - serverlessStepFunctions.getIamRole.restore(); - serverlessStepFunctions.deleteStateMachine.restore(); - serverlessStepFunctions.createStateMachine.restore(); - }); - }); - }); - - describe('#stateMachineRemove()', () => { - it('should run promise chain in order', () => { - const deleteIamRoleStub = sinon - .stub(serverlessStepFunctions, 'deleteIamRole').returns(BbPromise.resolve()); - const getStateMachineArnStub = sinon - .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); - const deleteStateMachineStub = sinon - .stub(serverlessStepFunctions, 'deleteStateMachine').returns(BbPromise.resolve()); - - return serverlessStepFunctions.stateMachineRemove() - .then(() => { - expect(deleteIamRoleStub.calledOnce).to.be.equal(true); - expect(getStateMachineArnStub.calledAfter(deleteIamRoleStub)).to.be.equal(true); - expect(deleteStateMachineStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); - - serverlessStepFunctions.getStateMachineArn.restore(); - serverlessStepFunctions.deleteStateMachine.restore(); - }); - }); - }); - - describe('#stateMachineInvoke()', () => { - it('should run promise chain in order', () => { - const getStateMachineArnStub = sinon - .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); - const startExecutionStub = sinon - .stub(serverlessStepFunctions, 'startExecution').returns(BbPromise.resolve()); - const describeExecutionStub = sinon - .stub(serverlessStepFunctions, 'describeExecution').returns(BbPromise.resolve()); - - return serverlessStepFunctions.stateMachineInvoke() - .then(() => { - expect(getStateMachineArnStub.calledOnce).to.be.equal(true); - expect(startExecutionStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); - expect(describeExecutionStub.calledAfter(startExecutionStub)).to.be.equal(true); - - serverlessStepFunctions.getStateMachineArn.restore(); - serverlessStepFunctions.startExecution.restore(); - serverlessStepFunctions.describeExecution.restore(); - }); - }); - }); - - describe('#getIamRoleName', () => { - it('should return IamRoleName', () => { - expect(serverlessStepFunctions.getIamRoleName()) - .to.be.equal('step-functions-us-east-1-dev-stateMachine-ssf-exerole'); - }); - }); - - describe('#getIamPolicyName', () => { - it('should return IamPolicyName', () => { - expect(serverlessStepFunctions.getIamPolicyName()) - .to.be.equal('step-functions-us-east-1-dev-stateMachine-ssf-exepolicy'); - }); - }); - - describe('#getIamRole()', () => { - let getRoleStub; - beforeEach(() => { - getRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ Role: { Arn: 'roleArn' } })); - }); - - it('should getIamRole with correct params', () => serverlessStepFunctions.getIamRole() - .then(() => { - expect(getRoleStub.calledOnce).to.be.equal(true); - expect(getRoleStub.calledWithExactly( - 'IAM', - 'getRole', - { - RoleName: 'step-functions-us-east-1-dev-stateMachine-ssf-exerole', - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - expect(serverlessStepFunctions.iamRoleArn).to.be.equal('roleArn'); - serverlessStepFunctions.provider.request.restore(); - }) - ); - - it('should createRole when statusCode is 404', () => { - serverlessStepFunctions.provider.request.restore(); - const getRoleErrorStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.reject({ statusCode: 404 })); - const createIamRoleStub = sinon - .stub(serverlessStepFunctions, 'createIamRole').returns(BbPromise.resolve()); - - serverlessStepFunctions.getIamRole().catch(() => { - expect(createIamRoleStub.calledOnce).to.be.equal(true); - expect(getRoleErrorStub.calledOnce).to.be.equal(true); - serverlessStepFunctions.provider.request.restore(); - serverlessStepFunctions.createIamRole.restore(); - }); - }); - - it('should throw error when statusCode is not 404', () => { - serverlessStepFunctions.provider.request.restore(); - const getRoleErrorStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.reject({ statusCode: 502 })); - - serverlessStepFunctions.getIamRole().catch((error) => { - expect(getRoleErrorStub.calledOnce).to.be.equal(true); - expect(error.name).to.be.equal('ServerlessError'); - serverlessStepFunctions.provider.request.restore(); - }); - }); - }); - - describe('#getFunctionArns()', () => { - let getCallerIdentityStub; - beforeEach(() => { - getCallerIdentityStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ Account: 1234 })); - }); - - it('should getFunctionArns with correct params', () => serverlessStepFunctions.getFunctionArns() - .then(() => { - expect(getCallerIdentityStub.calledOnce).to.be.equal(true); - expect(getCallerIdentityStub.calledWithExactly( - 'STS', - 'getCallerIdentity', - {}, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - expect(serverlessStepFunctions.functionArns.first).to.be - .equal('arn:aws:lambda:us-east-1:1234:function:first'); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#createIamRole()', () => { - let createIamRoleStub; - beforeEach(() => { - createIamRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request'); - createIamRoleStub.onFirstCall().returns(BbPromise.resolve({ Role: { Arn: 'roleArn' } })); - createIamRoleStub.onSecondCall().returns(BbPromise.resolve({ Policy: { Arn: 'policyArn' } })); - createIamRoleStub.onThirdCall().returns(BbPromise.resolve()); - }); - - it('should createIamRole with correct params', () => serverlessStepFunctions.createIamRole() - .then(() => { - expect(createIamRoleStub.calledThrice).to.be.equal(true); - expect(createIamRoleStub.args[0][0]).to.be.equal('IAM'); - expect(createIamRoleStub.args[0][1]).to.be.equal('createRole'); - expect(createIamRoleStub.args[1][0]).to.be.equal('IAM'); - expect(createIamRoleStub.args[1][1]).to.be.equal('createPolicy'); - expect(createIamRoleStub.args[2][0]).to.be.equal('IAM'); - expect(createIamRoleStub.args[2][1]).to.be.equal('attachRolePolicy'); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#deleteIamRole()', () => { - let deleteIamRoleStub; - beforeEach(() => { - deleteIamRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request'); - deleteIamRoleStub.onFirstCall().returns(BbPromise.resolve({ Account: 1234 })); - deleteIamRoleStub.onSecondCall().returns(BbPromise.resolve()); - deleteIamRoleStub.onThirdCall().returns(BbPromise.resolve()); - deleteIamRoleStub.onCall(4).returns(BbPromise.resolve()); - }); - - it('should deleteIamRole with correct params', () => serverlessStepFunctions.deleteIamRole() - .then(() => { - expect(deleteIamRoleStub.callCount).to.be.equal(4); - expect(deleteIamRoleStub.args[0][0]).to.be.equal('STS'); - expect(deleteIamRoleStub.args[0][1]).to.be.equal('getCallerIdentity'); - expect(deleteIamRoleStub.args[1][0]).to.be.equal('IAM'); - expect(deleteIamRoleStub.args[1][1]).to.be.equal('detachRolePolicy'); - expect(deleteIamRoleStub.args[2][0]).to.be.equal('IAM'); - expect(deleteIamRoleStub.args[2][1]).to.be.equal('deletePolicy'); - expect(deleteIamRoleStub.args[3][0]).to.be.equal('IAM'); - expect(deleteIamRoleStub.args[3][1]).to.be.equal('deleteRole'); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#getStateMachineArn()', () => { - let getStateMachineStub; - beforeEach(() => { - getStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ Account: 1234 })); - }); - - it('should getStateMachineStub with correct params' - , () => serverlessStepFunctions.getStateMachineArn() - .then(() => { - expect(getStateMachineStub.calledOnce).to.be.equal(true); - expect(getStateMachineStub.calledWithExactly( - 'STS', - 'getCallerIdentity', - {}, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - expect(serverlessStepFunctions.stateMachineArn).to.be - .equal('arn:aws:states:us-east-1:1234:stateMachine:step-functions-dev-stateMachine'); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#deleteStateMachine()', () => { - let deleteStateMachineStub; - beforeEach(() => { - deleteStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ Account: 1234 })); - }); - - it('should deleteStateMachine with correct params' - , () => serverlessStepFunctions.deleteStateMachine() - .then(() => { - expect(deleteStateMachineStub.calledOnce).to.be.equal(true); - expect(deleteStateMachineStub.calledWithExactly( - 'StepFunctions', - 'deleteStateMachine', - { - stateMachineArn: serverlessStepFunctions.stateMachineArn, - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#createStateMachine()', () => { - let createStateMachineStub; - beforeEach(() => { - createStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve()); - }); - - it('should createStateMachine with correct params' - , () => serverlessStepFunctions.createStateMachine() - .then(() => { - const stage = serverlessStepFunctions.options.stage; - const state = serverlessStepFunctions.options.state; - expect(createStateMachineStub.calledOnce).to.be.equal(true); - expect(createStateMachineStub.calledWithExactly( - 'StepFunctions', - 'createStateMachine', - { - definition: serverlessStepFunctions - .awsStateLanguage[serverlessStepFunctions.options.state], - name: `${serverless.service.service}-${stage}-${state}`, - roleArn: serverlessStepFunctions.iamRoleArn, - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#parseInputdate()', () => { - beforeEach(() => { - serverlessStepFunctions.serverless.config.servicePath = 'servicePath'; - sinon.stub(serverlessStepFunctions.serverless.utils, 'fileExistsSync').returns(true); - sinon.stub(serverlessStepFunctions.serverless.utils, 'readFileSync') - .returns({ foo: 'var' }); - serverlessStepFunctions.options.data = null; - serverlessStepFunctions.options.path = 'data.json'; - }); - - it('should throw error if file does not exists', () => { - serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); - sinon.stub(serverlessStepFunctions.serverless.utils, 'fileExistsSync').returns(false); - expect(() => serverlessStepFunctions.parseInputdate()).to.throw(Error); - serverlessStepFunctions.serverless.utils.readFileSync.restore(); - }); - - it('should parse file if path param is provided' - , () => serverlessStepFunctions.parseInputdate().then(() => { - expect(serverlessStepFunctions.options.data).to.deep.equal('{"foo":"var"}'); - serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); - serverlessStepFunctions.serverless.utils.readFileSync.restore(); - }) - ); - - it('should return resolve if path param is not provided', () => { - serverlessStepFunctions.options.path = null; - return serverlessStepFunctions.parseInputdate().then(() => { - expect(serverlessStepFunctions.options.data).to.deep.equal(null); - serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); - serverlessStepFunctions.serverless.utils.readFileSync.restore(); - }); - }); - }); - - describe('#startExecution()', () => { - let startExecutionStub; - beforeEach(() => { - startExecutionStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ executionArn: 'executionArn' })); - }); - - it('should startExecution with correct params', () => serverlessStepFunctions.startExecution() - .then(() => { - expect(startExecutionStub.calledOnce).to.be.equal(true); - expect(startExecutionStub.calledWithExactly( - 'StepFunctions', - 'startExecution', - { - stateMachineArn: serverlessStepFunctions.stateMachineArn, - input: serverlessStepFunctions.options.data, - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - expect(serverlessStepFunctions.executionArn).to.be.equal('executionArn'); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#describeExecution()', () => { - let describeExecutionStub; - it('should describeExecution with correct params', () => { - describeExecutionStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ status: 'SUCCESS' })); - - serverlessStepFunctions.describeExecution() - .then(() => { - expect(describeExecutionStub.calledOnce).to.be.equal(true); - expect(describeExecutionStub.calledWithExactly( - 'StepFunctions', - 'describeExecution', - { - executionArn: serverlessStepFunctions.executionArn, - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - serverlessStepFunctions.provider.request.restore(); - }); - }); - - it('should describeExecution with status FAILED', () => { - describeExecutionStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ status: 'FAILED' })); - const getExecutionHistoryStub = sinon - .stub(serverlessStepFunctions, 'getExecutionHistory') - .returns(BbPromise.resolve({ events: [{ executionFailedEventDetails: 'error' }] })); - - serverlessStepFunctions.describeExecution() - .then(() => { - expect(describeExecutionStub.calledOnce).to.be.equal(true); - expect(describeExecutionStub.calledWithExactly( - 'StepFunctions', - 'describeExecution', - { - executionArn: serverlessStepFunctions.executionArn, - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - expect(getExecutionHistoryStub.calledOnce).to.be.equal(true); - serverlessStepFunctions.provider.request.restore(); - serverlessStepFunctions.getExecutionHistory.restore(); - }); - }); - }); - - describe('#getExecutionHistory()', () => { - let getExecutionHistoryStub; - beforeEach(() => { - getExecutionHistoryStub = sinon.stub(serverlessStepFunctions.provider, 'request') - .returns(BbPromise.resolve({ events: [{ executionFailedEventDetails: 'error' }] })); - }); - - it('should getExecutionHistory with correct params' - , () => serverlessStepFunctions.getExecutionHistory() - .then(() => { - expect(getExecutionHistoryStub.calledOnce).to.be.equal(true); - expect(getExecutionHistoryStub.calledWithExactly( - 'StepFunctions', - 'getExecutionHistory', - { - executionArn: serverlessStepFunctions.executionArn, - }, - serverlessStepFunctions.options.stage, - serverlessStepFunctions.options.region - )).to.be.equal(true); - serverlessStepFunctions.provider.request.restore(); - }) - ); - }); - - describe('#yamlParse()', () => { - let yamlParserStub; - beforeEach(() => { - yamlParserStub = sinon.stub(serverlessStepFunctions.serverless.yamlParser, 'parse') - .returns(BbPromise.resolve({ stepFunctions: { stateMachine: 'stepFunctions' } })); - serverlessStepFunctions.serverless.config.servicePath = 'servicePath'; - }); - - it('should yamlParse with correct params' - , () => serverlessStepFunctions.yamlParse() - .then(() => { - expect(yamlParserStub.calledOnce).to.be.equal(true); - expect(serverless.service.stepFunctions).to.be.equal('stepFunctions'); - serverlessStepFunctions.serverless.yamlParser.parse.restore(); - }) - ); - - it('should return resolve when servicePath does not exists', () => { - serverlessStepFunctions.serverless.config.servicePath = null; - serverlessStepFunctions.yamlParse() - .then(() => { - expect(yamlParserStub.callCount).to.be.equal(0); - serverlessStepFunctions.serverless.yamlParser.parse.restore(); - }); - }); - - it('should return resolve when variables exists in the yaml', () => { - serverlessStepFunctions.serverless.yamlParser.parse.restore(); - yamlParserStub = sinon.stub(serverlessStepFunctions.serverless.yamlParser, 'parse') - .returns(BbPromise.resolve({ stepFunctions: { stateMachine: '${self:defaults.region}' } })); - serverlessStepFunctions.yamlParse() - .then(() => { - expect(yamlParserStub.calledOnce).to.be.equal(true); - expect(serverless.service.stepFunctions).to.be.equal('us-east-1'); - }); - }); - }); - - describe('#compile()', () => { - it('should throw error when stepFunction state does not exists', () => { - expect(() => serverlessStepFunctions.compile()).to.throw(Error); - }); - - it('should throw error when stateMachine name does not exists', () => { - serverlessStepFunctions.stepFunctions = {}; - expect(() => serverlessStepFunctions.compile()).to.throw(Error); - }); - - it('should comple with correct params', () => { - serverless.service.stepFunctions = { - stateMachine: { - States: { - HelloWorld: { - Resource: 'first', - }, - }, - }, - }; - serverlessStepFunctions.functionArns.first = 'lambdaArn'; - serverlessStepFunctions.compile().then(() => { - expect(serverlessStepFunctions.awsStateLanguage.stateMachine) - .to.be.equal('{"States":{"HelloWorld":{"Resource":"lambdaArn"}}}'); - }); - }); - - it('should comple with correct params when nested Resource', () => { - serverless.service.stepFunctions = { - stateMachine: { - States: { - HelloWorld: { - Resource: 'first', - HelloWorld: { - Resource: 'first', - HelloWorld: { - Resource: 'first', - }, - }, - }, - }, - }, - }; - - let a = '{"States":{"HelloWorld":{"Resource":"lambdaArn","HelloWorld"'; - a += ':{"Resource":"lambdaArn","HelloWorld":{"Resource":"lambdaArn"}}}}}'; - serverlessStepFunctions.functionArns.first = 'lambdaArn'; - serverlessStepFunctions.compile().then(() => { - expect(serverlessStepFunctions.awsStateLanguage.stateMachine).to.be.equal(a); - }); - }); - }); -}); - diff --git a/lib/dataProcessing.js b/lib/dataProcessing.js new file mode 100644 index 00000000..113ceffa --- /dev/null +++ b/lib/dataProcessing.js @@ -0,0 +1,101 @@ +'use strict'; +const BbPromise = require('bluebird'); +const path = require('path'); +const _ = require('lodash'); + +module.exports = { + functionArns: {}, + yamlParse() { + const servicePath = this.serverless.config.servicePath; + + if (!servicePath) { + return BbPromise.resolve(); + } + + const serverlessYmlPath = path.join(servicePath, 'serverless.yml'); + return this.serverless.yamlParser + .parse(serverlessYmlPath) + .then((serverlessFileParam) => { + this.serverless.service.stepFunctions = serverlessFileParam.stepFunctions.stateMachine; + this.serverless.variables.populateService(this.serverless.pluginManager.cliOptions); + return BbPromise.resolve(); + }); + }, + + parseInputdate() { + if (!this.options.data && this.options.path) { + const absolutePath = path.isAbsolute(this.options.path) ? + this.options.path : + path.join(this.serverless.config.servicePath, this.options.path); + if (!this.serverless.utils.fileExistsSync(absolutePath)) { + throw new this.serverless.classes.Error('The file you provided does not exist.'); + } + this.options.data = JSON.stringify(this.serverless.utils.readFileSync(absolutePath)); + } + return BbPromise.resolve(); + }, + + getFunctionArns() { + return this.provider.request('STS', + 'getCallerIdentity', + {}, + this.options.stage, + this.options.region) + .then((result) => { + _.forEach(this.serverless.service.functions, (value, key) => { + this.functionArns[key] + = `arn:aws:lambda:${this.region}:${result.Account}:function:${value.name}`; + }); + return BbPromise.resolve(); + }); + }, + + compile() { + if (!this.serverless.service.stepFunctions) { + const errorMessage = [ + 'stepFunctions statement does not exists in serverless.yml', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + if (typeof this.serverless.service.stepFunctions[this.options.state] === 'undefined') { + const errorMessage = [ + `Step function "${this.options.state}" is not exists`, + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + this.serverless.service.stepFunctions[this.options.state] = + JSON.stringify(this.serverless.service.stepFunctions[this.options.state]); + _.forEach(this.functionArns, (value, key) => { + const regExp = new RegExp(`"Resource":"${key}"`, 'g'); + this.serverless.service.stepFunctions[this.options.state] = + this.serverless.service.stepFunctions[this.options.state] + .replace(regExp, `"Resource":"${value}"`); + }); + return BbPromise.resolve(); + }, + + compileAll() { + if (!this.serverless.service.stepFunctions) { + const errorMessage = [ + 'stepFunctions statement does not exists in serverless.yml', + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + + _.forEach(this.serverless.service.stepFunctions, (stepFunctionObj, stepFunctionKey) => { + this.serverless.service.stepFunctions[stepFunctionKey] = JSON.stringify(stepFunctionObj); + }); + + _.forEach(this.functionArns, (functionObj, functionKey) => { + const regExp = new RegExp(`"Resource":"${functionKey}"`, 'g'); + _.forEach(this.serverless.service.stepFunctions, (stepFunctionObj, stepFunctionKey) => { + this.serverless.service.stepFunctions[stepFunctionKey] = + this.serverless.service.stepFunctions[stepFunctionKey] + .replace(regExp, `"Resource":"${functionObj}"`); + }); + }); + return BbPromise.resolve(); + }, +}; diff --git a/lib/dataProcessing.test.js b/lib/dataProcessing.test.js new file mode 100644 index 00000000..3e29e925 --- /dev/null +++ b/lib/dataProcessing.test.js @@ -0,0 +1,230 @@ +'use strict'; + +const expect = require('chai').expect; +const BbPromise = require('bluebird'); +const sinon = require('sinon'); +const Serverless = require('serverless/lib/Serverless'); +const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); +const ServerlessStepFunctions = require('./index'); + +describe('dataProsessing', () => { + let serverless; + let serverlessStepFunctions; + + beforeEach(() => { + serverless = new Serverless(); + serverless.servicePath = true; + serverless.service.service = 'step-functions'; + serverless.service.functions = { + first: { + handler: true, + name: 'first', + }, + }; + + const options = { + stage: 'dev', + region: 'us-east-1', + function: 'first', + functionObj: { + name: 'first', + }, + state: 'hellofunc', + data: 'inputData', + }; + + serverless.init(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); + }); + + describe('#yamlParse()', () => { + let yamlParserStub; + beforeEach(() => { + yamlParserStub = sinon.stub(serverlessStepFunctions.serverless.yamlParser, 'parse') + .returns(BbPromise.resolve({ stepFunctions: { stateMachine: 'stepFunctions' } })); + serverlessStepFunctions.serverless.config.servicePath = 'servicePath'; + }); + + it('should yamlParse with correct params' + , () => serverlessStepFunctions.yamlParse() + .then(() => { + expect(yamlParserStub.calledOnce).to.be.equal(true); + expect(serverless.service.stepFunctions).to.be.equal('stepFunctions'); + serverlessStepFunctions.serverless.yamlParser.parse.restore(); + }) + ); + + it('should return resolve when servicePath does not exists', () => { + serverlessStepFunctions.serverless.config.servicePath = null; + serverlessStepFunctions.yamlParse() + .then(() => { + expect(yamlParserStub.callCount).to.be.equal(0); + serverlessStepFunctions.serverless.yamlParser.parse.restore(); + }); + }); + + it('should return resolve when variables exists in the yaml', () => { + serverlessStepFunctions.serverless.yamlParser.parse.restore(); + yamlParserStub = sinon.stub(serverlessStepFunctions.serverless.yamlParser, 'parse') + .returns(BbPromise.resolve({ stepFunctions: { stateMachine: '${self:defaults.region}' } })); + serverlessStepFunctions.yamlParse() + .then(() => { + expect(yamlParserStub.calledOnce).to.be.equal(true); + expect(serverless.service.stepFunctions).to.be.equal('us-east-1'); + }); + }); + }); + + describe('#compile()', () => { + it('should throw error when stepFunction state does not exists', () => { + expect(() => serverlessStepFunctions.compile()).to.throw(Error); + }); + + it('should throw error when stateMachine name does not exists', () => { + serverlessStepFunctions.stepFunctions = {}; + expect(() => serverlessStepFunctions.compile()).to.throw(Error); + }); + + it('should comple with correct params', () => { + serverless.service.stepFunctions = { + hellofunc: { + States: { + HelloWorld: { + Resource: 'first', + }, + }, + }, + }; + serverlessStepFunctions.functionArns.first = 'lambdaArn'; + serverlessStepFunctions.compile().then(() => { + expect(serverlessStepFunctions.serverless.service.stepFunctions.hellofunc) + .to.be.equal('{"States":{"HelloWorld":{"Resource":"lambdaArn"}}}'); + }); + }); + + it('should comple with correct params when nested Resource', () => { + serverlessStepFunctions.serverless.service.stepFunctions = { + hellofunc: { + States: { + HelloWorld: { + Resource: 'first', + HelloWorld: { + Resource: 'first', + HelloWorld: { + Resource: 'first', + }, + }, + }, + }, + }, + }; + + let a = '{"States":{"HelloWorld":{"Resource":"lambdaArn","HelloWorld"'; + a += ':{"Resource":"lambdaArn","HelloWorld":{"Resource":"lambdaArn"}}}}}'; + serverlessStepFunctions.functionArns.first = 'lambdaArn'; + serverlessStepFunctions.compile().then(() => { + expect(serverlessStepFunctions.serverless.service.stepFunctions.hellofunc).to.be.equal(a); + }); + }); + }); + + describe('#parseInputdate()', () => { + beforeEach(() => { + serverlessStepFunctions.serverless.config.servicePath = 'servicePath'; + sinon.stub(serverlessStepFunctions.serverless.utils, 'fileExistsSync').returns(true); + sinon.stub(serverlessStepFunctions.serverless.utils, 'readFileSync') + .returns({ foo: 'var' }); + serverlessStepFunctions.options.data = null; + serverlessStepFunctions.options.path = 'data.json'; + }); + + it('should throw error if file does not exists', () => { + serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); + sinon.stub(serverlessStepFunctions.serverless.utils, 'fileExistsSync').returns(false); + expect(() => serverlessStepFunctions.parseInputdate()).to.throw(Error); + serverlessStepFunctions.serverless.utils.readFileSync.restore(); + }); + + it('should parse file if path param is provided' + , () => serverlessStepFunctions.parseInputdate().then(() => { + expect(serverlessStepFunctions.options.data).to.deep.equal('{"foo":"var"}'); + serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); + serverlessStepFunctions.serverless.utils.readFileSync.restore(); + }) + ); + + it('should parse file if path param is provided when absolute path', () => { + serverlessStepFunctions.options.path = '/data.json'; + serverlessStepFunctions.parseInputdate().then(() => { + expect(serverlessStepFunctions.options.data).to.deep.equal('{"foo":"var"}'); + serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); + serverlessStepFunctions.serverless.utils.readFileSync.restore(); + }); + }); + + it('should return resolve if path param is not provided', () => { + serverlessStepFunctions.options.path = null; + return serverlessStepFunctions.parseInputdate().then(() => { + expect(serverlessStepFunctions.options.data).to.deep.equal(null); + serverlessStepFunctions.serverless.utils.fileExistsSync.restore(); + serverlessStepFunctions.serverless.utils.readFileSync.restore(); + }); + }); + }); + + describe('#getFunctionArns()', () => { + let getCallerIdentityStub; + beforeEach(() => { + getCallerIdentityStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ Account: 1234 })); + }); + + it('should getFunctionArns with correct params', () => serverlessStepFunctions.getFunctionArns() + .then(() => { + expect(getCallerIdentityStub.calledOnce).to.be.equal(true); + expect(getCallerIdentityStub.calledWithExactly( + 'STS', + 'getCallerIdentity', + {}, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + expect(serverlessStepFunctions.functionArns.first).to.be + .equal('arn:aws:lambda:us-east-1:1234:function:first'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#compileAll()', () => { + it('should throw error when stepFunction state does not exists', () => { + expect(() => serverlessStepFunctions.compileAll()).to.throw(Error); + }); + + it('should comple with correct params when nested Resource', () => { + serverlessStepFunctions.serverless.service.stepFunctions = { + hellofunc: { + States: { + HelloWorld: { + Resource: 'first', + HelloWorld: { + Resource: 'first', + HelloWorld: { + Resource: 'first', + }, + }, + }, + }, + }, + }; + + let a = '{"States":{"HelloWorld":{"Resource":"lambdaArn","HelloWorld"'; + a += ':{"Resource":"lambdaArn","HelloWorld":{"Resource":"lambdaArn"}}}}}'; + serverlessStepFunctions.functionArns.first = 'lambdaArn'; + serverlessStepFunctions.compileAll().then(() => { + expect(serverlessStepFunctions.serverless.service.stepFunctions.hellofunc).to.be.equal(a); + }); + }); + }); +}); diff --git a/lib/iam.js b/lib/iam.js new file mode 100644 index 00000000..7bc42d5e --- /dev/null +++ b/lib/iam.js @@ -0,0 +1,162 @@ +'use strict'; +const BbPromise = require('bluebird'); +const _ = require('lodash'); + +module.exports = { + iamRoleArn: {}, + iamPolicyStatement: `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:InvokeFunction" + ], + "Resource": "*" + } + ] + } + `, + assumeRolePolicyDocument: `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "states.[region].amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + `, + getIamRoleName(state) { + let name = `${this.service}-${this.region}-${this.stage}-${state}-`; + name += 'ssf-exerole'; + return name.substr(0, 64); + }, + + getIamPolicyName(state) { + let name = `${this.service}-${this.region}-${this.stage}-${state}-`; + name += 'ssf-exepolicy'; + return name.substr(0, 64); + }, + + getIamRole(state) { + const stateMachine = state || this.options.state; + return this.provider.request('IAM', + 'getRole', + { + RoleName: this.getIamRoleName(stateMachine), + }, + this.options.stage, + this.options.region) + .then((result) => { + this.iamRoleArn[stateMachine] = result.Role.Arn; + return BbPromise.resolve(); + }).catch((error) => { + if (error.statusCode === 404) { + return this.createIamRole(stateMachine); + } + throw new this.serverless.classes.Error(error.message); + }); + }, + + getIamRoles() { + const promises = []; + _.forEach(this.serverless.service.stepFunctions, (value, key) => { + promises.push(key); + }); + + return BbPromise.map(promises, (value) => this.getIamRole(value)) + .then(() => BbPromise.resolve()); + }, + + createIamRole(state) { + const stateMachine = state || this.options.state; + return this.provider.request('IAM', + 'createRole', + { + AssumeRolePolicyDocument: this.assumeRolePolicyDocument, + RoleName: this.getIamRoleName(stateMachine), + }, + this.options.stage, + this.options.region) + .then((result) => { + this.iamRoleArn[stateMachine] = result.Role.Arn; + return this.provider.request('IAM', + 'createPolicy', + { + PolicyDocument: this.iamPolicyStatement, + PolicyName: this.getIamPolicyName(stateMachine), + }, + this.options.stage, + this.options.region); + }) + .then((result) => this.provider.request('IAM', + 'attachRolePolicy', + { + PolicyArn: result.Policy.Arn, + RoleName: this.getIamRoleName(stateMachine), + }, + this.options.stage, + this.options.region) + ) + .then(() => BbPromise.resolve()); + }, + + deleteIamRole(state) { + const stateMachine = state || this.options.state; + let policyArn; + return this.provider.request('STS', + 'getCallerIdentity', + {}, + this.options.stage, + this.options.region) + .then((result) => { + policyArn = `arn:aws:iam::${result.Account}:policy/${this.getIamPolicyName(stateMachine)}`; + + return this.provider.request('IAM', + 'detachRolePolicy', + { + PolicyArn: policyArn, + RoleName: this.getIamRoleName(stateMachine), + }, + this.options.stage, + this.options.region); + }) + .then(() => this.provider.request('IAM', + 'deletePolicy', + { + PolicyArn: policyArn, + }, + this.options.stage, + this.options.region) + ) + .then(() => this.provider.request('IAM', + 'deleteRole', + { + RoleName: this.getIamRoleName(stateMachine), + }, + this.options.stage, + this.options.region) + ) + .then(() => BbPromise.resolve()) + .catch((error) => { + if (error.statusCode === 404) { + return BbPromise.resolve(); + } + return BbPromise.reject(error.message); + }); + }, + + deleteIamRoles() { + const promises = []; + _.forEach(this.serverless.service.stepFunctions, (value, key) => { + promises.push(key); + }); + + return BbPromise.map(promises, (state) => this.deleteIamRole(state)) + .then(() => BbPromise.resolve()); + }, +}; diff --git a/lib/iam.test.js b/lib/iam.test.js new file mode 100644 index 00000000..bd09f326 --- /dev/null +++ b/lib/iam.test.js @@ -0,0 +1,218 @@ +'use strict'; + +const expect = require('chai').expect; +const BbPromise = require('bluebird'); +const sinon = require('sinon'); +const Serverless = require('serverless/lib/Serverless'); +const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); +const ServerlessStepFunctions = require('./index'); + +describe('iam', () => { + let serverless; + let serverlessStepFunctions; + + beforeEach(() => { + serverless = new Serverless(); + serverless.servicePath = true; + serverless.service.service = 'step-functions'; + serverless.service.functions = { + first: { + handler: true, + name: 'first', + }, + }; + + const options = { + stage: 'dev', + region: 'us-east-1', + function: 'first', + functionObj: { + name: 'first', + }, + state: 'hellofunc', + data: 'inputData', + }; + + serverless.init(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); + }); + + describe('#getIamRoleName', () => { + it('should return IamRoleName', () => { + expect(serverlessStepFunctions.getIamRoleName('state')) + .to.be.equal('step-functions-us-east-1-dev-state-ssf-exerole'); + }); + }); + + describe('#getIamPolicyName', () => { + it('should return IamPolicyName', () => { + expect(serverlessStepFunctions.getIamPolicyName('state')) + .to.be.equal('step-functions-us-east-1-dev-state-ssf-exepolicy'); + }); + }); + + describe('#getIamRole()', () => { + let getRoleStub; + beforeEach(() => { + getRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ Role: { Arn: 'roleArn' } })); + }); + + it('should getIamRole with correct params', () => serverlessStepFunctions.getIamRole('state') + .then(() => { + expect(getRoleStub.calledOnce).to.be.equal(true); + expect(getRoleStub.calledWithExactly( + 'IAM', + 'getRole', + { + RoleName: 'step-functions-us-east-1-dev-state-ssf-exerole', + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + expect(serverlessStepFunctions.iamRoleArn.state).to.be.equal('roleArn'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + + it('should createRole when statusCode is 404', () => { + serverlessStepFunctions.provider.request.restore(); + const getRoleErrorStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.reject({ statusCode: 404 })); + const createIamRoleStub = sinon + .stub(serverlessStepFunctions, 'createIamRole').returns(BbPromise.resolve()); + + serverlessStepFunctions.getIamRole().catch(() => { + expect(createIamRoleStub.calledOnce).to.be.equal(true); + expect(getRoleErrorStub.calledOnce).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + serverlessStepFunctions.createIamRole.restore(); + }); + }); + + it('should throw error when statusCode is not 404', () => { + serverlessStepFunctions.provider.request.restore(); + const getRoleErrorStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.reject({ statusCode: 502 })); + + serverlessStepFunctions.getIamRole().catch((error) => { + expect(getRoleErrorStub.calledOnce).to.be.equal(true); + expect(error.name).to.be.equal('ServerlessError'); + serverlessStepFunctions.provider.request.restore(); + }); + }); + }); + + describe('#createIamRole()', () => { + let createIamRoleStub; + beforeEach(() => { + createIamRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request'); + createIamRoleStub.onFirstCall().returns(BbPromise.resolve({ Role: { Arn: 'roleArn' } })); + createIamRoleStub.onSecondCall().returns(BbPromise.resolve({ Policy: { Arn: 'policyArn' } })); + createIamRoleStub.onThirdCall().returns(BbPromise.resolve()); + }); + + it('should createIamRole with correct params', () => serverlessStepFunctions.createIamRole() + .then(() => { + expect(createIamRoleStub.calledThrice).to.be.equal(true); + expect(createIamRoleStub.args[0][0]).to.be.equal('IAM'); + expect(createIamRoleStub.args[0][1]).to.be.equal('createRole'); + expect(createIamRoleStub.args[1][0]).to.be.equal('IAM'); + expect(createIamRoleStub.args[1][1]).to.be.equal('createPolicy'); + expect(createIamRoleStub.args[2][0]).to.be.equal('IAM'); + expect(createIamRoleStub.args[2][1]).to.be.equal('attachRolePolicy'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#deleteIamRole()', () => { + let deleteIamRoleStub; + beforeEach(() => { + deleteIamRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request'); + deleteIamRoleStub.onFirstCall().returns(BbPromise.resolve({ Account: 1234 })); + deleteIamRoleStub.onSecondCall().returns(BbPromise.resolve()); + deleteIamRoleStub.onThirdCall().returns(BbPromise.resolve()); + deleteIamRoleStub.onCall(4).returns(BbPromise.resolve()); + }); + + it('should deleteIamRole with correct params', () => serverlessStepFunctions.deleteIamRole() + .then(() => { + expect(deleteIamRoleStub.callCount).to.be.equal(4); + expect(deleteIamRoleStub.args[0][0]).to.be.equal('STS'); + expect(deleteIamRoleStub.args[0][1]).to.be.equal('getCallerIdentity'); + expect(deleteIamRoleStub.args[1][0]).to.be.equal('IAM'); + expect(deleteIamRoleStub.args[1][1]).to.be.equal('detachRolePolicy'); + expect(deleteIamRoleStub.args[2][0]).to.be.equal('IAM'); + expect(deleteIamRoleStub.args[2][1]).to.be.equal('deletePolicy'); + expect(deleteIamRoleStub.args[3][0]).to.be.equal('IAM'); + expect(deleteIamRoleStub.args[3][1]).to.be.equal('deleteRole'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + + it('should deleteIamRole when 404 error', () => { + serverlessStepFunctions.provider.request.restore(); + deleteIamRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.reject({ statusCode: 404 })); + serverlessStepFunctions.deleteIamRole() + .then(() => { + expect(deleteIamRoleStub.callCount).to.be.equal(1); + serverlessStepFunctions.provider.request.restore(); + }); + }); + + it('should other when other error', () => { + serverlessStepFunctions.provider.request.restore(); + deleteIamRoleStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.reject({ statusCode: 500 })); + serverlessStepFunctions.deleteIamRole() + .catch(() => { + expect(deleteIamRoleStub.callCount).to.be.equal(1); + serverlessStepFunctions.provider.request.restore(); + }); + }); + }); + + describe('#getIamRoles()', () => { + let getIamRoleStub; + beforeEach(() => { + serverless.service.stepFunctions = { + helloHello: 'value', + hogeHoge: 'value', + sssss: 'value', + }; + getIamRoleStub = sinon + .stub(serverlessStepFunctions, 'getIamRole').returns(BbPromise.resolve()); + }); + + it('should getIamRoles with correct params' + , () => serverlessStepFunctions.getIamRoles() + .then(() => { + expect(getIamRoleStub.calledThrice).to.be.equal(true); + serverlessStepFunctions.getIamRole.restore(); + }) + ); + }); + + describe('#deleteIamRoles()', () => { + let deleteIamRoleStub; + beforeEach(() => { + serverless.service.stepFunctions = { + helloHello: 'value', + hogeHoge: 'value', + }; + deleteIamRoleStub = sinon + .stub(serverlessStepFunctions, 'deleteIamRole').returns(BbPromise.resolve()); + }); + + it('should deleteIamRoles with correct params' + , () => serverlessStepFunctions.deleteIamRoles() + .then(() => { + expect(deleteIamRoleStub.calledTwice).to.be.equal(true); + serverlessStepFunctions.deleteIamRole.restore(); + }) + ); + }); +}); diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..d5267a2b --- /dev/null +++ b/lib/index.js @@ -0,0 +1,280 @@ +'use strict'; +const stateMachine = require('./stateMachine'); +const iam = require('./iam'); +const dataProcessing = require('./dataProcessing'); +const utils = require('./utils'); +const BbPromise = require('bluebird'); +const _ = require('lodash'); +const chalk = require('chalk'); + +class ServerlessStepFunctions { + constructor(serverless, options) { + this.serverless = serverless; + this.options = options || {}; + this.provider = this.serverless.getProvider('aws'); + this.service = this.serverless.service.service; + this.region = this.provider.getRegion(); + this.stage = this.provider.getStage(); + + Object.assign( + this, + stateMachine, + iam, + dataProcessing, + utils + ); + this.assumeRolePolicyDocument = + this.assumeRolePolicyDocument.replace('[region]', this.region); + this.commands = { + deploy: { + commands: { + stepf: { + usage: 'Deploy the State Machine of Step functions', + lifecycleEvents: [ + 'deploy', + ], + options: { + state: { + usage: 'Name of the State Machine', + shortcut: 't', + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + }, + region: { + usage: 'Region of the service', + shortcut: 'r', + }, + }, + }, + tasks: { + usage: 'Deploy the Tasks of Step functions', + lifecycleEvents: [ + 'deploy', + ], + options: { + state: { + usage: 'Name of the Tasks', + shortcut: 't', + required: true, + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + }, + region: { + usage: 'Region of the service', + shortcut: 'r', + }, + }, + }, + }, + }, + remove: { + commands: { + stepf: { + usage: 'Remove Step functions', + lifecycleEvents: [ + 'remove', + ], + options: { + state: { + usage: 'Name of the State Machine', + shortcut: 't', + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + }, + region: { + usage: 'Region of the service', + shortcut: 'r', + }, + }, + }, + tasks: { + usage: 'Remove the Tasks of Step functions', + lifecycleEvents: [ + 'deploy', + ], + options: { + state: { + usage: 'Name of the Tasks', + shortcut: 't', + required: true, + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + }, + region: { + usage: 'Region of the service', + shortcut: 'r', + }, + }, + }, + }, + }, + invoke: { + commands: { + stepf: { + usage: 'Invoke Step functions', + lifecycleEvents: [ + 'invoke', + ], + options: { + state: { + usage: 'Name of the State Machine', + shortcut: 't', + required: true, + }, + data: { + usage: 'String data to be passed as an event to your step function', + shortcut: 'd', + }, + path: { + usage: + 'The path to a json file with input data to be passed to the invoked step function', + shortcut: 'p', + }, + stage: { + usage: 'Stage of the service', + shortcut: 's', + }, + region: { + usage: 'Region of the service', + shortcut: 'r', + }, + }, + }, + }, + }, + }; + + this.hooks = { + 'deploy:stepf:deploy': () => BbPromise.bind(this) + .then(this.stateMachineDeploy), + 'remove:stepf:remove': () => BbPromise.bind(this) + .then(this.stateMachineRemove), + 'invoke:stepf:invoke': () => BbPromise.bind(this) + .then(this.stateMachineInvoke), +// 'deploy:tasks:deploy': () => BbPromise.bind(this) +// .then(this.tasksDeploy), +// 'remove:tasks:remove': () => BbPromise.bind(this) +// .then(this.tasksRemove), + }; + } + + stateMachineDeploy() { + if (this.options.state) { + this.serverless.cli.log(`Start to deploy ${this.options.state} step function...`); + return BbPromise.bind(this) + .then(this.yamlParse) + .then(this.getStateMachineArn) + .then(this.getFunctionArns) + .then(this.compile) + .then(this.getIamRole) + .then(this.deleteStateMachine) + .then(this.createStateMachine) + .then(() => { + this.serverless.cli.consoleLog(''); + this.serverless.cli.log(`Finish to deploy ${this.options.state} step function`); + let message = ''; + message += `${chalk.yellow.underline('Service Information')}\n`; + message += `${chalk.yellow('service:')} ${this.service}\n`; + message += `${chalk.yellow('stage:')} ${this.stage}\n`; + message += `${chalk.yellow('region:')} ${this.region}\n\n`; + message += `${chalk.yellow.underline('State Machine Information')}\n`; + message += `${chalk.yellow(this.options.state)}${chalk.yellow(':')} `; + message += `${this.stateMachineArns[this.options.state]}\n`; + this.serverless.cli.consoleLog(message); + return BbPromise.resolve(); + }); + } + this.serverless.cli.log('Start to deploy all step functions...'); + return BbPromise.bind(this) + .then(this.yamlParse) + .then(this.getStateMachineNames) + .then(this.getFunctionArns) + .then(this.compileAll) + .then(this.getIamRoles) + .then(this.deleteStateMachines) + .then(this.createStateMachines) + .then(() => { + this.serverless.cli.consoleLog(''); + this.serverless.cli.log('Finish to deploy all step functions'); + let message = ''; + message += `${chalk.yellow.underline('Service Information')}\n`; + message += `${chalk.yellow('service:')} ${this.service}\n`; + message += `${chalk.yellow('stage:')} ${this.stage}\n`; + message += `${chalk.yellow('region:')} ${this.region}\n\n`; + message += `${chalk.yellow.underline('State Machine Information')}\n`; + _.forEach(this.stateMachineArns, (arn, name) => { + message += `${chalk.yellow(name)}${chalk.yellow(':')} ${arn}\n`; + }); + this.serverless.cli.consoleLog(message); + return BbPromise.resolve(); + }); + } + + stateMachineRemove() { + if (this.options.state) { + return BbPromise.bind(this) + .then(this.yamlParse) + .then(this.deleteIamRole) + .then(this.getStateMachineArn) + .then(this.deleteStateMachine) + .then(() => { + this.serverless.cli.log(`Remove ${this.options.state}`); + return BbPromise.resolve(); + }); + } + return BbPromise.bind(this) + .then(this.yamlParse) + .then(this.deleteIamRoles) + .then(this.getStateMachineNames) + .then(this.deleteStateMachines) + .then(() => { + this.serverless.cli.log('Remove all state machine'); + let message = ''; + message += `${chalk.yellow.underline('Service Information')}\n`; + message += `${chalk.yellow('service:')} ${this.service}\n`; + message += `${chalk.yellow('stage:')} ${this.stage}\n`; + message += `${chalk.yellow('region:')} ${this.region}\n\n`; + message += `${chalk.yellow.underline('Deleted State Machine')}\n`; + _.forEach(this.stateMachineArns, (arn, name) => { + message += `${chalk.yellow(name)}${chalk.yellow(':')} ${arn}\n`; + }); + this.serverless.cli.consoleLog(message); + return BbPromise.resolve(); + }); + } + + stateMachineInvoke() { + return BbPromise.bind(this) + .then(this.parseInputdate) + .then(this.getStateMachineArn) + .then(this.startExecution) + .then(this.describeExecution) + .then((result) => { + this.serverless.cli.consoleLog(''); + this.serverless.cli.consoleLog(chalk.yellow.underline('Execution Result')); + this.serverless.cli.consoleLog(''); + this.serverless.cli.consoleLog(result); + + if (result.status === 'FAILED') { + return this.getExecutionHistory() + .then((error) => { + this.serverless.cli.consoleLog(''); + this.serverless.cli.consoleLog(chalk.yellow.underline('Error Log')); + this.serverless.cli.consoleLog(''); + this.serverless.cli.consoleLog(error.events[error.events.length - 1] + .executionFailedEventDetails); + }); + } + return BbPromise.resolve(); + }); + } +} +module.exports = ServerlessStepFunctions; diff --git a/lib/index.test.js b/lib/index.test.js new file mode 100644 index 00000000..983f99c1 --- /dev/null +++ b/lib/index.test.js @@ -0,0 +1,297 @@ +'use strict'; + +const expect = require('chai').expect; +const BbPromise = require('bluebird'); +const sinon = require('sinon'); +const Serverless = require('serverless/lib/Serverless'); +const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); +const ServerlessStepFunctions = require('./index'); + +describe('ServerlessStepFunctions', () => { + let serverless; + let provider; + let serverlessStepFunctions; + + beforeEach(() => { + serverless = new Serverless(); + serverless.servicePath = true; + serverless.service.service = 'step-functions'; + serverless.service.functions = { + first: { + handler: true, + name: 'first', + }, + }; + + const options = { + stage: 'dev', + region: 'us-east-1', + function: 'first', + functionObj: { + name: 'first', + }, + state: 'hellofunc', + data: 'inputData', + }; + + serverless.init(); + serverless.setProvider('aws', new AwsProvider(serverless)); + provider = serverless.getProvider('aws'); + serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); + }); + + describe('#constructor()', () => { + it('should have hooks', () => expect(serverlessStepFunctions.hooks).to.be.not.empty); + + it('should set the provider variable to an instance of AwsProvider', () => + expect(serverlessStepFunctions.provider).to.be.instanceof(AwsProvider)); + + it('should have access to the serverless instance', () => + expect(serverlessStepFunctions.serverless).to.deep.equal(serverless)); + + it('should set the region variable', () => + expect(serverlessStepFunctions.region).to.be.equal(provider.getRegion())); + + it('should set the stage variable', () => + expect(serverlessStepFunctions.stage).to.be.equal(provider.getStage())); + + it('should set the assumeRolePolicyDocument variable', () => + expect(serverlessStepFunctions.assumeRolePolicyDocument).to.be + .equal(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "states.us-east-1.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + ` +)); + + it('should run deploy:stepf:deploy promise chain in order', () => { + const deployStub = sinon + .stub(serverlessStepFunctions, 'stateMachineDeploy').returns(BbPromise.resolve()); + return serverlessStepFunctions.hooks['deploy:stepf:deploy']() + .then(() => { + expect(deployStub.calledOnce).to.be.equal(true); + serverlessStepFunctions.stateMachineDeploy.restore(); + }); + }); + + it('should run remove:stepf:remove promise chain in order', () => { + const removeStub = sinon + .stub(serverlessStepFunctions, 'stateMachineRemove').returns(BbPromise.resolve()); + return serverlessStepFunctions.hooks['remove:stepf:remove']() + .then(() => { + expect(removeStub.calledOnce).to.be.equal(true); + serverlessStepFunctions.stateMachineRemove.restore(); + }); + }); + + it('should run invoke:stepf:invoke promise chain in order', () => { + const invokeStub = sinon + .stub(serverlessStepFunctions, 'stateMachineInvoke').returns(BbPromise.resolve()); + return serverlessStepFunctions.hooks['invoke:stepf:invoke']() + .then(() => { + expect(invokeStub.calledOnce).to.be.equal(true); + serverlessStepFunctions.stateMachineInvoke.restore(); + }); + }); + + it('should set an empty options object if no options are given', () => { + const serverlessStepFunctionsWithEmptyOptions = new ServerlessStepFunctions(serverless); + expect(serverlessStepFunctionsWithEmptyOptions.options).to.deep.equal({}); + }); + }); + + describe('#stateMachineDeploy()', () => { + it('should run promise chain in order when state is given', () => { + const yamlParseStub = sinon + .stub(serverlessStepFunctions, 'yamlParse').returns(BbPromise.resolve()); + const getStateMachineArnStub = sinon + .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); + const getFunctionArnsStub = sinon + .stub(serverlessStepFunctions, 'getFunctionArns').returns(BbPromise.resolve()); + const compileStub = sinon + .stub(serverlessStepFunctions, 'compile').returns(BbPromise.resolve()); + const getIamRoleStub = sinon + .stub(serverlessStepFunctions, 'getIamRole').returns(BbPromise.resolve()); + const deleteStateMachineStub = sinon + .stub(serverlessStepFunctions, 'deleteStateMachine').returns(BbPromise.resolve()); + const createStateMachineStub = sinon + .stub(serverlessStepFunctions, 'createStateMachine').returns(BbPromise.resolve()); + + return serverlessStepFunctions.stateMachineDeploy() + .then(() => { + expect(yamlParseStub.calledOnce).to.be.equal(true); + expect(getStateMachineArnStub.calledAfter(yamlParseStub)).to.be.equal(true); + expect(getFunctionArnsStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); + expect(compileStub.calledAfter(getFunctionArnsStub)).to.be.equal(true); + expect(getIamRoleStub.calledAfter(compileStub)).to.be.equal(true); + expect(deleteStateMachineStub.calledAfter(getIamRoleStub)).to.be.equal(true); + expect(createStateMachineStub.calledAfter(deleteStateMachineStub)).to.be.equal(true); + + serverlessStepFunctions.yamlParse.restore(); + serverlessStepFunctions.getStateMachineArn.restore(); + serverlessStepFunctions.getFunctionArns.restore(); + serverlessStepFunctions.compile.restore(); + serverlessStepFunctions.getIamRole.restore(); + serverlessStepFunctions.deleteStateMachine.restore(); + serverlessStepFunctions.createStateMachine.restore(); + }); + }); + }); + + it('should run promise chain in order when state is not given', () => { + serverlessStepFunctions.options.state = null; + const yamlParseStub = sinon + .stub(serverlessStepFunctions, 'yamlParse').returns(BbPromise.resolve()); + const getStateMachineNamesStub = sinon + .stub(serverlessStepFunctions, 'getStateMachineNames').returns(BbPromise.resolve()); + const getFunctionArnsStub = sinon + .stub(serverlessStepFunctions, 'getFunctionArns').returns(BbPromise.resolve()); + const compileAllStub = sinon + .stub(serverlessStepFunctions, 'compileAll').returns(BbPromise.resolve()); + const getIamRolesStub = sinon + .stub(serverlessStepFunctions, 'getIamRoles').returns(BbPromise.resolve()); + const deleteStateMachinesStub = sinon + .stub(serverlessStepFunctions, 'deleteStateMachines').returns(BbPromise.resolve()); + const createStateMachinesStub = sinon + .stub(serverlessStepFunctions, 'createStateMachines').returns(BbPromise.resolve()); + + return serverlessStepFunctions.stateMachineDeploy() + .then(() => { + expect(yamlParseStub.calledOnce).to.be.equal(true); + expect(getStateMachineNamesStub.calledAfter(yamlParseStub)).to.be.equal(true); + expect(getFunctionArnsStub.calledAfter(getStateMachineNamesStub)).to.be.equal(true); + expect(compileAllStub.calledAfter(getFunctionArnsStub)).to.be.equal(true); + expect(getIamRolesStub.calledAfter(compileAllStub)).to.be.equal(true); + expect(deleteStateMachinesStub.calledAfter(getIamRolesStub)).to.be.equal(true); + expect(createStateMachinesStub.calledAfter(deleteStateMachinesStub)).to.be.equal(true); + + serverlessStepFunctions.yamlParse.restore(); + serverlessStepFunctions.getStateMachineNames.restore(); + serverlessStepFunctions.getFunctionArns.restore(); + serverlessStepFunctions.compileAll.restore(); + serverlessStepFunctions.getIamRoles.restore(); + serverlessStepFunctions.deleteStateMachines.restore(); + serverlessStepFunctions.createStateMachines.restore(); + }); + }); + + describe('#stateMachineRemove()', () => { + it('should run promise chain in order when state is given', () => { + const yamlParseStub = sinon + .stub(serverlessStepFunctions, 'yamlParse').returns(BbPromise.resolve()); + const deleteIamRoleStub = sinon + .stub(serverlessStepFunctions, 'deleteIamRole').returns(BbPromise.resolve()); + const getStateMachineArnStub = sinon + .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); + const deleteStateMachineStub = sinon + .stub(serverlessStepFunctions, 'deleteStateMachine').returns(BbPromise.resolve()); + + return serverlessStepFunctions.stateMachineRemove() + .then(() => { + expect(yamlParseStub.calledOnce).to.be.equal(true); + expect(deleteIamRoleStub.calledAfter(yamlParseStub)).to.be.equal(true); + expect(getStateMachineArnStub.calledAfter(deleteIamRoleStub)).to.be.equal(true); + expect(deleteStateMachineStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); + + serverlessStepFunctions.yamlParse.restore(); + serverlessStepFunctions.deleteIamRole.restore(); + serverlessStepFunctions.getStateMachineArn.restore(); + serverlessStepFunctions.deleteStateMachine.restore(); + }); + }); + + it('should run promise chain in order when state is not given', () => { + serverlessStepFunctions.options.state = null; + const yamlParseStub = sinon + .stub(serverlessStepFunctions, 'yamlParse').returns(BbPromise.resolve()); + const deleteIamRolesStub = sinon + .stub(serverlessStepFunctions, 'deleteIamRoles').returns(BbPromise.resolve()); + const getStateMachineNamesStub = sinon + .stub(serverlessStepFunctions, 'getStateMachineNames').returns(BbPromise.resolve()); + const deleteStateMachinesStub = sinon + .stub(serverlessStepFunctions, 'deleteStateMachines').returns(BbPromise.resolve()); + + return serverlessStepFunctions.stateMachineRemove() + .then(() => { + expect(yamlParseStub.calledOnce).to.be.equal(true); + expect(deleteIamRolesStub.calledAfter(yamlParseStub)).to.be.equal(true); + expect(getStateMachineNamesStub.calledAfter(deleteIamRolesStub)).to.be.equal(true); + expect(deleteStateMachinesStub.calledAfter(getStateMachineNamesStub)).to.be.equal(true); + + serverlessStepFunctions.yamlParse.restore(); + serverlessStepFunctions.deleteIamRoles.restore(); + serverlessStepFunctions.getStateMachineNames.restore(); + serverlessStepFunctions.deleteStateMachines.restore(); + }); + }); + }); + + describe('#stateMachineInvoke()', () => { + it('should run promise chain in order', () => { + const parseInputdateStub = sinon + .stub(serverlessStepFunctions, 'parseInputdate').returns(BbPromise.resolve()); + const getStateMachineArnStub = sinon + .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); + const startExecutionStub = sinon + .stub(serverlessStepFunctions, 'startExecution').returns(BbPromise.resolve()); + const describeExecutionStub = sinon + .stub(serverlessStepFunctions, 'describeExecution') + .returns(BbPromise.resolve({ status: 'SUCCEED' })); + + return serverlessStepFunctions.stateMachineInvoke() + .then(() => { + expect(parseInputdateStub.calledOnce).to.be.equal(true); + expect(getStateMachineArnStub.calledAfter(parseInputdateStub)).to.be.equal(true); + expect(startExecutionStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); + expect(describeExecutionStub.calledAfter(startExecutionStub)).to.be.equal(true); + + serverlessStepFunctions.parseInputdate.restore(); + serverlessStepFunctions.getStateMachineArn.restore(); + serverlessStepFunctions.startExecution.restore(); + serverlessStepFunctions.describeExecution.restore(); + }); + }); + + it('should run promise chain in order when invocation error occurs', () => { + const parseInputdateStub = sinon + .stub(serverlessStepFunctions, 'parseInputdate').returns(BbPromise.resolve()); + const getStateMachineArnStub = sinon + .stub(serverlessStepFunctions, 'getStateMachineArn').returns(BbPromise.resolve()); + const startExecutionStub = sinon + .stub(serverlessStepFunctions, 'startExecution').returns(BbPromise.resolve()); + const describeExecutionStub = sinon + .stub(serverlessStepFunctions, 'describeExecution') + .returns(BbPromise.resolve({ status: 'FAILED' })); + const getExecutionHistoryStub = sinon + .stub(serverlessStepFunctions, 'getExecutionHistory').returns(BbPromise.resolve({ + events: [{ + executionFailedEventDetails: '', + }], + })); + + return serverlessStepFunctions.stateMachineInvoke() + .then(() => { + expect(parseInputdateStub.calledOnce).to.be.equal(true); + expect(getStateMachineArnStub.calledAfter(parseInputdateStub)).to.be.equal(true); + expect(startExecutionStub.calledAfter(getStateMachineArnStub)).to.be.equal(true); + expect(describeExecutionStub.calledAfter(startExecutionStub)).to.be.equal(true); + expect(getExecutionHistoryStub.calledAfter(describeExecutionStub)).to.be.equal(true); + + serverlessStepFunctions.parseInputdate.restore(); + serverlessStepFunctions.getStateMachineArn.restore(); + serverlessStepFunctions.startExecution.restore(); + serverlessStepFunctions.describeExecution.restore(); + serverlessStepFunctions.getExecutionHistory.restore(); + }); + }); + }); +}); + diff --git a/lib/stateMachine.js b/lib/stateMachine.js new file mode 100644 index 00000000..f696179e --- /dev/null +++ b/lib/stateMachine.js @@ -0,0 +1,146 @@ +'use strict'; +const BbPromise = require('bluebird'); +const _ = require('lodash'); + +module.exports = { + stateMachineArns: {}, + getStateMachineName(state) { + return `${this.service}-${this.stage}-${state}`; + }, + + getStateMachineArn(state) { + const stateMachine = state || this.options.state; + return this.provider.request('STS', + 'getCallerIdentity', + {}, + this.options.stage, + this.options.region) + .then((result) => { + this.stateMachineArns[stateMachine] = + `arn:aws:states:${this.region}:${result.Account}:`; + this.stateMachineArns[stateMachine] += + `stateMachine:${this.getStateMachineName(stateMachine)}`; + return BbPromise.resolve(); + }); + }, + + getStateMachineNames() { + return this.provider.request('STS', + 'getCallerIdentity', + {}, + this.options.stage, + this.options.region) + .then((result) => { + _.forEach(this.serverless.service.stepFunctions, (value, key) => { + this.stateMachineArns[key] = + `arn:aws:states:${this.region}:${result.Account}:`; + this.stateMachineArns[key] += + `stateMachine:${this.getStateMachineName(key)}`; + }); + return BbPromise.resolve(); + }); + }, + + deleteStateMachine(state) { + const stateMachine = state || this.options.state; + return this.provider.request('StepFunctions', + 'deleteStateMachine', + { + stateMachineArn: this.stateMachineArns[stateMachine], + }, + this.options.stage, + this.options.region) + .then(() => BbPromise.resolve()); + }, + + deleteStateMachines() { + const promises = []; + _.forEach(this.serverless.service.stepFunctions, (value, key) => { + promises.push(key); + }); + + return BbPromise + .map(promises, (state) => this.deleteStateMachine(state)) + .then(() => BbPromise.resolve()); + }, + + createStateMachine(state) { + const stateMachine = state || this.options.state; + return this.provider.request('StepFunctions', + 'createStateMachine', + { + definition: this.serverless.service.stepFunctions[stateMachine], + name: this.getStateMachineName(stateMachine), + roleArn: this.iamRoleArn[stateMachine], + }, + this.options.stage, + this.options.region) + .then(() => BbPromise.resolve()) + .catch((error) => { + if (error.message.match(/State Machine is being deleted/)) { + this.serverless.cli.printDot(); + return this.setTimeout() + .then(() => this.createStateMachine(stateMachine)); + } + + throw new this.serverless.classes.Error(error.message); + }); + }, + + createStateMachines() { + const promises = []; + _.forEach(this.serverless.service.stepFunctions, (value, key) => { + promises.push(key); + }); + + return BbPromise + .map(promises, (state) => this.createStateMachine(state)) + .then(() => BbPromise.resolve()); + }, + + startExecution() { + return this.provider.request('StepFunctions', + 'startExecution', + { + stateMachineArn: this.stateMachineArns[this.options.state], + input: this.options.data, + }, + this.options.stage, + this.options.region) + .then((result) => { + this.executionArn = result.executionArn; + return BbPromise.resolve(); + }).catch((error) => { + throw new this.serverless.classes.Error(error.message); + }); + }, + + describeExecution() { + return this.provider.request('StepFunctions', + 'describeExecution', + { + executionArn: this.executionArn, + }, + this.options.stage, + this.options.region) + .then((result) => { + if (result.status === 'RUNNING') { + this.serverless.cli.printDot(); + return this.setTimeout() + .then(() => this.describeExecution()); + } + return BbPromise.resolve(result); + }); + }, + + getExecutionHistory() { + return this.provider.request('StepFunctions', + 'getExecutionHistory', + { + executionArn: this.executionArn, + }, + this.options.stage, + this.options.region) + .then((result) => BbPromise.resolve(result)); + }, +}; diff --git a/lib/stateMachine.test.js b/lib/stateMachine.test.js new file mode 100644 index 00000000..32b44c5c --- /dev/null +++ b/lib/stateMachine.test.js @@ -0,0 +1,353 @@ +'use strict'; + +const expect = require('chai').expect; +const BbPromise = require('bluebird'); +const sinon = require('sinon'); +const Serverless = require('serverless/lib/Serverless'); +const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider'); +const ServerlessStepFunctions = require('./index'); + +describe('stateMachine', () => { + let serverless; + let serverlessStepFunctions; + + beforeEach(() => { + serverless = new Serverless(); + serverless.servicePath = true; + serverless.service.service = 'step-functions'; + serverless.service.functions = { + first: { + handler: true, + name: 'first', + }, + }; + + const options = { + stage: 'dev', + region: 'us-east-1', + function: 'first', + functionObj: { + name: 'first', + }, + state: 'hellofunc', + data: 'inputData', + }; + + serverless.init(); + serverless.setProvider('aws', new AwsProvider(serverless)); + serverlessStepFunctions = new ServerlessStepFunctions(serverless, options); + }); + + describe('#getStateMachineArn()', () => { + let getStateMachineStub; + beforeEach(() => { + getStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ Account: 1234 })); + }); + + it('should getStateMachineStub when correct params is not given' + , () => serverlessStepFunctions.getStateMachineArn() + .then(() => { + expect(getStateMachineStub.calledOnce).to.be.equal(true); + expect(getStateMachineStub.calledWithExactly( + 'STS', + 'getCallerIdentity', + {}, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + expect(serverlessStepFunctions.stateMachineArns.hellofunc).to.be + .equal('arn:aws:states:us-east-1:1234:stateMachine:step-functions-dev-hellofunc'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + + it('should getStateMachineStub with correct params' + , () => serverlessStepFunctions.getStateMachineArn('state') + .then(() => { + expect(getStateMachineStub.calledOnce).to.be.equal(true); + expect(getStateMachineStub.calledWithExactly( + 'STS', + 'getCallerIdentity', + {}, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + expect(serverlessStepFunctions.stateMachineArns.state).to.be + .equal('arn:aws:states:us-east-1:1234:stateMachine:step-functions-dev-state'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#deleteStateMachine()', () => { + let deleteStateMachineStub; + beforeEach(() => { + deleteStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ Account: 1234 })); + }); + + it('should deleteStateMachine with correct params' + , () => serverlessStepFunctions.deleteStateMachine() + .then(() => { + expect(deleteStateMachineStub.calledOnce).to.be.equal(true); + expect(deleteStateMachineStub.calledWithExactly( + 'StepFunctions', + 'deleteStateMachine', + { + stateMachineArn: serverlessStepFunctions.stateMachineArns.hellofunc, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#createStateMachine()', () => { + let createStateMachineStub; + beforeEach(() => { + createStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve()); + serverless.service.stepFunctions = { state: 'state' }; + }); + + it('should createStateMachine when correct params is not given' + , () => serverlessStepFunctions.createStateMachine() + .then(() => { + const stage = serverlessStepFunctions.options.stage; + expect(createStateMachineStub.calledOnce).to.be.equal(true); + expect(createStateMachineStub.calledWithExactly( + 'StepFunctions', + 'createStateMachine', + { + definition: serverlessStepFunctions + .serverless.service.stepFunctions.hellofunc, + name: `${serverless.service.service}-${stage}-hellofunc`, + roleArn: serverlessStepFunctions.iamRoleArn.hellofunc, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + }) + ); + + it('should createStateMachine with correct params' + , () => serverlessStepFunctions.createStateMachine('state') + .then(() => { + const stage = serverlessStepFunctions.options.stage; + expect(createStateMachineStub.calledOnce).to.be.equal(true); + expect(createStateMachineStub.calledWithExactly( + 'StepFunctions', + 'createStateMachine', + { + definition: serverlessStepFunctions + .serverless.service.stepFunctions.state, + name: `${serverless.service.service}-${stage}-state`, + roleArn: serverlessStepFunctions.iamRoleArn.state, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + }) + ); + + it('should throw error when createStateMachineStub throw error', () => { + serverlessStepFunctions.provider.request.restore(); + createStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.reject(new Error('some error'))); + + serverlessStepFunctions.createStateMachine().catch((error) => { + expect(error.message).to.be.equal('some error'); + serverlessStepFunctions.provider.request.restore(); + }); + }); + + it('should do setTimeout when createStateMachineStub throw a certain error', + () => { + serverlessStepFunctions.provider.request.restore(); + createStateMachineStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.reject(new Error('some State Machine is being deleted error'))); + const setTimeoutStub = sinon.stub(serverlessStepFunctions, 'setTimeout') + .returns(BbPromise.reject()); + + serverlessStepFunctions.createStateMachine().catch(() => { + expect(createStateMachineStub.calledOnce).to.be.equal(true); + expect(setTimeoutStub.calledOnce).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + serverlessStepFunctions.setTimeout.restore(); + }); + }); + }); + + describe('#startExecution()', () => { + let startExecutionStub; + beforeEach(() => { + startExecutionStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ executionArn: 'executionArn' })); + }); + + it('should startExecution with correct params', () => serverlessStepFunctions.startExecution() + .then(() => { + expect(startExecutionStub.calledOnce).to.be.equal(true); + expect(startExecutionStub.calledWithExactly( + 'StepFunctions', + 'startExecution', + { + stateMachineArn: serverlessStepFunctions.stateMachineArns.hellofunc, + input: serverlessStepFunctions.options.data, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + expect(serverlessStepFunctions.executionArn).to.be.equal('executionArn'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#describeExecution()', () => { + let describeExecutionStub; + it('should describeExecution with correct params', () => { + describeExecutionStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ status: 'SUCCESS' })); + + serverlessStepFunctions.describeExecution() + .then(() => { + expect(describeExecutionStub.calledOnce).to.be.equal(true); + expect(describeExecutionStub.calledWithExactly( + 'StepFunctions', + 'describeExecution', + { + executionArn: serverlessStepFunctions.executionArn, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + }); + }); + + it('should do describeExecution once when status is RUNNING', () => { + describeExecutionStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ status: 'RUNNING' })); + const setTimeoutStub = sinon.stub(serverlessStepFunctions, 'setTimeout') + .returns(BbPromise.reject()); + + serverlessStepFunctions.describeExecution() + .then(() => { + expect(describeExecutionStub.calledOnce).to.be.equal(true); + expect(describeExecutionStub.calledWithExactly( + 'StepFunctions', + 'describeExecution', + { + executionArn: serverlessStepFunctions.executionArn, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + expect(setTimeoutStub.calledOnce).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + serverlessStepFunctions.setTimeout.restore(); + }); + }); + }); + + describe('#getExecutionHistory()', () => { + let getExecutionHistoryStub; + beforeEach(() => { + getExecutionHistoryStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ events: [{ executionFailedEventDetails: 'error' }] })); + }); + + it('should getExecutionHistory with correct params' + , () => serverlessStepFunctions.getExecutionHistory() + .then(() => { + expect(getExecutionHistoryStub.calledOnce).to.be.equal(true); + expect(getExecutionHistoryStub.calledWithExactly( + 'StepFunctions', + 'getExecutionHistory', + { + executionArn: serverlessStepFunctions.executionArn, + }, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#getStateMachineNames()', () => { + let getStateMachineNamesStub; + beforeEach(() => { + serverless.service.stepFunctions = { + helloHello: 'value', + }; + getStateMachineNamesStub = sinon.stub(serverlessStepFunctions.provider, 'request') + .returns(BbPromise.resolve({ Account: 1234 })); + }); + + it('should getStateMachineNames with correct params' + , () => serverlessStepFunctions.getStateMachineNames() + .then(() => { + expect(getStateMachineNamesStub.calledOnce).to.be.equal(true); + expect(getStateMachineNamesStub.calledWithExactly( + 'STS', + 'getCallerIdentity', + {}, + serverlessStepFunctions.options.stage, + serverlessStepFunctions.options.region + )).to.be.equal(true); + + expect(serverlessStepFunctions.stateMachineArns.helloHello).to.be + .equal('arn:aws:states:us-east-1:1234:stateMachine:step-functions-dev-helloHello'); + serverlessStepFunctions.provider.request.restore(); + }) + ); + }); + + describe('#deleteStateMachines()', () => { + let deleteStateMachineStub; + beforeEach(() => { + serverless.service.stepFunctions = { + helloHello: 'value', + hogeHoge: 'value', + }; + deleteStateMachineStub = sinon + .stub(serverlessStepFunctions, 'deleteStateMachine').returns(BbPromise.resolve()); + }); + + it('should deleteStateMachines with correct params' + , () => serverlessStepFunctions.deleteStateMachines() + .then(() => { + expect(deleteStateMachineStub.calledTwice).to.be.equal(true); + serverlessStepFunctions.deleteStateMachine.restore(); + }) + ); + }); + + describe('#createStateMachines()', () => { + let createStateMachineStub; + beforeEach(() => { + serverless.service.stepFunctions = { + helloHello: 'value', + hogeHoge: 'value', + sssss: 'value', + }; + createStateMachineStub = sinon + .stub(serverlessStepFunctions, 'createStateMachine').returns(BbPromise.resolve()); + }); + + it('should createStateMachines with correct params' + , () => serverlessStepFunctions.createStateMachines() + .then(() => { + expect(createStateMachineStub.calledThrice).to.be.equal(true); + serverlessStepFunctions.createStateMachine.restore(); + }) + ); + }); +}); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..209d2c2f --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,10 @@ +'use strict'; +const BbPromise = require('bluebird'); + +module.exports = { + setTimeout() { + return new BbPromise((resolve) => { + setTimeout(resolve, 5000); + }); + }, +}; diff --git a/lib/utils.test.js b/lib/utils.test.js new file mode 100644 index 00000000..d8e3f2d1 --- /dev/null +++ b/lib/utils.test.js @@ -0,0 +1,25 @@ +'use strict'; + +const expect = require('chai').expect; +const sinon = require('sinon'); +const utils = require('./utils'); + +describe('utils', () => { + describe('#setTimeout()', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(new Date(Date.UTC(2016, 9, 1)).getTime()); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should do settimeout', () => { + utils.setTimeout().then((result) => + expect(result).to.be.undefined); + clock.tick(5000); + }); + }); +}); diff --git a/package.json b/package.json index 5b46c96d..77e9af9f 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "serverless-step-functions", "version": "0.2.0", "description": "The module is AWS Step Functions plugin for Serverless Framework", - "main": "index.js", + "main": "lib/index.js", "scripts": { - "test": "istanbul cover -x '*.test.js' node_modules/mocha/bin/_mocha '*.test.js' -- -R spec --recursive", + "test": "istanbul cover -x '**/*.test.js' node_modules/mocha/bin/_mocha '!(node_modules)/**/*.test.js' -- -R spec --recursive", "lint": "eslint ." }, "repository": {