Skip to content

Commit bbe0380

Browse files
apalumbomergify[bot]
authored andcommitted
fix(stepfunctions): map state validation fix (#4382)
* fix(stepfunctions) Fixed validation for missing iterator for Map state, added synth unit tests * feat(stepfunctions): Added test and validation for maxConcurrency, that has to be a positive integer * refactor(stepfunctions): Refactored Map test to reduce code duplication * refactor(stepfunctions): extracted integer validation outside map, added tests * fix(stepfunctions) Fixed map test and isPositiveInteger
1 parent 392aefc commit bbe0380

File tree

2 files changed

+145
-4
lines changed

2 files changed

+145
-4
lines changed

packages/@aws-cdk/aws-stepfunctions/lib/states/map.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ export interface MapProps {
7070
readonly maxConcurrency?: number;
7171
}
7272

73+
/**
74+
* Returns true if the value passed is a positive integer
75+
* @param value the value ti validate
76+
*/
77+
78+
export const isPositiveInteger = (value: number) => {
79+
const isFloat = Math.floor(value) !== value;
80+
81+
const isNotPositiveInteger = value < 0 || value > Number.MAX_SAFE_INTEGER;
82+
83+
return !isFloat && !isNotPositiveInteger;
84+
};
85+
7386
/**
7487
* Define a Map state in the state machine
7588
*
@@ -80,7 +93,7 @@ export interface MapProps {
8093
export class Map extends State implements INextable {
8194
public readonly endStates: INextable[];
8295

83-
private readonly maxConcurrency?: number;
96+
private readonly maxConcurrency: number | undefined;
8497
private readonly itemsPath?: string;
8598

8699
constructor(scope: cdk.Construct, id: string, props: MapProps = {}) {
@@ -150,10 +163,17 @@ export class Map extends State implements INextable {
150163
* Validate this state
151164
*/
152165
protected validate(): string[] {
153-
if (!!this.iterator) {
154-
return ['Map state must have a non-empty iterator'];
166+
const errors: string[] = [];
167+
168+
if (this.iteration === undefined) {
169+
errors.push('Map state must have a non-empty iterator');
155170
}
156-
return [];
171+
172+
if (this.maxConcurrency !== undefined && !isPositiveInteger(this.maxConcurrency)) {
173+
errors.push('maxConcurrency has to be a positive integer');
174+
}
175+
176+
return errors;
157177
}
158178

159179
private renderItemsPath(): any {

packages/@aws-cdk/aws-stepfunctions/test/test.map.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cdk = require('@aws-cdk/core');
22
import { Test } from 'nodeunit';
33
import stepfunctions = require('../lib');
4+
import { isPositiveInteger } from '../lib';
45

56
export = {
67
'State Machine With Map State'(test: Test) {
@@ -42,9 +43,129 @@ export = {
4243
});
4344

4445
test.done();
46+
},
47+
'synth is successful'(test: Test) {
48+
49+
const app = createAppWithMap((stack) => {
50+
const map = new stepfunctions.Map(stack, 'Map State', {
51+
maxConcurrency: 1,
52+
itemsPath: stepfunctions.Data.stringAt('$.inputForMap')
53+
});
54+
map.iterator(new stepfunctions.Pass(stack, 'Pass State'));
55+
return map;
56+
});
57+
58+
app.synth();
59+
test.done();
60+
},
61+
'fails in synthesis if iterator is missing'(testTest) {
62+
63+
const app = createAppWithMap((stack) => {
64+
const map = new stepfunctions.Map(stack, 'Map State', {
65+
            maxConcurrency1,
66+
itemsPath: stepfunctions.Data.stringAt('$.inputForMap')
67+
});
68+
69+
return map;
70+
});
71+
72+
        test.throws(() => {
73+
            app.synth();
74+
        }, /Map state must have a non-empty iterator/, 'A validation was expected');
75+
76+
        test.done();
77+
    },
78+
'fails in synthesis when maxConcurrency is a float'(testTest) {
79+
80+
const app = createAppWithMap((stack) => {
81+
            const map = new stepfunctions.Map(stack, 'Map State', {
82+
                maxConcurrency1.2,
83+
                itemsPathstepfunctions.Data.stringAt('$.inputForMap')
84+
            });
85+
            map.iterator(new stepfunctions.Pass(stack, 'Pass State'));
86+
87+
return map;
88+
});
89+
90+
        test.throws(() => {
91+
            app.synth();
92+
        }, /maxConcurrency has to be a positive integer/, 'A validation was expected');
93+
94+
        test.done();
95+
96+
    },
97+
    'fails in synthesis when maxConcurrency is a negative integer'(testTest) {
98+
99+
const app = createAppWithMap((stack) => {
100+
            const map = new stepfunctions.Map(stack, 'Map State', {
101+
                maxConcurrency-1,
102+
                itemsPathstepfunctions.Data.stringAt('$.inputForMap')
103+
            });
104+
            map.iterator(new stepfunctions.Pass(stack, 'Pass State'));
105+
106+
return map;
107+
});
108+
109+
        test.throws(() => {
110+
            app.synth();
111+
        }, /maxConcurrency has to be a positive integer/, 'A validation was expected');
112+
113+
        test.done();
114+
    },
115+
    'fails in synthesis when maxConcurrency is too big to be an integer'(testTest) {
116+
117+
const app = createAppWithMap((stack) => {
118+
            const map = new stepfunctions.Map(stack, 'Map State', {
119+
            maxConcurrencyNumber.MAX_VALUE,
120+
            itemsPathstepfunctions.Data.stringAt('$.inputForMap')
121+
            });
122+
            map.iterator(new stepfunctions.Pass(stack, 'Pass State'));
123+
124+
return map;
125+
});
126+
127+
        test.throws(() => {
128+
            app.synth();
129+
        }, /maxConcurrency has to be a positive integer/, 'A validation was expected');
130+
131+
        test.done();
132+
133+
    },
134+
'isPositiveInteger is false with negative number'(test: Test) {
135+
test.equals(isPositiveInteger(-1), false, '-1 is not a valid positive integer');
136+
        test.done();
137+
},
138+
'isPositiveInteger is false with decimal number'(test: Test) {
139+
test.equals(isPositiveInteger(1.2), false, '1.2 is not a valid positive integer');
140+
        test.done();
141+
},
142+
'isPositiveInteger is false with a value greater than safe integer '(test: Test) {
143+
const valueToTest = Number.MAX_SAFE_INTEGER + 1;
144+
test.equals(isPositiveInteger(valueToTest), false, `${valueToTest} is not a valid positive integer`);
145+
        test.done();
146+
},
147+
'isPositiveInteger is true with 0'(test: Test) {
148+
test.equals(isPositiveInteger(0), true, '0 is expected to be a positive integer');
149+
        test.done();
150+
},
151+
'isPositiveInteger is true with 10'(test: Test) {
152+
test.equals(isPositiveInteger(10), true, '10 is expected to be a positive integer');
153+
        test.done();
154+
},
155+
'isPositiveInteger is true with max integer value'(test: Test) {
156+
test.equals(isPositiveInteger(Number.MAX_SAFE_INTEGER), true, `${Number.MAX_SAFE_INTEGER} is expected to be a positive integer`);
157+
        test.done();
45158
}
46159
};
47160

48161
function render(sm: stepfunctions.IChainable) {
49162
return new cdk.Stack().resolve(new stepfunctions.StateGraph(sm.startState, 'Test Graph').toGraphJson());
50163
}
164+
165+
function createAppWithMap(mapFactory: (stack: cdk.Stack) => stepfunctions.Map) {
166+
    const app = new cdk.App();
167+
const stack = new cdk.Stack(app, 'my-stack');
168+
const map = mapFactory(stack);
169+
    new stepfunctions.StateGraph(map, 'Test Graph');
170+
return app;
171+
}

0 commit comments

Comments
 (0)