From c2234066c437c8dee547e70a2b2bf2ddd298852c Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 15 Apr 2020 00:29:54 -0700 Subject: [PATCH 1/2] feat(kinesis): add `grant` API to IStream to add permissions to a Stream (#7354) The `grant` API enables the capability to provide permissions on a stream to an IAM principal. Context: `DescribeStream` permissions are redundant with `DescribeStreamSummary` and read operations do not need permissions to both. However, there are cases (i.e. Lambda::EventSourceMapping ) where the presence of the permission is being validated when creating the resource. This change exposes the `grant` API on an IStream so that the necessary permissions can be provided with an extra API call BREAKING CHANGE: `grantRead()` API no longer provides permissions to `kinesis:DescribeStream` as it provides permissions to `kinesis:DescribeStreamSummary` and `kinesis:SubscribeToShard` in it's place. If it's still desired, it can be added through the `grant()` API on the stream. --- packages/@aws-cdk/aws-kinesis/README.md | 15 +++- packages/@aws-cdk/aws-kinesis/lib/stream.ts | 6 +- .../test/integ.stream.expected.json | 1 - .../@aws-cdk/aws-kinesis/test/stream.test.ts | 86 +++++++++++++++++-- .../aws-lambda-event-sources/lib/kinesis.ts | 7 ++ .../test/integ.kinesis.expected.json | 11 ++- .../test/integ.kinesiswithdlq.expected.json | 11 ++- .../test/test.kinesis.ts | 11 ++- 8 files changed, 137 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/aws-kinesis/README.md b/packages/@aws-cdk/aws-kinesis/README.md index 4f6f121ee7186..c3914e014564b 100644 --- a/packages/@aws-cdk/aws-kinesis/README.md +++ b/packages/@aws-cdk/aws-kinesis/README.md @@ -25,6 +25,7 @@ intake and aggregation. - [Permission Grants](#permission-grants) - [Read Permissions](#read-permissions) - [Write Permissions](#write-permissions) + - [Custom Permissions](#custom-permissions) ## Streams @@ -150,7 +151,6 @@ stream.grantRead(lambdaRole); The following read permissions are provided to a service principal by the `grantRead()` API: -- `kinesis:DescribeStream` - `kinesis:DescribeStreamSummary` - `kinesis:GetRecords` - `kinesis:GetShardIterator` @@ -181,3 +181,16 @@ The following write permissions are provided to a service principal by the `gran - `kinesis:ListShards` - `kinesis:PutRecord` - `kinesis:PutRecords` + +#### Custom Permissions + +You can add any set of permissions to a stream by calling the `grant()` API. + +```ts +const user = new iam.User(stack, 'MyUser'); + +const stream = new Stream(stack, 'MyStream'); + +// give my user permissions to list shards +stream.grant(user, 'kinesis:ListShards'); +``` diff --git a/packages/@aws-cdk/aws-kinesis/lib/stream.ts b/packages/@aws-cdk/aws-kinesis/lib/stream.ts index 197f09e0aa4a8..57ade70fada6f 100644 --- a/packages/@aws-cdk/aws-kinesis/lib/stream.ts +++ b/packages/@aws-cdk/aws-kinesis/lib/stream.ts @@ -5,7 +5,6 @@ import { IResolvable } from 'constructs'; import { CfnStream } from './kinesis.generated'; const READ_OPERATIONS = [ - 'kinesis:DescribeStream', 'kinesis:DescribeStreamSummary', 'kinesis:GetRecords', 'kinesis:GetShardIterator', @@ -68,6 +67,11 @@ export interface IStream extends IResource { * encrypt/decrypt will also be granted. */ grantReadWrite(grantee: iam.IGrantable): iam.Grant; + + /** + * Grant the indicated permissions on this stream to the provided IAM principal. + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; } /** diff --git a/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json b/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json index 9a9921057f295..15055271413a2 100644 --- a/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json +++ b/packages/@aws-cdk/aws-kinesis/test/integ.stream.expected.json @@ -39,7 +39,6 @@ "Statement": [ { "Action": [ - "kinesis:DescribeStream", "kinesis:DescribeStreamSummary", "kinesis:GetRecords", "kinesis:GetShardIterator", diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index d5427260f9727..40bbe5ea8097b 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -583,7 +583,6 @@ describe('Kinesis data streams', () => { Statement: [ { Action: [ - 'kinesis:DescribeStream', 'kinesis:DescribeStreamSummary', 'kinesis:GetRecords', 'kinesis:GetShardIterator', @@ -836,7 +835,6 @@ describe('Kinesis data streams', () => { Statement: [ { Action: [ - 'kinesis:DescribeStream', 'kinesis:DescribeStreamSummary', 'kinesis:GetRecords', 'kinesis:GetShardIterator', @@ -910,7 +908,6 @@ describe('Kinesis data streams', () => { Statement: [ { Action: [ - 'kinesis:DescribeStream', 'kinesis:DescribeStreamSummary', 'kinesis:GetRecords', 'kinesis:GetShardIterator', @@ -1039,7 +1036,7 @@ describe('Kinesis data streams', () => { }); }), - test('greatReadWrite creates and attaches a policy with write only access to Stream', () => { + test('grantReadWrite creates and attaches a policy with write only access to Stream', () => { const stack = new Stack(); const stream = new Stream(stack, 'MyStream'); @@ -1077,7 +1074,6 @@ describe('Kinesis data streams', () => { Statement: [ { Action: [ - 'kinesis:DescribeStream', 'kinesis:DescribeStreamSummary', 'kinesis:GetRecords', 'kinesis:GetShardIterator', @@ -1128,6 +1124,86 @@ describe('Kinesis data streams', () => { }); }), + test('grant creates and attaches a policy to Stream which includes supplied permissions', () => { + const stack = new Stack(); + const stream = new Stream(stack, 'MyStream'); + + const user = new iam.User(stack, 'MyUser'); + stream.grant(user, 'kinesis:DescribeStream'); + + expect(stack).toMatchTemplate({ + Resources: { + MyStream5C050E93: { + Type: 'AWS::Kinesis::Stream', + Properties: { + ShardCount: 1, + RetentionPeriodHours: 24, + StreamEncryption: { + 'Fn::If': [ + 'AwsCdkKinesisEncryptedStreamsUnsupportedRegions', + { + Ref: 'AWS::NoValue', + }, + { + EncryptionType: 'KMS', + KeyId: 'alias/aws/kinesis', + }, + ], + }, + }, + }, + MyUserDC45028B: { + Type: 'AWS::IAM::User', + }, + MyUserDefaultPolicy7B897426: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [ + { + Action: 'kinesis:DescribeStream', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': ['MyStream5C050E93', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyUserDefaultPolicy7B897426', + Users: [ + { + Ref: 'MyUserDC45028B', + }, + ], + }, + }, + }, + Conditions: { + AwsCdkKinesisEncryptedStreamsUnsupportedRegions: { + 'Fn::Or': [ + { + 'Fn::Equals': [ + { + Ref: 'AWS::Region', + }, + 'cn-north-1', + ], + }, + { + 'Fn::Equals': [ + { + Ref: 'AWS::Region', + }, + 'cn-northwest-1', + ], + }, + ], + }, + }, + }); + }), + test('cross-stack permissions - no encryption', () => { const app = new App(); const stackA = new Stack(app, 'stackA'); diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts index f7309681f3767..7034f9d0e5a3c 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts @@ -26,6 +26,13 @@ export class KinesisEventSource extends StreamEventSource { this._eventSourceMappingId = eventSourceMapping.eventSourceMappingId; this.stream.grantRead(target); + + // The `grantRead` API provides all the permissions recommended by the Kinesis team for reading a stream. + // `DescribeStream` permissions are not required to read a stream as it's covered by the `DescribeStreamSummary` + // and `SubscribeToShard` APIs. + // The Lambda::EventSourceMapping resource validates against the `DescribeStream` permission. So we add it explicitly. + // FIXME This permission can be removed when the event source mapping resource drops it from validation. + this.stream.grant(target, 'kinesis:DescribeStream'); } /** diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json index 80e11a45e9dea..670d7d05ced59 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json @@ -38,7 +38,6 @@ "Statement": [ { "Action": [ - "kinesis:DescribeStream", "kinesis:DescribeStreamSummary", "kinesis:GetRecords", "kinesis:GetShardIterator", @@ -52,6 +51,16 @@ "Arn" ] } + }, + { + "Action": "kinesis:DescribeStream", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Q63C6E3AB", + "Arn" + ] + } } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json index 00fedc03e9077..135d1ca0514a2 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesiswithdlq.expected.json @@ -52,7 +52,6 @@ }, { "Action": [ - "kinesis:DescribeStream", "kinesis:DescribeStreamSummary", "kinesis:GetRecords", "kinesis:GetShardIterator", @@ -66,6 +65,16 @@ "Arn" ] } + }, + { + "Action": "kinesis:DescribeStream", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "S509448A1", + "Arn" + ] + } } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts index c62af36d09249..12e80ae35f0ba 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.kinesis.ts @@ -26,7 +26,6 @@ export = { 'Statement': [ { 'Action': [ - 'kinesis:DescribeStream', 'kinesis:DescribeStreamSummary', 'kinesis:GetRecords', 'kinesis:GetShardIterator', @@ -40,6 +39,16 @@ export = { 'Arn' ] } + }, + { + 'Action': 'kinesis:DescribeStream', + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'S509448A1', + 'Arn' + ] + } } ], 'Version': '2012-10-17' From 7695f2bd3a6c5bd72f973cbb026bd13a235a2c95 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Wed, 15 Apr 2020 03:07:09 -0700 Subject: [PATCH 2/2] chore(stepfunctions): convert tests to jest (#7351) I have not modified any of the tests in this PR and kept the scope to simply converting to jest. Also added an override for the `coverageThreshold` as it's just a touch below (currently at `78`) our configured global of `80`. Hopefully we can get rid of that block soon as we start adding more tests in this module. --- .../@aws-cdk/aws-stepfunctions/package.json | 10 +- .../{test.activity.ts => activity.test.ts} | 27 +- .../aws-stepfunctions/test/condition.test.ts | 45 ++ .../test/{test.fail.ts => fail.test.ts} | 12 +- .../aws-stepfunctions/test/fields.test.ts | 129 +++ .../test/{test.map.ts => map.test.ts} | 107 ++- .../{test.parallel.ts => parallel.test.ts} | 14 +- .../aws-stepfunctions/test/pass.test.ts | 34 + ...ces.ts => state-machine-resources.test.ts} | 81 +- ...state-machine.ts => state-machine.test.ts} | 48 +- .../test/states-language.test.ts | 668 ++++++++++++++++ .../aws-stepfunctions/test/test.condition.ts | 79 -- .../aws-stepfunctions/test/test.fields.ts | 153 ---- .../aws-stepfunctions/test/test.pass.ts | 40 - .../test/test.states-language.ts | 735 ------------------ 15 files changed, 1005 insertions(+), 1177 deletions(-) rename packages/@aws-cdk/aws-stepfunctions/test/{test.activity.ts => activity.test.ts} (62%) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts rename packages/@aws-cdk/aws-stepfunctions/test/{test.fail.ts => fail.test.ts} (59%) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts rename packages/@aws-cdk/aws-stepfunctions/test/{test.map.ts => map.test.ts} (55%) rename packages/@aws-cdk/aws-stepfunctions/test/{test.parallel.ts => parallel.test.ts} (83%) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts rename packages/@aws-cdk/aws-stepfunctions/test/{test.state-machine-resources.ts => state-machine-resources.test.ts} (82%) rename packages/@aws-cdk/aws-stepfunctions/test/{test.state-machine.ts => state-machine.test.ts} (79%) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts delete mode 100644 packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 4bbead1ec9e84..47a7e88e9e1cc 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -63,11 +63,9 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, "dependencies": { @@ -87,6 +85,14 @@ "@aws-cdk/core": "0.0.0", "constructs": "^2.0.0" }, + "jest": { + "coverageThreshold": { + "global": { + "branches": 75, + "statements": 80 + } + } + }, "engines": { "node": ">= 10.12.0" }, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts similarity index 62% rename from packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts rename to packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts index 34439fbac3760..05c74569e3760 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/activity.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'instantiate Activity'(test: Test) { +describe('Activity', () => { + test('instantiate Activity', () => { // GIVEN const stack = new cdk.Stack(); @@ -12,14 +11,12 @@ export = { new stepfunctions.Activity(stack, 'Activity'); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::Activity', { + expect(stack).toHaveResource('AWS::StepFunctions::Activity', { Name: 'Activity' - })); - - test.done(); - }, + }); + }); - 'Activity exposes metrics'(test: Test) { + test('Activity exposes metrics', () => { // GIVEN const stack = new cdk.Stack(); @@ -32,18 +29,16 @@ export = { namespace: 'AWS/States', dimensions: { ActivityArn: { Ref: 'Activity04690B0A' }}, }; - test.deepEqual(stack.resolve(activity.metricRunTime()), { + expect((stack.resolve(activity.metricRunTime()))).toEqual({ ...sharedMetric, metricName: 'ActivityRunTime', statistic: 'Average' }); - test.deepEqual(stack.resolve(activity.metricFailed()), { + expect(stack.resolve(activity.metricFailed())).toEqual({ ...sharedMetric, metricName: 'ActivitiesFailed', statistic: 'Sum' }); - - test.done(); - } -}; + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts new file mode 100644 index 0000000000000..084b6f28857f9 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/condition.test.ts @@ -0,0 +1,45 @@ +import '@aws-cdk/assert/jest'; +import * as stepfunctions from '../lib'; + +describe('Condition Variables', () => { + test('Condition variables must start with $. or $[', () => { + expect(() => stepfunctions.Condition.stringEquals('a', 'b')).toThrow(); + }), + test('Condition variables can start with $.', () => { + expect(() => stepfunctions.Condition.stringEquals('$.a', 'b')).not.toThrow(); + }), + test('Condition variables can start with $[', () => { + expect(() => stepfunctions.Condition.stringEquals('$[0]', 'a')).not.toThrow(); + }), + test('NotConditon must render properly', () => { + assertRendersTo(stepfunctions.Condition.not(stepfunctions.Condition.stringEquals('$.a', 'b')), { Not: { Variable: '$.a', StringEquals: 'b' } }); + }), + test('CompoundCondition must render properly', () => { + assertRendersTo( + stepfunctions.Condition.and(stepfunctions.Condition.booleanEquals('$.a', true), stepfunctions.Condition.numberGreaterThan('$.b', 3)), + { + And: [ + { Variable: '$.a', BooleanEquals: true }, + { Variable: '$.b', NumericGreaterThan: 3 }, + ], + } + ); + }), + test('Exercise a number of other conditions', () => { + const cases: Array<[stepfunctions.Condition, object]> = [ + [stepfunctions.Condition.stringLessThan('$.a', 'foo'), { Variable: '$.a', StringLessThan: 'foo' }], + [stepfunctions.Condition.stringLessThanEquals('$.a', 'foo'), { Variable: '$.a', StringLessThanEquals: 'foo' }], + [stepfunctions.Condition.stringGreaterThan('$.a', 'foo'), { Variable: '$.a', StringGreaterThan: 'foo' }], + [stepfunctions.Condition.stringGreaterThanEquals('$.a', 'foo'), { Variable: '$.a', StringGreaterThanEquals: 'foo' }], + [stepfunctions.Condition.numberEquals('$.a', 5), { Variable: '$.a', NumericEquals: 5 }], + ]; + + for (const [cond, expected] of cases) { + assertRendersTo(cond, expected); + } + }); +}); + +function assertRendersTo(cond: stepfunctions.Condition, expected: any) { + expect(cond.renderCondition()).toStrictEqual(expected); +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.fail.ts b/packages/@aws-cdk/aws-stepfunctions/test/fail.test.ts similarity index 59% rename from packages/@aws-cdk/aws-stepfunctions/test/test.fail.ts rename to packages/@aws-cdk/aws-stepfunctions/test/fail.test.ts index b37d9d77ea488..4666a7a31adf1 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.fail.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/fail.test.ts @@ -1,13 +1,11 @@ +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'Props are optional'(test: Test) { +describe('Fail State', () => { + test('Props are optional', () => { const stack = new cdk.Stack(); new stepfunctions.Fail(stack, 'Fail'); - - test.done(); - } -}; + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts new file mode 100644 index 0000000000000..989b6046f8cf0 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts @@ -0,0 +1,129 @@ +import '@aws-cdk/assert/jest'; +import { Context, Data, FieldUtils } from '../lib'; + +describe('Fields', () => { + test('deep replace correctly handles fields in arrays', () => { + expect( + FieldUtils.renderObject({ + unknown: undefined, + bool: true, + literal: 'literal', + field: Data.stringAt('$.stringField'), + listField: Data.listAt('$.listField'), + deep: [ + 'literal', + { + deepField: Data.numberAt('$.numField'), + }, + ], + }) + ).toStrictEqual({ + 'bool': true, + 'literal': 'literal', + 'field.$': '$.stringField', + 'listField.$': '$.listField', + 'deep': [ + 'literal', + { + 'deepField.$': '$.numField', + }, + ], + }); + }), + test('exercise contextpaths', () => { + expect( + FieldUtils.renderObject({ + str: Context.stringAt('$$.Execution.StartTime'), + count: Context.numberAt('$$.State.RetryCount'), + token: Context.taskToken, + entire: Context.entireContext, + }) + ).toStrictEqual({ + 'str.$': '$$.Execution.StartTime', + 'count.$': '$$.State.RetryCount', + 'token.$': '$$.Task.Token', + 'entire.$': '$$', + }); + }), + test('find all referenced paths', () => { + expect( + FieldUtils.findReferencedPaths({ + bool: false, + literal: 'literal', + field: Data.stringAt('$.stringField'), + listField: Data.listAt('$.listField'), + deep: [ + 'literal', + { + field: Data.stringAt('$.stringField'), + deepField: Data.numberAt('$.numField'), + }, + ], + }) + ).toStrictEqual(['$.listField', '$.numField', '$.stringField']); + }), + test('cannot have JsonPath fields in arrays', () => { + expect(() => FieldUtils.renderObject({ + deep: [Data.stringAt('$.hello')], + })).toThrowError(/Cannot use JsonPath fields in an array/); + }), + test('datafield path must be correct', () => { + expect(Data.stringAt('$')).toBeDefined(); + + expect(() => Data.stringAt('$hello')).toThrowError(/exactly equal to '\$' or start with '\$.'/); + + expect(() => Data.stringAt('hello')).toThrowError(/exactly equal to '\$' or start with '\$.'/); + }), + test('context path must be correct', () => { + expect(Context.stringAt('$$')).toBeDefined(); + + expect(() => Context.stringAt('$$hello')).toThrowError(/exactly equal to '\$\$' or start with '\$\$.'/); + + expect(() => Context.stringAt('hello')).toThrowError(/exactly equal to '\$\$' or start with '\$\$.'/); + }), + test('test contains task token', () => { + expect(true).toEqual( + FieldUtils.containsTaskToken({ + field: Context.taskToken, + }) + ); + + expect(true).toEqual( + FieldUtils.containsTaskToken({ + field: Context.stringAt('$$.Task'), + }) + ); + + expect(true).toEqual( + FieldUtils.containsTaskToken({ + field: Context.entireContext, + }) + ); + + expect(false).toEqual( + FieldUtils.containsTaskToken({ + oops: 'not here', + }) + ); + + expect(false).toEqual( + FieldUtils.containsTaskToken({ + oops: Context.stringAt('$$.Execution.StartTime'), + }) + ); + }), + test('arbitrary JSONPath fields are not replaced', () => { + expect( + FieldUtils.renderObject({ + field: '$.content', + }) + ).toStrictEqual({ + field: '$.content', + }); + }), + test('fields cannot be used somewhere in a string interpolation', () => { + expect(() => FieldUtils.renderObject({ + field: `contains ${Data.stringAt('$.hello')}`, + })).toThrowError(/Field references must be the entire string/); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.map.ts b/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts similarity index 55% rename from packages/@aws-cdk/aws-stepfunctions/test/test.map.ts rename to packages/@aws-cdk/aws-stepfunctions/test/map.test.ts index e4cee1c8cdae9..f43b425898128 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.map.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts @@ -1,9 +1,9 @@ +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'State Machine With Map State'(test: Test) { +describe('Map State', () => { + test('State Machine With Map State', () => { // GIVEN const stack = new cdk.Stack(); @@ -19,7 +19,7 @@ export = { map.iterator(new stepfunctions.Pass(stack, 'Pass State')); // THEN - test.deepEqual(render(map), { + expect(render(map)).toStrictEqual({ StartAt: 'Map State', States: { 'Map State': { @@ -40,10 +40,9 @@ export = { } } }); + }), - test.done(); - }, - 'synth is successful'(test: Test) { + test('synth is successful', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -55,9 +54,9 @@ export = { }); app.synth(); - test.done(); - }, - 'fails in synthesis if iterator is missing'(test: Test) { + }), + + test('fails in synthesis if iterator is missing', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -68,13 +67,10 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /Map state must have a non-empty iterator/, 'A validation was expected'); + expect(() => app.synth()).toThrow(/Map state must have a non-empty iterator/); + }), - test.done(); - }, - 'fails in synthesis when maxConcurrency is a float'(test: Test) { + test('fails in synthesis when maxConcurrency is a float', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -86,14 +82,10 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /maxConcurrency has to be a positive integer/, 'A validation was expected'); - - test.done(); + expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); + }), - }, - 'fails in synthesis when maxConcurrency is a negative integer'(test: Test) { + test('fails in synthesis when maxConcurrency is a negative integer', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -105,13 +97,10 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /maxConcurrency has to be a positive integer/, 'A validation was expected'); + expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); + }), - test.done(); - }, - 'fails in synthesis when maxConcurrency is too big to be an integer'(test: Test) { + test('fails in synthesis when maxConcurrency is too big to be an integer', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -123,40 +112,34 @@ export = { return map; }); - test.throws(() => { - app.synth(); - }, /maxConcurrency has to be a positive integer/, 'A validation was expected'); - - test.done(); - - }, - 'isPositiveInteger is false with negative number'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(-1), false, '-1 is not a valid positive integer'); - test.done(); - }, - 'isPositiveInteger is false with decimal number'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(1.2), false, '1.2 is not a valid positive integer'); - test.done(); - }, - 'isPositiveInteger is false with a value greater than safe integer '(test: Test) { + expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); + }), + + test('isPositiveInteger is false with negative number', () => { + expect(stepfunctions.isPositiveInteger(-1)).toEqual(false); + }), + + test('isPositiveInteger is false with decimal number', () => { + expect(stepfunctions.isPositiveInteger(1.2)).toEqual(false); + }), + + test('isPositiveInteger is false with a value greater than safe integer', () => { const valueToTest = Number.MAX_SAFE_INTEGER + 1; - test.equals(stepfunctions.isPositiveInteger(valueToTest), false, `${valueToTest} is not a valid positive integer`); - test.done(); - }, - 'isPositiveInteger is true with 0'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(0), true, '0 is expected to be a positive integer'); - test.done(); - }, - 'isPositiveInteger is true with 10'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(10), true, '10 is expected to be a positive integer'); - test.done(); - }, - 'isPositiveInteger is true with max integer value'(test: Test) { - test.equals(stepfunctions.isPositiveInteger(Number.MAX_SAFE_INTEGER), true, - `${Number.MAX_SAFE_INTEGER} is expected to be a positive integer`); - test.done(); - } -}; + expect(stepfunctions.isPositiveInteger(valueToTest)).toEqual(false); + }), + + test('isPositiveInteger is true with 0', () => { + expect(stepfunctions.isPositiveInteger(0)).toEqual(true); + }), + + test('isPositiveInteger is true with 10', () => { + expect(stepfunctions.isPositiveInteger(10)).toEqual(true); + }), + + test('isPositiveInteger is true with max integer value', () => { + expect(stepfunctions.isPositiveInteger(Number.MAX_SAFE_INTEGER)).toEqual(true); + }); +}); function render(sm: stepfunctions.IChainable) { return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.parallel.ts b/packages/@aws-cdk/aws-stepfunctions/test/parallel.test.ts similarity index 83% rename from packages/@aws-cdk/aws-stepfunctions/test/test.parallel.ts rename to packages/@aws-cdk/aws-stepfunctions/test/parallel.test.ts index 19ca289bb78d9..a018f858290cf 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/parallel.test.ts @@ -1,9 +1,9 @@ +import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'State Machine With Parallel State'(test: Test) { +describe('Parallel State', () => { + test('State Machine With Parallel State', () => { // GIVEN const stack = new cdk.Stack(); @@ -13,7 +13,7 @@ export = { parallel.branch(new stepfunctions.Pass(stack, 'Branch 2')); // THEN - test.deepEqual(render(parallel), { + expect(render(parallel)).toStrictEqual({ StartAt: 'Parallel State', States: { 'Parallel State': { @@ -26,10 +26,8 @@ export = { } } }); - - test.done(); - } -}; + }); +}); function render(sm: stepfunctions.IChainable) { return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts new file mode 100644 index 0000000000000..aa78cd4618cf2 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/pass.test.ts @@ -0,0 +1,34 @@ +import '@aws-cdk/assert/jest'; +import { Result } from '../lib'; + +describe('Pass State', () => { + test('fromString has proper value', () => { + const testValue = 'test string'; + const result = Result.fromString(testValue); + expect(result.value).toEqual(testValue); + }), + + test('fromNumber has proper value', () => { + const testValue = 1; + const result = Result.fromNumber(testValue); + expect(result.value).toEqual(testValue); + }), + + test('fromBoolean has proper value', () => { + const testValue = false; + const result = Result.fromBoolean(testValue); + expect(result.value).toEqual(testValue); + }), + + test('fromObject has proper value', () => { + const testValue = {a: 1}; + const result = Result.fromObject(testValue); + expect(result.value).toStrictEqual(testValue); + }), + + test('fromArray has proper value', () => { + const testValue = [1]; + const result = Result.fromArray(testValue); + expect(result.value).toEqual(testValue); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts similarity index 82% rename from packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts rename to packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index 16840ef821ded..3b89936fa4f34 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine-resources.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -1,11 +1,11 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'Tasks can add permissions to the execution role'(test: Test) { +describe('State Machine Resources', () => { + test('Tasks can add permissions to the execution role', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -26,7 +26,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -37,12 +37,10 @@ export = { } ], } - })); - - test.done(); - }, + }); + }), - 'Tasks hidden inside a Parallel state are also included'(test: Test) { + test('Tasks hidden inside a Parallel state are also included', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -68,7 +66,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -79,12 +77,10 @@ export = { } ], } - })); - - test.done(); - }, + }); + }), - 'Task should render InputPath / Parameters / OutputPath correctly'(test: Test) { + test('Task should render InputPath / Parameters / OutputPath correctly', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -108,7 +104,7 @@ export = { const taskState = task.toStateJson(); // THEN - test.deepEqual(taskState, { End: true, + expect(taskState).toStrictEqual({ End: true, Retry: undefined, Catch: undefined, InputPath: '$', @@ -126,11 +122,9 @@ export = { TimeoutSeconds: undefined, HeartbeatSeconds: undefined }); + }), - test.done(); - }, - - 'Task combines taskobject parameters with direct parameters'(test: Test) { + test('Task combines taskobject parameters with direct parameters', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -153,7 +147,7 @@ export = { const taskState = task.toStateJson(); // THEN - test.deepEqual(taskState, { End: true, + expect(taskState).toStrictEqual({ End: true, Retry: undefined, Catch: undefined, InputPath: '$', @@ -168,11 +162,9 @@ export = { TimeoutSeconds: undefined, HeartbeatSeconds: undefined }); + }), - test.done(); - }, - - 'Created state machine can grant start execution to a role'(test: Test) { + test('Created state machine can grant start execution to a role', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -191,7 +183,7 @@ export = { stateMachine.grantStartExecution(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -210,12 +202,11 @@ export = { Ref: 'Role1ABCC5F0' } ] - })); + }); - test.done(); - }, + }), - 'Imported state machine can grant start execution to a role'(test: Test) { + test('Imported state machine can grant start execution to a role', () => { // GIVEN const stack = new cdk.Stack(); const stateMachineArn = 'arn:aws:states:::my-state-machine'; @@ -228,7 +219,7 @@ export = { stateMachine.grantStartExecution(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -245,12 +236,10 @@ export = { Ref: 'Role1ABCC5F0' } ] - })); - - test.done(); - }, + }); + }), - 'Pass should render InputPath / Parameters / OutputPath correctly'(test: Test) { + test('Pass should render InputPath / Parameters / OutputPath correctly', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Pass(stack, 'Pass', { @@ -269,7 +258,7 @@ export = { const taskState = task.toStateJson(); // THEN - test.deepEqual(taskState, { End: true, + expect(taskState).toStrictEqual({ End: true, InputPath: '$', OutputPath: '$.state', Parameters: @@ -283,11 +272,9 @@ export = { Result: undefined, ResultPath: undefined, }); + }), - test.done(); - }, - - 'State machines must depend on their roles'(test: Test) { + test('State machines must depend on their roles', () => { // GIVEN const stack = new cdk.Stack(); const task = new stepfunctions.Task(stack, 'Task', { @@ -308,14 +295,12 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { DependsOn: [ 'StateMachineRoleDefaultPolicyDF1E6607', 'StateMachineRoleB840431D' ] - }, ResourcePart.CompleteDefinition)); - - test.done(); - }, + }, ResourcePart.CompleteDefinition); + }); -}; +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts similarity index 79% rename from packages/@aws-cdk/aws-stepfunctions/test/test.state-machine.ts rename to packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts index 3c0741b6cbedb..78928bb7c06f4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as stepfunctions from '../lib'; -export = { - 'Instantiate Default State Machine'(test: Test) { +describe('State Machine', () => { + test('Instantiate Default State Machine', () => { // GIVEN const stack = new cdk.Stack(); @@ -16,15 +15,13 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { StateMachineName: 'MyStateMachine', DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}' - })); - - test.done(); - }, + }); + }), - 'Instantiate Standard State Machine'(test: Test) { + test('Instantiate Standard State Machine', () => { // GIVEN const stack = new cdk.Stack(); @@ -36,16 +33,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { StateMachineName: 'MyStateMachine', StateMachineType: 'STANDARD', DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}' - })); + }); - test.done(); - }, + }), - 'Instantiate Express State Machine'(test: Test) { + test('Instantiate Express State Machine', () => { // GIVEN const stack = new cdk.Stack(); @@ -57,16 +53,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { StateMachineName: 'MyStateMachine', StateMachineType: 'EXPRESS', DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}' - })); + }); - test.done(); - }, + }), - 'log configuration'(test: Test) { + test('log configuration', () => { // GIVEN const stack = new cdk.Stack(); @@ -83,7 +78,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::StepFunctions::StateMachine', { + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { DefinitionString: '{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}', LoggingConfiguration: { Destinations: [{ @@ -96,9 +91,9 @@ export = { IncludeExecutionData: false, Level: 'FATAL' } - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Action: [ @@ -122,8 +117,7 @@ export = { Ref: 'MyStateMachineRoleD59FFEBC' } ] - })); + }); + }); - test.done(); - }, -}; +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts new file mode 100644 index 0000000000000..cb39d8fdd6239 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/states-language.test.ts @@ -0,0 +1,668 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as stepfunctions from '../lib'; + +describe('States Language', () => { + test('A single task is a State Machine', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const chain = new stepfunctions.Pass(stack, 'Some State'); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Some State', + States: { + 'Some State': { Type: 'Pass', End: true } + } + }); + }), + + test('A sequence of two tasks is a State Machine', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + + const chain = stepfunctions.Chain + .start(task1) + .next(task2); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', End: true }, + } + }); + }), + + test('You dont need to hold on to the state to render the entire state machine correctly', () => { + const stack = new cdk.Stack(); + + // WHEN + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + + task1.next(task2); + + // THEN + expect(render(task1)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', End: true }, + } + }); + }), + + test('A chain can be appended to', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + const task3 = new stepfunctions.Pass(stack, 'State Three'); + + // WHEN + const chain = stepfunctions.Chain + .start(task1) + .next(task2) + .next(task3); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Pass', End: true }, + } + }); + }), + + test('A state machine can be appended to another state machine', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Pass(stack, 'State One'); + const task2 = new stepfunctions.Pass(stack, 'State Two'); + const task3 = new stepfunctions.Wait(stack, 'State Three', { + time: stepfunctions.WaitTime.duration(cdk.Duration.seconds(10)) + }); + + // WHEN + const chain = stepfunctions.Chain + .start(task1) + .next(stepfunctions.Chain.start(task2).next(task3)); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'State One', + States: { + 'State One': { Type: 'Pass', Next: 'State Two' }, + 'State Two': { Type: 'Pass', Next: 'State Three' }, + 'State Three': { Type: 'Wait', End: true, Seconds: 10 }, + } + }); + + }), + + test('A state machine definition can be instantiated and chained', () => { + const stack = new cdk.Stack(); + const before = new stepfunctions.Pass(stack, 'Before'); + const after = new stepfunctions.Pass(stack, 'After'); + + // WHEN + const chain = before.next(new ReusableStateMachine(stack, 'Reusable')).next(after); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Before', + States: { + 'Before': { Type: 'Pass', Next: 'Choice' }, + 'Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Right Branch' }, + ] + }, + 'Left Branch': { Type: 'Pass', Next: 'After' }, + 'Right Branch': { Type: 'Pass', Next: 'After' }, + 'After': { Type: 'Pass', End: true }, + } + }); + }), + + test('A success state cannot be chained onto', () => { + // GIVEN + const stack = new cdk.Stack(); + + const succeed = new stepfunctions.Succeed(stack, 'Succeed'); + const pass = new stepfunctions.Pass(stack, 'Pass'); + + // WHEN + expect(() => pass.next(succeed).next(pass)).toThrow(); + }), + + test('A failure state cannot be chained onto', () => { + // GIVEN + const stack = new cdk.Stack(); + const fail = new stepfunctions.Fail(stack, 'Fail', { error: 'X', cause: 'Y' }); + const pass = new stepfunctions.Pass(stack, 'Pass'); + + // WHEN + expect(() => pass.next(fail).next(pass)).toThrow(); + }), + + test('Parallels can contain direct states', () => { + // GIVEN + const stack = new cdk.Stack(); + + const one = new stepfunctions.Pass(stack, 'One'); + const two = new stepfunctions.Pass(stack, 'Two'); + const three = new stepfunctions.Pass(stack, 'Three'); + + // WHEN + const para = new stepfunctions.Parallel(stack, 'Parallel'); + para.branch(one.next(two)); + para.branch(three); + + // THEN + expect(render(para)).toStrictEqual({ + StartAt: 'Parallel', + States: { + Parallel: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'One', + States: { + One: { Type: 'Pass', Next: 'Two' }, + Two: { Type: 'Pass', End: true }, + } + }, + { + StartAt: 'Three', + States: { + Three: { Type: 'Pass', End: true } + } + } + ] + } + } + }); + }), + + test('Parallels can contain instantiated reusable definitions', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const para = new stepfunctions.Parallel(stack, 'Parallel'); + para.branch(new ReusableStateMachine(stack, 'Reusable1').prefixStates('Reusable1/')); + para.branch(new ReusableStateMachine(stack, 'Reusable2').prefixStates('Reusable2/')); + + // THEN + expect(render(para)).toStrictEqual({ + StartAt: 'Parallel', + States: { + Parallel: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Reusable1/Choice', + States: { + 'Reusable1/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable1/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable1/Right Branch' }, + ] + }, + 'Reusable1/Left Branch': { Type: 'Pass', End: true }, + 'Reusable1/Right Branch': { Type: 'Pass', End: true }, + } + }, + { + StartAt: 'Reusable2/Choice', + States: { + 'Reusable2/Choice': { + Type: 'Choice', + Choices: [ + { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable2/Left Branch' }, + { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable2/Right Branch' }, + ] + }, + 'Reusable2/Left Branch': { Type: 'Pass', End: true }, + 'Reusable2/Right Branch': { Type: 'Pass', End: true }, + } + }, + ] + } + } + }); + }), + + test('State Machine Fragments can be wrapped in a single state', () => { + // GIVEN + const stack = new cdk.Stack(); + + const reusable = new SimpleChain(stack, 'Hello'); + const state = reusable.toSingleState(); + + expect(render(state)).toStrictEqual({ + StartAt: 'Hello', + States: { + Hello: { + Type: 'Parallel', + End: true, + Branches: [ + { + StartAt: 'Hello: Task1', + States: { + 'Hello: Task1': { Type: 'Task', Next: 'Hello: Task2', Resource: 'resource' }, + 'Hello: Task2': { Type: 'Task', End: true, Resource: 'resource' }, + } + } + ], + }, + } + }); + }), + + test('Chaining onto branched failure state ignores failure state', () => { + // GIVEN + const stack = new cdk.Stack(); + + const yes = new stepfunctions.Pass(stack, 'Yes'); + const no = new stepfunctions.Fail(stack, 'No', { error: 'Failure', cause: 'Wrong branch' }); + const enfin = new stepfunctions.Pass(stack, 'Finally'); + const choice = new stepfunctions.Choice(stack, 'Choice') + .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) + .otherwise(no); + + // WHEN + choice.afterwards().next(enfin); + + // THEN + expect(render(choice)).toStrictEqual({ + StartAt: 'Choice', + States: { + Choice: { + Type: 'Choice', + Choices: [ + { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, + ], + Default: 'No', + }, + Yes: { Type: 'Pass', Next: 'Finally' }, + No: { Type: 'Fail', Error: 'Failure', Cause: 'Wrong branch' }, + Finally: { Type: 'Pass', End: true }, + } + }); + }), + + test('Can include OTHERWISE transition for Choice in afterwards()', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const chain = new stepfunctions.Choice(stack, 'Choice') + .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), + new stepfunctions.Pass(stack, 'Yes')) + .afterwards({ includeOtherwise: true }) + .next(new stepfunctions.Pass(stack, 'Finally')); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Choice', + States: { + Choice: { + Type: 'Choice', + Choices: [ + { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, + ], + Default: 'Finally', + }, + Yes: { Type: 'Pass', Next: 'Finally' }, + Finally: { Type: 'Pass', End: true }, + } + }); + + }), + + test('State machines can have unconstrainted gotos', () => { + // GIVEN + const stack = new cdk.Stack(); + + const one = new stepfunctions.Pass(stack, 'One'); + const two = new stepfunctions.Pass(stack, 'Two'); + + // WHEN + const chain = one.next(two).next(one); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'One', + States: { + One: { Type: 'Pass', Next: 'Two' }, + Two: { Type: 'Pass', Next: 'One' }, + } + }); + }), + + test('States can have error branches', () => { + // GIVEN + const stack = new cdk.Stack(); + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask()}); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + const chain = task1.addCatch(failure); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + }), + + test('Retries and errors with a result path', () => { + // GIVEN + const stack = new cdk.Stack(); + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + const chain = task1.addRetry({ errors: ['HTTPError'], maxAttempts: 2 }).addCatch(failure, { resultPath: '$.some_error' }).next(failure); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed', ResultPath: '$.some_error' } ], + Retry: [ { ErrorEquals: ['HTTPError'], MaxAttempts: 2 } ], + Next: 'Failed', + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + }), + + test('Can wrap chain and attach error handler', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.next(task2).toSingleState('Wrapped').addCatch(errorHandler); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Wrapped', + States: { + Wrapped: { + Type: 'Parallel', + Branches: [ + { + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + }, + Task2: { + Type: 'Task', + Resource: 'resource', + End: true, + }, + } + } + ], + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ], + End: true + }, + ErrorHandler: { Type: 'Pass', End: true } + }, + }); + }), + + test('Chaining does not chain onto error handler state', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.addCatch(errorHandler).next(task2); + + // THEN + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, + ] + }, + Task2: { Type: 'Task', Resource: 'resource', End: true }, + ErrorHandler: { Type: 'Pass', End: true }, + } + }); + }), + + test('Chaining does not chain onto error handler, extended', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const task3 = new stepfunctions.Task(stack, 'Task3', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + const chain = task1.addCatch(errorHandler) + .next(task2.addCatch(errorHandler)) + .next(task3.addCatch(errorHandler)); + + // THEN + const sharedTaskProps = { Type: 'Task', Resource: 'resource', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' } ] }; + expect(render(chain)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { Next: 'Task2', ...sharedTaskProps }, + Task2: { Next: 'Task3', ...sharedTaskProps }, + Task3: { End: true, ...sharedTaskProps }, + ErrorHandler: { Type: 'Pass', End: true }, + } + }); + }), + + test('Error handler with a fragment', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); + + // WHEN + task1.addCatch(errorHandler) + .next(new SimpleChain(stack, 'Chain').catch(errorHandler)) + .next(task2.addCatch(errorHandler)); + }), + + test('Can merge state machines with shared states', () => { + // GIVEN + const stack = new cdk.Stack(); + + const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); + const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); + const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); + + // WHEN + task1.addCatch(failure); + task2.addCatch(failure); + + task1.next(task2); + + // THEN + expect(render(task1)).toStrictEqual({ + StartAt: 'Task1', + States: { + Task1: { + Type: 'Task', + Resource: 'resource', + Next: 'Task2', + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Task2: { + Type: 'Task', + Resource: 'resource', + End: true, + Catch: [ + { ErrorEquals: ['States.ALL'], Next: 'Failed' }, + ] + }, + Failed: { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + } + } + }); + }), + + test('No duplicate state IDs', () => { + // GIVEN + const stack = new cdk.Stack(); + const intermediateParent = new cdk.Construct(stack, 'Parent'); + + const state1 = new stepfunctions.Pass(stack, 'State'); + const state2 = new stepfunctions.Pass(intermediateParent, 'State'); + + state1.next(state2); + + // WHEN + expect(() => render(state1)).toThrow(); + }), + + test('No duplicate state IDs even across Parallel branches', () => { + // GIVEN + const stack = new cdk.Stack(); + const intermediateParent = new cdk.Construct(stack, 'Parent'); + + const state1 = new stepfunctions.Pass(stack, 'State'); + const state2 = new stepfunctions.Pass(intermediateParent, 'State'); + + const parallel = new stepfunctions.Parallel(stack, 'Parallel') + .branch(state1) + .branch(state2); + + // WHEN + expect(() => render(parallel)).toThrow(); + }), + + test('No cross-parallel jumps', () => { + // GIVEN + const stack = new cdk.Stack(); + const state1 = new stepfunctions.Pass(stack, 'State1'); + const state2 = new stepfunctions.Pass(stack, 'State2'); + + expect(() => new stepfunctions.Parallel(stack, 'Parallel') + .branch(state1.next(state2)) + .branch(state2)).toThrow(); + }); +}); + +class ReusableStateMachine extends stepfunctions.StateMachineFragment { + public readonly startState: stepfunctions.State; + public readonly endStates: stepfunctions.INextable[]; + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + const choice = new stepfunctions.Choice(this, 'Choice') + .when(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) + .when(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); + + this.startState = choice; + this.endStates = choice.afterwards().endStates; + } +} + +class SimpleChain extends stepfunctions.StateMachineFragment { + public readonly startState: stepfunctions.State; + public readonly endStates: stepfunctions.INextable[]; + + private readonly task2: stepfunctions.Task; + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + const task1 = new stepfunctions.Task(this, 'Task1', { task: new FakeTask() }); + this.task2 = new stepfunctions.Task(this, 'Task2', { task: new FakeTask() }); + + task1.next(this.task2); + + this.startState = task1; + this.endStates = [this.task2]; + } + + public catch(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { + this.task2.addCatch(state, props); + return this; + } +} + +function render(sm: stepfunctions.IChainable) { + return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); +} + +class FakeTask implements stepfunctions.IStepFunctionsTask { + public bind(_task: stepfunctions.Task): stepfunctions.StepFunctionsTaskConfig { + return { + resourceArn: 'resource' + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts deleted file mode 100644 index edb3179de3a29..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.condition.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Test } from 'nodeunit'; -import * as stepfunctions from '../lib'; - -export = { - 'Condition variables must start with $. or $['(test: Test) { - test.throws(() => { - stepfunctions.Condition.stringEquals('a', 'b'); - }); - - test.done(); - }, - 'Condition variables can start with $.'(test: Test) { - test.doesNotThrow(() => { - stepfunctions.Condition.stringEquals('$.a', 'b'); - }); - - test.done(); - }, - 'Condition variables can start with $['(test: Test) { - test.doesNotThrow(() => { - stepfunctions.Condition.stringEquals('$[0]', 'a'); - }); - - test.done(); - }, - 'NotConditon must render properly'(test: Test) { - assertRendersTo(test, - stepfunctions.Condition.not(stepfunctions.Condition.stringEquals('$.a', 'b')), - {Not: {Variable: '$.a', StringEquals: 'b'}} - ); - - test.done(); - }, - 'CompoundCondition must render properly'(test: Test) { - assertRendersTo(test, - stepfunctions.Condition.and( - stepfunctions.Condition.booleanEquals('$.a', true), - stepfunctions.Condition.numberGreaterThan('$.b', 3) - ), - { And: [ { Variable: '$.a', BooleanEquals: true }, { Variable: '$.b', NumericGreaterThan: 3 } ] } - ); - - test.done(); - }, - 'Exercise a number of other conditions'(test: Test) { - const cases: Array<[stepfunctions.Condition, object]> = [ - [ - stepfunctions.Condition.stringLessThan('$.a', 'foo'), - { Variable: '$.a', StringLessThan: 'foo' }, - ], - [ - stepfunctions.Condition.stringLessThanEquals('$.a', 'foo'), - { Variable: '$.a', StringLessThanEquals: 'foo' }, - ], - [ - stepfunctions.Condition.stringGreaterThan('$.a', 'foo'), - { Variable: '$.a', StringGreaterThan: 'foo' }, - ], - [ - stepfunctions.Condition.stringGreaterThanEquals('$.a', 'foo'), - { Variable: '$.a', StringGreaterThanEquals: 'foo' }, - ], - [ - stepfunctions.Condition.numberEquals('$.a', 5), - { Variable: '$.a', NumericEquals: 5 } - ], - ]; - - for (const [cond, expected] of cases) { - assertRendersTo(test, cond, expected); - } - - test.done(); - }, -}; - -function assertRendersTo(test: Test, cond: stepfunctions.Condition, expected: any) { - test.deepEqual(cond.renderCondition(), expected); -} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts deleted file mode 100644 index 94ac02b7051ca..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.fields.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Test } from 'nodeunit'; -import { Context, Data, FieldUtils } from '../lib'; - -export = { - 'deep replace correctly handles fields in arrays'(test: Test) { - test.deepEqual(FieldUtils.renderObject({ - unknown: undefined, - bool: true, - literal: 'literal', - field: Data.stringAt('$.stringField'), - listField: Data.listAt('$.listField'), - deep: [ - 'literal', - { - deepField: Data.numberAt('$.numField'), - } - ] - }), { - 'bool': true, - 'literal': 'literal', - 'field.$': '$.stringField', - 'listField.$': '$.listField', - 'deep': [ - 'literal', - { - 'deepField.$': '$.numField' - } - ], - }); - - test.done(); - }, - - 'exercise contextpaths'(test: Test) { - test.deepEqual(FieldUtils.renderObject({ - str: Context.stringAt('$$.Execution.StartTime'), - count: Context.numberAt('$$.State.RetryCount'), - token: Context.taskToken, - entire: Context.entireContext - }), { - 'str.$': '$$.Execution.StartTime', - 'count.$': '$$.State.RetryCount', - 'token.$': '$$.Task.Token', - 'entire.$': '$$' - }); - - test.done(); - }, - - 'find all referenced paths'(test: Test) { - test.deepEqual(FieldUtils.findReferencedPaths({ - bool: false, - literal: 'literal', - field: Data.stringAt('$.stringField'), - listField: Data.listAt('$.listField'), - deep: [ - 'literal', - { - field: Data.stringAt('$.stringField'), - deepField: Data.numberAt('$.numField'), - } - ] - }), [ - '$.listField', - '$.numField', - '$.stringField', - ]); - - test.done(); - }, - - 'cannot have JsonPath fields in arrays'(test: Test) { - test.throws(() => { - FieldUtils.renderObject({ - deep: [Data.stringAt('$.hello')] - }); - }, /Cannot use JsonPath fields in an array/); - - test.done(); - }, - - 'datafield path must be correct'(test: Test) { - test.ok(Data.stringAt('$')); - - test.throws(() => { - Data.stringAt('$hello'); - }, /exactly equal to '\$' or start with '\$.'/); - - test.throws(() => { - Data.stringAt('hello'); - }, /exactly equal to '\$' or start with '\$.'/); - - test.done(); - }, - - 'context path must be correct'(test: Test) { - test.ok(Context.stringAt('$$')); - - test.throws(() => { - Context.stringAt('$$hello'); - }, /exactly equal to '\$\$' or start with '\$\$.'/); - - test.throws(() => { - Context.stringAt('hello'); - }, /exactly equal to '\$\$' or start with '\$\$.'/); - - test.done(); - }, - - 'test contains task token'(test: Test) { - test.equal(true, FieldUtils.containsTaskToken({ - field: Context.taskToken - })); - - test.equal(true, FieldUtils.containsTaskToken({ - field: Context.stringAt('$$.Task'), - })); - - test.equal(true, FieldUtils.containsTaskToken({ - field: Context.entireContext - })); - - test.equal(false, FieldUtils.containsTaskToken({ - oops: 'not here' - })); - - test.equal(false, FieldUtils.containsTaskToken({ - oops: Context.stringAt('$$.Execution.StartTime') - })); - - test.done(); - }, - - 'arbitrary JSONPath fields are not replaced'(test: Test) { - test.deepEqual(FieldUtils.renderObject({ - field: '$.content', - }), { - field: '$.content' - }); - - test.done(); - }, - - 'fields cannot be used somewhere in a string interpolation'(test: Test) { - test.throws(() => { - FieldUtils.renderObject({ - field: `contains ${Data.stringAt('$.hello')}` - }); - }, /Field references must be the entire string/); - - test.done(); - } -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts deleted file mode 100644 index 5e73edd5d35ff..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.pass.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Test } from 'nodeunit'; -import { Result } from '../lib'; - -export = { - 'fromString has proper value'(test: Test) { - const testValue = 'test string'; - const result = Result.fromString(testValue); - test.equal(result.value, testValue); - - test.done(); - }, - 'fromNumber has proper value'(test: Test) { - const testValue = 1; - const result = Result.fromNumber(testValue); - test.equal(result.value, testValue); - - test.done(); - }, - 'fromBoolean has proper value'(test: Test) { - const testValue = false; - const result = Result.fromBoolean(testValue); - test.equal(result.value, testValue); - - test.done(); - }, - 'fromObject has proper value'(test: Test) { - const testValue = {a: 1}; - const result = Result.fromObject(testValue); - test.deepEqual(result.value, testValue); - - test.done(); - }, - 'fromArray has proper value'(test: Test) { - const testValue = [1]; - const result = Result.fromArray(testValue); - test.deepEqual(result.value, testValue); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts b/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts deleted file mode 100644 index f1552d1bcc6d5..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions/test/test.states-language.ts +++ /dev/null @@ -1,735 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as stepfunctions from '../lib'; - -export = { - 'Basic composition': { - 'A single task is a State Machine'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const chain = new stepfunctions.Pass(stack, 'Some State'); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Some State', - States: { - 'Some State': { Type: 'Pass', End: true } - } - }); - - test.done(); - }, - - 'A sequence of two tasks is a State Machine'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - - const chain = stepfunctions.Chain - .start(task1) - .next(task2); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'You dont need to hold on to the state to render the entire state machine correctly'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - - task1.next(task2); - - // THEN - test.deepEqual(render(task1), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'A chain can be appended to'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - const task3 = new stepfunctions.Pass(stack, 'State Three'); - - // WHEN - const chain = stepfunctions.Chain - .start(task1) - .next(task2) - .next(task3); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', Next: 'State Three' }, - 'State Three': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'A state machine can be appended to another state machine'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Pass(stack, 'State One'); - const task2 = new stepfunctions.Pass(stack, 'State Two'); - const task3 = new stepfunctions.Wait(stack, 'State Three', { - time: stepfunctions.WaitTime.duration(cdk.Duration.seconds(10)) - }); - - // WHEN - const chain = stepfunctions.Chain - .start(task1) - .next(stepfunctions.Chain.start(task2).next(task3)); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'State One', - States: { - 'State One': { Type: 'Pass', Next: 'State Two' }, - 'State Two': { Type: 'Pass', Next: 'State Three' }, - 'State Three': { Type: 'Wait', End: true, Seconds: 10 }, - } - }); - - test.done(); - }, - - 'A state machine definition can be instantiated and chained'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const before = new stepfunctions.Pass(stack, 'Before'); - const after = new stepfunctions.Pass(stack, 'After'); - - // WHEN - const chain = before.next(new ReusableStateMachine(stack, 'Reusable')).next(after); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Before', - States: { - 'Before': { Type: 'Pass', Next: 'Choice' }, - 'Choice': { - Type: 'Choice', - Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Right Branch' }, - ] - }, - 'Left Branch': { Type: 'Pass', Next: 'After' }, - 'Right Branch': { Type: 'Pass', Next: 'After' }, - 'After': { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'A success state cannot be chained onto'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const succeed = new stepfunctions.Succeed(stack, 'Succeed'); - const pass = new stepfunctions.Pass(stack, 'Pass'); - - // WHEN - test.throws(() => { - pass.next(succeed).next(pass); - }); - - test.done(); - }, - - 'A failure state cannot be chained onto'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const fail = new stepfunctions.Fail(stack, 'Fail', { error: 'X', cause: 'Y' }); - const pass = new stepfunctions.Pass(stack, 'Pass'); - - // WHEN - test.throws(() => { - pass.next(fail).next(pass); - }); - - test.done(); - }, - - 'Parallels can contain direct states'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const one = new stepfunctions.Pass(stack, 'One'); - const two = new stepfunctions.Pass(stack, 'Two'); - const three = new stepfunctions.Pass(stack, 'Three'); - - // WHEN - const para = new stepfunctions.Parallel(stack, 'Parallel'); - para.branch(one.next(two)); - para.branch(three); - - // THEN - test.deepEqual(render(para), { - StartAt: 'Parallel', - States: { - Parallel: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'One', - States: { - One: { Type: 'Pass', Next: 'Two' }, - Two: { Type: 'Pass', End: true }, - } - }, - { - StartAt: 'Three', - States: { - Three: { Type: 'Pass', End: true } - } - } - ] - } - } - }); - - test.done(); - }, - - 'Parallels can contain instantiated reusable definitions'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const para = new stepfunctions.Parallel(stack, 'Parallel'); - para.branch(new ReusableStateMachine(stack, 'Reusable1').prefixStates('Reusable1/')); - para.branch(new ReusableStateMachine(stack, 'Reusable2').prefixStates('Reusable2/')); - - // THEN - test.deepEqual(render(para), { - StartAt: 'Parallel', - States: { - Parallel: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'Reusable1/Choice', - States: { - 'Reusable1/Choice': { - Type: 'Choice', - Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable1/Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable1/Right Branch' }, - ] - }, - 'Reusable1/Left Branch': { Type: 'Pass', End: true }, - 'Reusable1/Right Branch': { Type: 'Pass', End: true }, - } - }, - { - StartAt: 'Reusable2/Choice', - States: { - 'Reusable2/Choice': { - Type: 'Choice', - Choices: [ - { Variable: '$.branch', StringEquals: 'left', Next: 'Reusable2/Left Branch' }, - { Variable: '$.branch', StringEquals: 'right', Next: 'Reusable2/Right Branch' }, - ] - }, - 'Reusable2/Left Branch': { Type: 'Pass', End: true }, - 'Reusable2/Right Branch': { Type: 'Pass', End: true }, - } - }, - ] - } - } - }); - - test.done(); - }, - - 'State Machine Fragments can be wrapped in a single state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const reusable = new SimpleChain(stack, 'Hello'); - const state = reusable.toSingleState(); - - test.deepEqual(render(state), { - StartAt: 'Hello', - States: { - Hello: { - Type: 'Parallel', - End: true, - Branches: [ - { - StartAt: 'Hello: Task1', - States: { - 'Hello: Task1': { Type: 'Task', Next: 'Hello: Task2', Resource: 'resource' }, - 'Hello: Task2': { Type: 'Task', End: true, Resource: 'resource' }, - } - } - ], - }, - } - }); - - test.done(); - }, - - 'Chaining onto branched failure state ignores failure state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const yes = new stepfunctions.Pass(stack, 'Yes'); - const no = new stepfunctions.Fail(stack, 'No', { error: 'Failure', cause: 'Wrong branch' }); - const enfin = new stepfunctions.Pass(stack, 'Finally'); - const choice = new stepfunctions.Choice(stack, 'Choice') - .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), yes) - .otherwise(no); - - // WHEN - choice.afterwards().next(enfin); - - // THEN - test.deepEqual(render(choice), { - StartAt: 'Choice', - States: { - Choice: { - Type: 'Choice', - Choices: [ - { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, - ], - Default: 'No', - }, - Yes: { Type: 'Pass', Next: 'Finally' }, - No: { Type: 'Fail', Error: 'Failure', Cause: 'Wrong branch' }, - Finally: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'Can include OTHERWISE transition for Choice in afterwards()'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const chain = new stepfunctions.Choice(stack, 'Choice') - .when(stepfunctions.Condition.stringEquals('$.foo', 'bar'), - new stepfunctions.Pass(stack, 'Yes')) - .afterwards({ includeOtherwise: true }) - .next(new stepfunctions.Pass(stack, 'Finally')); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Choice', - States: { - Choice: { - Type: 'Choice', - Choices: [ - { Variable: '$.foo', StringEquals: 'bar', Next: 'Yes' }, - ], - Default: 'Finally', - }, - Yes: { Type: 'Pass', Next: 'Finally' }, - Finally: { Type: 'Pass', End: true }, - } - }); - - test.done(); - } - }, - - 'Goto support': { - 'State machines can have unconstrainted gotos'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const one = new stepfunctions.Pass(stack, 'One'); - const two = new stepfunctions.Pass(stack, 'Two'); - - // WHEN - const chain = one.next(two).next(one); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'One', - States: { - One: { Type: 'Pass', Next: 'Two' }, - Two: { Type: 'Pass', Next: 'One' }, - } - }); - - test.done(); - }, - }, - - 'Catches': { - 'States can have error branches'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask()}); - const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); - - // WHEN - const chain = task1.addCatch(failure); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - End: true, - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'Failed' }, - ] - }, - Failed: { - Type: 'Fail', - Error: 'DidNotWork', - Cause: 'We got stuck', - } - } - }); - - test.done(); - }, - - 'Retries and errors with a result path'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); - - // WHEN - const chain = task1.addRetry({ errors: ['HTTPError'], maxAttempts: 2 }).addCatch(failure, { resultPath: '$.some_error' }).next(failure); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Catch: [ { ErrorEquals: ['States.ALL'], Next: 'Failed', ResultPath: '$.some_error' } ], - Retry: [ { ErrorEquals: ['HTTPError'], MaxAttempts: 2 } ], - Next: 'Failed', - }, - Failed: { - Type: 'Fail', - Error: 'DidNotWork', - Cause: 'We got stuck', - } - } - }); - - test.done(); - - }, - - 'Can wrap chain and attach error handler'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.next(task2).toSingleState('Wrapped').addCatch(errorHandler); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Wrapped', - States: { - Wrapped: { - Type: 'Parallel', - Branches: [ - { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - }, - Task2: { - Type: 'Task', - Resource: 'resource', - End: true, - }, - } - } - ], - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ], - End: true - }, - ErrorHandler: { Type: 'Pass', End: true } - }, - }); - - test.done(); - }, - - 'Chaining does not chain onto error handler state'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.addCatch(errorHandler).next(task2); - - // THEN - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' }, - ] - }, - Task2: { Type: 'Task', Resource: 'resource', End: true }, - ErrorHandler: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'Chaining does not chain onto error handler, extended'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const task3 = new stepfunctions.Task(stack, 'Task3', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - const chain = task1.addCatch(errorHandler) - .next(task2.addCatch(errorHandler)) - .next(task3.addCatch(errorHandler)); - - // THEN - const sharedTaskProps = { Type: 'Task', Resource: 'resource', Catch: [ { ErrorEquals: ['States.ALL'], Next: 'ErrorHandler' } ] }; - test.deepEqual(render(chain), { - StartAt: 'Task1', - States: { - Task1: { Next: 'Task2', ...sharedTaskProps }, - Task2: { Next: 'Task3', ...sharedTaskProps }, - Task3: { End: true, ...sharedTaskProps }, - ErrorHandler: { Type: 'Pass', End: true }, - } - }); - - test.done(); - }, - - 'Error handler with a fragment'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const errorHandler = new stepfunctions.Pass(stack, 'ErrorHandler'); - - // WHEN - task1.addCatch(errorHandler) - .next(new SimpleChain(stack, 'Chain').catch(errorHandler)) - .next(task2.addCatch(errorHandler)); - - test.done(); - }, - - 'Can merge state machines with shared states'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const task1 = new stepfunctions.Task(stack, 'Task1', { task: new FakeTask() }); - const task2 = new stepfunctions.Task(stack, 'Task2', { task: new FakeTask() }); - const failure = new stepfunctions.Fail(stack, 'Failed', { error: 'DidNotWork', cause: 'We got stuck' }); - - // WHEN - task1.addCatch(failure); - task2.addCatch(failure); - - task1.next(task2); - - // THEN - test.deepEqual(render(task1), { - StartAt: 'Task1', - States: { - Task1: { - Type: 'Task', - Resource: 'resource', - Next: 'Task2', - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'Failed' }, - ] - }, - Task2: { - Type: 'Task', - Resource: 'resource', - End: true, - Catch: [ - { ErrorEquals: ['States.ALL'], Next: 'Failed' }, - ] - }, - Failed: { - Type: 'Fail', - Error: 'DidNotWork', - Cause: 'We got stuck', - } - } - }); - - test.done(); - } - }, - - 'State machine validation': { - 'No duplicate state IDs'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const intermediateParent = new cdk.Construct(stack, 'Parent'); - - const state1 = new stepfunctions.Pass(stack, 'State'); - const state2 = new stepfunctions.Pass(intermediateParent, 'State'); - - state1.next(state2); - - // WHEN - test.throws(() => { - render(state1); - }); - - test.done(); - }, - - 'No duplicate state IDs even across Parallel branches'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const intermediateParent = new cdk.Construct(stack, 'Parent'); - - const state1 = new stepfunctions.Pass(stack, 'State'); - const state2 = new stepfunctions.Pass(intermediateParent, 'State'); - - const parallel = new stepfunctions.Parallel(stack, 'Parallel') - .branch(state1) - .branch(state2); - - // WHEN - test.throws(() => { - render(parallel); - }); - - test.done(); - }, - - 'No cross-parallel jumps'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const state1 = new stepfunctions.Pass(stack, 'State1'); - const state2 = new stepfunctions.Pass(stack, 'State2'); - - test.throws(() => { - new stepfunctions.Parallel(stack, 'Parallel') - .branch(state1.next(state2)) - .branch(state2); - }); - - test.done(); - }, - }, -}; - -class ReusableStateMachine extends stepfunctions.StateMachineFragment { - public readonly startState: stepfunctions.State; - public readonly endStates: stepfunctions.INextable[]; - constructor(scope: cdk.Construct, id: string) { - super(scope, id); - - const choice = new stepfunctions.Choice(this, 'Choice') - .when(stepfunctions.Condition.stringEquals('$.branch', 'left'), new stepfunctions.Pass(this, 'Left Branch')) - .when(stepfunctions.Condition.stringEquals('$.branch', 'right'), new stepfunctions.Pass(this, 'Right Branch')); - - this.startState = choice; - this.endStates = choice.afterwards().endStates; - } -} - -class SimpleChain extends stepfunctions.StateMachineFragment { - public readonly startState: stepfunctions.State; - public readonly endStates: stepfunctions.INextable[]; - - private readonly task2: stepfunctions.Task; - constructor(scope: cdk.Construct, id: string) { - super(scope, id); - - const task1 = new stepfunctions.Task(this, 'Task1', { task: new FakeTask() }); - this.task2 = new stepfunctions.Task(this, 'Task2', { task: new FakeTask() }); - - task1.next(this.task2); - - this.startState = task1; - this.endStates = [this.task2]; - } - - public catch(state: stepfunctions.IChainable, props?: stepfunctions.CatchProps): SimpleChain { - this.task2.addCatch(state, props); - return this; - } -} - -function render(sm: stepfunctions.IChainable) { - return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson()); -} - -class FakeTask implements stepfunctions.IStepFunctionsTask { - public bind(_task: stepfunctions.Task): stepfunctions.StepFunctionsTaskConfig { - return { - resourceArn: 'resource' - }; - } -}