UNPKG

serverless-step-functions

Version:

The module is AWS Step Functions plugin for Serverless Framework

1,418 lines (1,308 loc) 62 kB
'use strict'; const _ = require('lodash'); const expect = require('chai').expect; const Serverless = require('serverless/lib/Serverless'); const CLI = require('serverless/lib/classes/CLI'); const AwsProvider = require('serverless/lib/plugins/aws/provider'); const ServerlessStepFunctions = require('./../../index'); describe('#compileStateMachines', () => { let serverless; let serverlessStepFunctions; beforeEach(() => { serverless = new Serverless(); serverless.cli = new CLI(serverless); serverless.configSchemaHandler = { // eslint-disable-next-line no-unused-vars defineTopLevelProperty: (propertyName, propertySchema) => { }, }; serverless.servicePath = true; serverless.service.service = 'step-functions'; serverless.service.provider.compiledCloudFormationTemplate = { Resources: {}, Outputs: {}, }; serverless.setProvider('aws', new AwsProvider(serverless)); serverlessStepFunctions = new ServerlessStepFunctions(serverless); }); it('should create corresponding resources when definition and name property is given', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition: 'definition1', }, myStateMachine2: { name: 'stateMachineBeta2', definition: 'definition2', }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.DefinitionString).to.equal('"definition1"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.DefinitionString).to.equal('"definition2"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('StateMachineBeta1Role'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('StateMachineBeta2Role'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.DependsOn).to.deep.eq(['StateMachineBeta1Role']); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.DependsOn).to.deep.eq(['StateMachineBeta2Role']); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Outputs .StateMachineBeta1Arn.Value.Ref).to.equal('StateMachineBeta1'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Outputs .StateMachineBeta2Arn.Value.Ref).to.equal('StateMachineBeta2'); }); it('should create corresponding resources when definition property is given and no name', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', }, myStateMachine2: { definition: 'definition2', }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine1StepFunctionsStateMachine.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine2StepFunctionsStateMachine.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine1StepFunctionsStateMachine.Properties.DefinitionString).to.equal('"definition1"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine2StepFunctionsStateMachine.Properties.DefinitionString).to.equal('"definition2"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine1StepFunctionsStateMachine.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('MyStateMachine1StepFunctionsStateMachineRole'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine2StepFunctionsStateMachine.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('MyStateMachine2StepFunctionsStateMachineRole'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine1StepFunctionsStateMachine.DependsOn).to.deep.eq(['MyStateMachine1StepFunctionsStateMachineRole']); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .MyStateMachine2StepFunctionsStateMachine.DependsOn).to.deep.eq(['MyStateMachine2StepFunctionsStateMachineRole']); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Outputs .MyStateMachine1StepFunctionsStateMachineArn.Value.Ref).to.equal('MyStateMachine1StepFunctionsStateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Outputs .MyStateMachine2StepFunctionsStateMachineArn.Value.Ref).to.equal('MyStateMachine2StepFunctionsStateMachine'); }); it('should create named resources when Name is provided', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.DefinitionString).to.equal('"definition1"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.DefinitionString).to.equal('"definition2"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('StateMachineBeta1Role'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('StateMachineBeta2Role'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.DependsOn).to.deep.eq(['StateMachineBeta1Role']); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.DependsOn).to.deep.eq(['StateMachineBeta2Role']); }); it('should create corresponding resources when definition and role property are given', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition: 'definition1', role: 'arn:aws:role1', }, myStateMachine2: { name: 'stateMachineBeta2', definition: 'definition2', role: 'arn:aws:role2', }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.DefinitionString).to.equal('"definition1"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.DefinitionString).to.equal('"definition2"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.RoleArn).to.equal('arn:aws:role1'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.RoleArn).to.equal('arn:aws:role2'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Outputs .StateMachineBeta1Arn.Value.Ref).to.equal('StateMachineBeta1'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Outputs .StateMachineBeta2Arn.Value.Ref).to.equal('StateMachineBeta2'); }); it('should respect CloudFormation intrinsic functions for role property', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineWithIntrinsicRole1', definition: 'definition1\n', role: { 'Fn::GetAtt': ['RoleID', 'Arn'] }, }, myStateMachine2: { name: 'stateMachineWithIntrinsicRole2', definition: 'definition1\n', role: { Ref: 'CloudformationId' }, }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineWithIntrinsicRole1.Properties.RoleArn).to.deep.equal({ 'Fn::GetAtt': ['RoleID', 'Arn'] }); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineWithIntrinsicRole2.Properties.RoleArn).to.deep.equal({ Ref: 'CloudformationId' }); }); it('should throw error if role property is neither string nor intrinsic functions', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineWithIntrinsicRole', definition: 'definition1\n', role: { XXX: ['RoleID', 'Arn'] }, }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should throw error when definition property is not given', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should respect variables if multi-line variables is given', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition: 'definition1\n', }, myStateMachine2: { name: 'stateMachineBeta2', definition: 'definition2\n', }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.DefinitionString).to.equal('"definition1"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.DefinitionString).to.equal('"definition2"'); }); it('should throw error when role property is not given as ARN format', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', role: 'srn:aws:role1', name: 'stateMachineBeta1', }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should throw error when role property is not given as string', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition: 'definition1', role: { 'arn:aws:role1': 'ss' }, }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta2', definition: 'definition1', role: ['arn:aws:role1'], }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should not create corresponding resources when stepfunctions are not given', () => { serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources).to.deep.equal({}); }); it('should not create corresponding resources when stateMachines are not given', () => { serverless.service.stepFunctions = {}; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources).to.deep.equal({}); }); it('should print pretty JSON for the state machine definition', () => { const definition = { Comment: 'Hello World', StartAt: 'HelloWorld', States: { HelloWorld: { Type: 'Task', Resource: 'arn:aws:lambda:us-east-1:1234567890:function:hello', End: true, }, }, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition, }, }, }; serverlessStepFunctions.compileStateMachines(); const actual = serverlessStepFunctions .serverless .service .provider .compiledCloudFormationTemplate .Resources .StateMachineBeta1 .Properties .DefinitionString; expect(actual).to.equal(JSON.stringify(definition, undefined, 2)); }); it('should use raw values for Seconds for Wait task', () => { const definition = { Comment: 'Hello World', StartAt: 'HelloWorld', States: { HelloWorld: { Type: 'Wait', Seconds: { Ref: 'SomeSeconds' }, End: true, }, }, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition, }, }, }; serverlessStepFunctions.compileStateMachines(); const actual = serverlessStepFunctions .serverless .service .provider .compiledCloudFormationTemplate .Resources .StateMachineBeta1 .Properties .DefinitionString; expect(actual['Fn::Sub'][0]).to.equal(` { "Comment": "Hello World", "StartAt": "HelloWorld", "States": { "HelloWorld": { "Type": "Wait", "Seconds": \${4dfb8832166d083d5c26a32fbfcaebf9}, "End": true } } } `.trim()); }); it('should not use raw values for Seconds in other task', () => { const definition = { Comment: 'Hello World', StartAt: 'HelloWorld', States: { HelloWorld: { Type: 'Task', Resource: 'arn:aws:states:::glue:startJobRun.sync', Parameters: { Seconds: { Ref: 'SomeSeconds' }, }, TimeoutSecondsPath: '$.params.maxTime', HeartbeatSecondsPath: '$.params.heartbeat', End: true, }, }, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { name: 'stateMachineBeta1', definition, }, }, }; serverlessStepFunctions.compileStateMachines(); const actual = serverlessStepFunctions .serverless .service .provider .compiledCloudFormationTemplate .Resources .StateMachineBeta1 .Properties .DefinitionString; expect(actual['Fn::Sub'][0]).to.equal(` { "Comment": "Hello World", "StartAt": "HelloWorld", "States": { "HelloWorld": { "Type": "Task", "Resource": "arn:aws:states:::glue:startJobRun.sync", "Parameters": { "Seconds": "\${4dfb8832166d083d5c26a32fbfcaebf9}" }, "TimeoutSecondsPath": "$.params.maxTime", "HeartbeatSecondsPath": "$.params.heartbeat", "End": true } } } `.trim()); }); it('should add dependsOn resources', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', dependsOn: 'DynamoDBTable', }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', dependsOn: [ 'DynamoDBTable', 'KinesisStream', ], }, }, }; serverlessStepFunctions.compileStateMachines(); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Type).to.equal('AWS::StepFunctions::StateMachine'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.DefinitionString).to.equal('"definition1"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.DefinitionString).to.equal('"definition2"'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('StateMachineBeta1Role'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.Properties.RoleArn['Fn::GetAtt'][0]).to.equal('StateMachineBeta2Role'); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1.DependsOn).to.deep.eq(['StateMachineBeta1Role', 'DynamoDBTable']); expect(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2.DependsOn).to.deep.eq(['StateMachineBeta2Role', 'DynamoDBTable', 'KinesisStream']); }); it('should throw error when dependsOn property is neither string nor [string]', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', dependsOn: { Ref: 'ss' }, }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', dependsOn: [{ Ref: 'ss' }], }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should add tags', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', tags: { team: 'core', score: 42, }, }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', tags: { team: 'core', score: 42, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachineBeta1 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1; const stateMachineBeta2 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2; expect(stateMachineBeta1.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta2.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta1.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); expect(stateMachineBeta2.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); }); it('should add global tags', () => { serverless.service.provider.tags = { team: 'core', score: 42, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachineBeta1 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1; const stateMachineBeta2 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2; expect(stateMachineBeta1.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta2.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta1.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); expect(stateMachineBeta2.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); }); it('should merge global and state machine tags', () => { serverless.service.provider.tags = { team: 'core', }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', tags: { score: 42, }, }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', tags: { score: 42, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachineBeta1 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1; const stateMachineBeta2 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2; expect(stateMachineBeta1.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta2.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta1.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); expect(stateMachineBeta2.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); }); it('should not add tags property to state machine if none are provided', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachineBeta1 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1; expect(stateMachineBeta1.Properties).to.not.have.property('Tags'); }); it('should not inherit global tags if inheritGlobalTags is set to false', () => { serverless.service.provider.tags = { team: 'core', }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', inheritGlobalTags: false, tags: { score: 42, }, }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', tags: { score: 42, }, }, myStateMachine3: { definition: 'definition3', name: 'stateMachineBeta3', inheritGlobalTags: true, tags: { question: 'Meaning of life', answer: 42, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachineBeta1 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1; const stateMachineBeta2 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2; const stateMachineBeta3 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta3; expect(stateMachineBeta1.Properties.Tags).to.have.lengthOf(1); expect(stateMachineBeta2.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta3.Properties.Tags).to.have.lengthOf(3); expect(stateMachineBeta1.Properties.Tags) .to.deep.eq([{ Key: 'score', Value: '42' }]); expect(stateMachineBeta2.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'score', Value: '42' }]); expect(stateMachineBeta3.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: 'core' }, { Key: 'question', Value: 'Meaning of life' }, { Key: 'answer', Value: '42' }]); }); it('should throw error when tags property contains malformed tags', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', tags: ['team:core'], }, }, }; expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should leave empty tag values as empty string', () => { serverless.service.provider.tags = { team: undefined, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: 'definition1', name: 'stateMachineBeta1', tags: { score: undefined, }, }, myStateMachine2: { definition: 'definition2', name: 'stateMachineBeta2', tags: { score: undefined, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachineBeta1 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta1; const stateMachineBeta2 = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachineBeta2; expect(stateMachineBeta1.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta2.Properties.Tags).to.have.lengthOf(2); expect(stateMachineBeta1.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: '' }, { Key: 'score', Value: '' }]); expect(stateMachineBeta2.Properties.Tags) .to.deep.eq([{ Key: 'team', Value: '' }, { Key: 'score', Value: '' }]); }); it('should respect CloudFormation intrinsic functions', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine: { name: 'stateMachine', definition: { StartAt: 'Lambda', States: { Lambda: { Type: 'Task', Resource: { Ref: 'MyFunction', }, Next: 'Sns', }, Sns: { Type: 'Task', Resource: 'arn:aws:states:::sns:publish', Parameters: { Message: { 'Fn::GetAtt': ['MyTopic', 'TopicName'], }, TopicArn: { Ref: 'MyTopic', }, }, Next: 'Sqs', }, Sqs: { Type: 'Task', Resource: 'arn:aws:states:::sqs:sendMessage', Parameters: { QueueUrl: { Ref: 'MyQueue', }, MessageBody: 'This is a static message', }, Next: 'Fargate', }, Fargate: { Type: 'Task', Resource: 'arn:aws:states:::ecs:runTask.waitForTaskToken', Parameters: { LaunchType: 'FARGATE', Cluster: { Ref: 'ActivityCluster', }, NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'ENABLED', SecurityGroups: [{ Ref: 'ActivitySecurityGroup', }], Subnets: [{ Ref: 'ActivitySubnet', }], }, }, }, Next: 'Parallel', }, Parallel: { Type: 'Parallel', End: true, Branches: [ { StartAt: 'Lambda2', States: { Lambda2: { Type: 'Task', Resource: { Ref: 'MyFunction2', }, End: true, }, }, }, ], }, }, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachine; expect(stateMachine.Properties.DefinitionString).to.haveOwnProperty('Fn::Sub'); expect(stateMachine.Properties.DefinitionString['Fn::Sub']).to.have.lengthOf(2); const [json, params] = stateMachine.Properties.DefinitionString['Fn::Sub']; const modifiedDefinition = JSON.parse(json); const hasParam = (state, path, expected) => { const attr = _.get(state, path); expect(attr.startsWith('${')).to.eq(true); const paramName = attr.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(paramName); expect(params[paramName]).to.eql(expected); }; hasParam(modifiedDefinition.States.Lambda, 'Resource', { Ref: 'MyFunction', }); hasParam(modifiedDefinition.States.Sns, 'Parameters.Message', { 'Fn::GetAtt': ['MyTopic', 'TopicName'], }); hasParam(modifiedDefinition.States.Sns, 'Parameters.TopicArn', { Ref: 'MyTopic', }); hasParam(modifiedDefinition.States.Sqs, 'Parameters.QueueUrl', { Ref: 'MyQueue', }); hasParam(modifiedDefinition.States.Fargate, 'Parameters.Cluster', { Ref: 'ActivityCluster', }); hasParam(modifiedDefinition.States.Fargate, 'Parameters.NetworkConfiguration.AwsvpcConfiguration.SecurityGroups.0', { Ref: 'ActivitySecurityGroup', }); hasParam(modifiedDefinition.States.Fargate, 'Parameters.NetworkConfiguration.AwsvpcConfiguration.Subnets.0', { Ref: 'ActivitySubnet', }); hasParam(modifiedDefinition.States.Parallel, 'Branches.0.States.Lambda2.Resource', { Ref: 'MyFunction2', }); }); it('should do deterministic compilcation', () => { const definition = { stateMachines: { myStateMachine: { name: 'stateMachine', definition: { StartAt: 'LambdaA', States: { LambdaA: { Type: 'Task', Resource: { Ref: 'MyFunction', }, Next: 'LambdaB', }, LambdaB: { Type: 'Task', Resource: { Ref: 'MyFunction2', }, Next: 'Parallel', }, Parallel: { Type: 'Parallel', End: true, Branches: [ { StartAt: 'Lambda2', States: { Lambda2: { Type: 'Task', Resource: { Ref: 'MyFunction', }, End: true, }, }, }, ], }, }, }, }, }, }; serverless.service.stepFunctions = _.cloneDeep(definition); serverlessStepFunctions.compileStateMachines(); const stateMachine1 = _.cloneDeep(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachine); serverless.service.stepFunctions = _.cloneDeep(definition); serverlessStepFunctions.compileStateMachines(); const stateMachine2 = _.cloneDeep(serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .StateMachine); expect(stateMachine1).to.deep.equal(stateMachine2); }); it('should allow null values #193', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'Test', name: 'test', definition: { StartAt: 'AnyStep', States: { AnyStep: { Type: 'Pass', ResultPath: null, Next: 'Finish', }, Finish: { Type: 'Succeed', }, }, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; expect(stateMachine.Properties.DefinitionString).to.not.equal(null); }); it('should not interpret states starting with "Ref" as intrinsic functions #203', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'Test', name: 'test', definition: { StartAt: 'One', States: { One: { Type: 'Wait', Seconds: 10, Next: 'RefreshLead', }, RefreshLead: { Type: 'Task', Resource: 'arn:aws:lambda:us-east-1:12345:function:test-dev-lambda', TimeoutSeconds: 60, Next: 'EndState', }, EndState: { Type: 'Succeed', }, }, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; expect(stateMachine.Properties.DefinitionString).to.not.haveOwnProperty('Fn::Sub'); const stateMachineObj = JSON.parse(stateMachine.Properties.DefinitionString); expect(stateMachineObj.States).to.haveOwnProperty('One'); expect(stateMachineObj.States).to.haveOwnProperty('RefreshLead'); expect(stateMachineObj.States).to.haveOwnProperty('EndState'); }); it('should support local function names', () => { serverless.service.functions = { 'hello-world': { handler: 'hello-world.handler', }, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'Test', definition: { StartAt: 'Lambda', States: { Lambda: { Type: 'Task', Resource: { 'Fn::GetAtt': ['hello-world', 'Arn'], }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; expect(stateMachine.Properties.DefinitionString).to.haveOwnProperty('Fn::Sub'); expect(stateMachine.Properties.DefinitionString['Fn::Sub']).to.have.lengthOf(2); const [json, params] = stateMachine.Properties.DefinitionString['Fn::Sub']; const modifiedDefinition = JSON.parse(json); const lambda = modifiedDefinition.States.Lambda; expect(lambda.Resource.startsWith('${')).to.eq(true); const functionParam = lambda.Resource.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(functionParam); const refParam = params[functionParam]; expect(refParam).to.eql({ 'Fn::GetAtt': ['HelloDashworldLambdaFunction', 'Arn'] }); }); it('should support local function names for lambda::invoke resource type', () => { serverless.service.functions = { 'hello-world': { handler: 'hello-world.handler', }, }; serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'Test', definition: { StartAt: 'Lambda1', States: { Lambda1: { Type: 'Task', Resource: 'arn:aws:states:::lambda:invoke', Parameters: { FunctionName: { Ref: 'hello-world', }, Payload: { 'ExecutionName.$': '$$.Execution.Name', }, }, Next: 'Lambda2', }, Lambda2: { Type: 'Task', Resource: 'arn:aws:states:::lambda:invoke', Parameters: { FunctionName: { 'Fn::GetAtt': ['hello-world', 'Arn'], }, Payload: { 'ExecutionName.$': '$$.Execution.Name', }, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; expect(stateMachine.Properties.DefinitionString).to.haveOwnProperty('Fn::Sub'); expect(stateMachine.Properties.DefinitionString['Fn::Sub']).to.have.lengthOf(2); const [json, params] = stateMachine.Properties.DefinitionString['Fn::Sub']; const modifiedDefinition = JSON.parse(json); const lambda1 = modifiedDefinition.States.Lambda1; expect(lambda1.Parameters.FunctionName.startsWith('${')).to.eq(true); const lambda1ParamName = lambda1.Parameters.FunctionName.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(lambda1ParamName); const lambda1Param = params[lambda1ParamName]; expect(lambda1Param).to.eql({ Ref: 'HelloDashworldLambdaFunction' }); const lambda2 = modifiedDefinition.States.Lambda2; expect(lambda2.Parameters.FunctionName.startsWith('${')).to.eq(true); const lambda2ParamName = lambda2.Parameters.FunctionName.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(lambda2ParamName); const lambda2Param = params[lambda2ParamName]; expect(lambda2Param).to.eql({ 'Fn::GetAtt': ['HelloDashworldLambdaFunction', 'Arn'] }); }); describe('#useExactVersions', () => { beforeEach(() => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'Test', useExactVersion: true, definition: { StartAt: 'Lambda1', States: { Lambda1: { Type: 'Task', Resource: 'arn:aws:states:::lambda:invoke', Parameters: { FunctionName: { Ref: 'HelloLambdaFunction', }, Payload: { 'ExecutionName.$': '$$.Execution.Name', }, }, Next: 'Lambda2', }, Lambda2: { Type: 'Task', Resource: { 'Fn::GetAtt': ['WorldLambdaFunction', 'Arn'], }, End: true, }, }, }, }, }, }; serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .HelloLambdaFunction = { Type: 'AWS::Lambda::Function', }; serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .WorldLambdaFunction = { Type: 'AWS::Lambda::Function', }; }); const compileStateMachines = () => { serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; expect(stateMachine.Properties.DefinitionString).to.haveOwnProperty('Fn::Sub'); expect(stateMachine.Properties.DefinitionString['Fn::Sub']).to.have.lengthOf(2); const [json, params] = stateMachine.Properties.DefinitionString['Fn::Sub']; const modifiedDefinition = JSON.parse(json); const lambda1 = modifiedDefinition.States.Lambda1; expect(lambda1.Parameters.FunctionName.startsWith('${')).to.eq(true); const lambda1ParamName = lambda1.Parameters.FunctionName.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(lambda1ParamName); const lambda1Param = params[lambda1ParamName]; const lambda2 = modifiedDefinition.States.Lambda2; expect(lambda2.Resource.startsWith('${')).to.eq(true); const lambda2ParamName = lambda2.Resource.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(lambda2ParamName); const lambda2Param = params[lambda2ParamName]; return { lambda1Param, lambda2Param }; }; it('should change refs to lambda version when useExactVersion is true', () => { serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Lambda1Version13579 = { Type: 'AWS::Lambda::Version', Properties: { FunctionName: { Ref: 'HelloLambdaFunction', }, }, }; serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Lambda2Version24680 = { Type: 'AWS::Lambda::Version', Properties: { FunctionName: { Ref: 'WorldLambdaFunction', }, }, }; const { lambda1Param, lambda2Param } = compileStateMachines(); expect(lambda1Param).to.eql({ Ref: 'Lambda1Version13579' }); expect(lambda2Param).to.eql({ Ref: 'Lambda2Version24680' }); }); it('should not change refs to lambda version if version is not found, even if useExactVersion is true', () => { const { lambda1Param, lambda2Param } = compileStateMachines(); expect(lambda1Param).to.eql({ Ref: 'HelloLambdaFunction' }); expect(lambda2Param).to.eql({ 'Fn::GetAtt': ['WorldLambdaFunction', 'Arn'] }); }); it('should not change refs to lambda version if not using intrinsic functions, even if useExactVersion is true', () => { const states = serverless.service.stepFunctions .stateMachines.myStateMachine1.definition.States; states.Lambda1.Parameters.FunctionName = 'hello'; states.Lambda2.Resource = 'arn:aws:lambda:us-east-1:1234567890:function:world'; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; const definition = JSON.parse(stateMachine.Properties.DefinitionString); expect(definition.States.Lambda1.Parameters.FunctionName).to.equal('hello'); expect(definition.States.Lambda2.Resource) .to.equal('arn:aws:lambda:us-east-1:1234567890:function:world'); }); it('should do nothing if there are no ref to lambda functions, even if useExactVersion is true', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { id: 'Test', useExactVersion: true, definition: { StartAt: 'Sns', States: { Sns: { Type: 'Task', Resource: 'arn:aws:states:::sns:publish', Parameters: { Message: { 'Fn::GetAtt': ['MyTopic', 'TopicName'], }, TopicArn: { Ref: 'MyTopic', }, }, End: true, }, }, }, }, }, }; serverlessStepFunctions.compileStateMachines(); const stateMachine = serverlessStepFunctions.serverless.service .provider.compiledCloudFormationTemplate.Resources .Test; expect(stateMachine.Properties.DefinitionString).to.haveOwnProperty('Fn::Sub'); expect(stateMachine.Properties.DefinitionString['Fn::Sub']).to.have.lengthOf(2); const [json, params] = stateMachine.Properties.DefinitionString['Fn::Sub']; const modifiedDefinition = JSON.parse(json); const sns = modifiedDefinition.States.Sns; expect(sns.Parameters.TopicArn.startsWith('${')).to.eq(true); const topicArnParam = sns.Parameters.TopicArn.replace(/[${}]/g, ''); expect(params).to.haveOwnProperty(topicArnParam); const topicArn = params[topicArnParam]; expect(topicArn).to.deep.equal({ Ref: 'MyTopic' }); }); }); it('should not validate definition if not enabled', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: { StartAt: 'Start', States: { Start: { Type: 'Inexistant type', End: true, }, }, }, }, }, validate: false, }; // Definition is invalid, but should succeed because validate=false serverlessStepFunctions.compileStateMachines(); }); it('should validate definition and pass', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: { StartAt: 'GetAttResource', States: { GetAttResource: { Type: 'Task', Resource: { 'Fn::GetAtt': [ 'lambda-name_GetAtt', 'Arn', ], }, Next: 'RefResource', }, RefResource: { Type: 'Task', Resource: { Ref: 'lambda-name_Ref', }, Next: 'ArnResource', }, ArnResource: { Type: 'Task', Resource: 'arn:aws:lambda:region-1:1234567890:function:lambda-name_Arn', End: true, }, }, }, }, }, validate: true, }; // Definition is valid, should succeed serverlessStepFunctions.compileStateMachines(); }); it('should validate definition and fail', () => { serverless.service.stepFunctions = { stateMachines: { myStateMachine1: { definition: { StartAt: 'Start', States: { Start: { Type: 'Inexistant type', End: true, }, }, }, }, }, validate: true, }; // Definition is invalid and validate=true, should throw expect(() => serverlessStepFunctions.compileStateMachines()).to.throw(Error); }); it('should replace pseudo parameters that starts with #