Skip to content

Commit 530e07b

Browse files
authored
feat(integ-tests): chain assertion api calls (#22196)
This PR does two things. 1. Adds a helper method `next()` that makes it easier to chain assertion api calls together. Yes, it is possible to grab the underlying `node` and call `addDependency`, but I think `then` is a more intuitive experience. Look at `integ.log-group.ts` to see where I updated a test from `addDependency` -> `next` 2. Added an `ApiCallBase` class and renamed the api call interface. This will make it easier to add more types of Api Calls in the future (`HttpApiCall` coming soon*) ---- ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 89bc626 commit 530e07b

File tree

11 files changed

+215
-110
lines changed

11 files changed

+215
-110
lines changed

packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import * as events from '@aws-cdk/aws-events';
22
import * as logs from '@aws-cdk/aws-logs';
33
import * as sqs from '@aws-cdk/aws-sqs';
44
import * as cdk from '@aws-cdk/core';
5+
import { IntegTest, ExpectedResult } from '@aws-cdk/integ-tests';
56
import * as targets from '../../lib';
6-
import { IntegTest, ExpectedResult, AssertionsProvider } from '@aws-cdk/integ-tests';
77
import { LogGroupTargetInput } from '../../lib';
88

99
const app = new cdk.App();
@@ -71,16 +71,15 @@ const putEvent = integ.assertions.awsApiCall('EventBridge', 'putEvents', {
7171
},
7272
],
7373
});
74-
const assertionProvider = putEvent.node.tryFindChild('SdkProvider') as AssertionsProvider;
75-
assertionProvider.addPolicyStatementFromSdkCall('events', 'PutEvents');
74+
putEvent.provider.addPolicyStatementFromSdkCall('events', 'PutEvents');
7675

7776
const logEvents = integ.assertions.awsApiCall('CloudWatchLogs', 'filterLogEvents', {
7877
logGroupName: logGroup2.logGroupName,
7978
startTime: putEventsDate,
8079
limit: 1,
8180
});
8281

83-
logEvents.node.addDependency(putEvent);
82+
putEvent.next(logEvents);
8483

8584
logEvents.assertAtPath('events.0.message', ExpectedResult.stringLikeRegexp(expectedValue));
8685

packages/@aws-cdk/integ-tests/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ There are two main scenarios in which assertions are created.
182182
- Part of an integration test using `integ-runner`
183183

184184
In this case you would create an integration test using the `IntegTest` construct and then make assertions using the `assert` property.
185-
You should **not** utilize the assertion constructs directly, but should instead use the `methods` on `IntegTest.assert`.
185+
You should **not** utilize the assertion constructs directly, but should instead use the `methods` on `IntegTest.assertions`.
186186

187187
```ts
188188
declare const app: App;
@@ -410,3 +410,21 @@ describe.expect(ExpectedResult.objectLike({
410410
}));
411411
```
412412

413+
#### Chain ApiCalls
414+
415+
Sometimes it may be necessary to chain API Calls. Since each API call is its own resource, all you
416+
need to do is add a dependency between the calls. There is an helper method `next` that can be used.
417+
418+
```ts
419+
declare const integ: IntegTest;
420+
421+
integ.assertions.awsApiCall('S3', 'putObject', {
422+
Bucket: 'my-bucket',
423+
Key: 'my-key',
424+
Body: 'helloWorld',
425+
}).next(integ.assertions.awsApiCall('S3', 'getObject', {
426+
Bucket: 'my-bucket',
427+
Key: 'my-key',
428+
}));
429+
```
430+
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { CustomResource, Reference } from '@aws-cdk/core';
2+
import { Construct, IConstruct } from 'constructs';
3+
import { ExpectedResult } from './common';
4+
import { AssertionsProvider } from './providers';
5+
6+
/**
7+
* Represents an ApiCall
8+
*/
9+
export interface IApiCall extends IConstruct {
10+
/**
11+
* access the AssertionsProvider. This can be used to add additional IAM policies
12+
* the the provider role policy
13+
*
14+
* @example
15+
* declare const apiCall: AwsApiCall;
16+
* apiCall.provider.addToRolePolicy({
17+
* Effect: 'Allow',
18+
* Action: ['s3:GetObject'],
19+
* Resource: ['*'],
20+
* });
21+
*/
22+
readonly provider: AssertionsProvider;
23+
24+
/**
25+
* Returns the value of an attribute of the custom resource of an arbitrary
26+
* type. Attributes are returned from the custom resource provider through the
27+
* `Data` map where the key is the attribute name.
28+
*
29+
* @param attributeName the name of the attribute
30+
* @returns a token for `Fn::GetAtt`. Use `Token.asXxx` to encode the returned `Reference` as a specific type or
31+
* use the convenience `getAttString` for string attributes.
32+
*/
33+
getAtt(attributeName: string): Reference;
34+
35+
/**
36+
* Returns the value of an attribute of the custom resource of type string.
37+
* Attributes are returned from the custom resource provider through the
38+
* `Data` map where the key is the attribute name.
39+
*
40+
* @param attributeName the name of the attribute
41+
* @returns a token for `Fn::GetAtt` encoded as a string.
42+
*/
43+
getAttString(attributeName: string): string;
44+
45+
/**
46+
* Assert that the ExpectedResult is equal
47+
* to the result of the AwsApiCall
48+
*
49+
* @example
50+
* declare const integ: IntegTest;
51+
* const invoke = integ.assertions.invokeFunction({
52+
* functionName: 'my-func',
53+
* });
54+
* invoke.expect(ExpectedResult.objectLike({ Payload: 'OK' }));
55+
*/
56+
expect(expected: ExpectedResult): void;
57+
58+
/**
59+
* Assert that the ExpectedResult is equal
60+
* to the result of the AwsApiCall at the given path.
61+
*
62+
* For example the SQS.receiveMessage api response would look
63+
* like:
64+
*
65+
* If you wanted to assert the value of `Body` you could do
66+
*
67+
* @example
68+
* const actual = {
69+
* Messages: [{
70+
* MessageId: '',
71+
* ReceiptHandle: '',
72+
* MD5OfBody: '',
73+
* Body: 'hello',
74+
* Attributes: {},
75+
* MD5OfMessageAttributes: {},
76+
* MessageAttributes: {}
77+
* }]
78+
* };
79+
*
80+
*
81+
* declare const integ: IntegTest;
82+
* const message = integ.assertions.awsApiCall('SQS', 'receiveMessage');
83+
*
84+
* message.assertAtPath('Messages.0.Body', ExpectedResult.stringLikeRegexp('hello'));
85+
*/
86+
assertAtPath(path: string, expected: ExpectedResult): void;
87+
88+
/**
89+
* Allows you to chain IApiCalls. This adds an explicit dependency
90+
* betweent the two resources.
91+
*
92+
* Returns the IApiCall provided as `next`
93+
*
94+
* @example
95+
* declare const first: IApiCall;
96+
* declare const second: IApiCall;
97+
*
98+
* first.next(second);
99+
*/
100+
next(next: IApiCall): IApiCall;
101+
}
102+
103+
/**
104+
* Base class for an ApiCall
105+
*/
106+
export abstract class ApiCallBase extends Construct implements IApiCall {
107+
protected abstract readonly apiCallResource: CustomResource;
108+
protected expectedResult?: string;
109+
protected flattenResponse: string = 'false';
110+
protected stateMachineArn?: string;
111+
112+
public abstract readonly provider: AssertionsProvider;
113+
114+
constructor(scope: Construct, id: string) {
115+
super(scope, id);
116+
117+
}
118+
119+
public getAtt(attributeName: string): Reference {
120+
this.flattenResponse = 'true';
121+
return this.apiCallResource.getAtt(`apiCallResponse.${attributeName}`);
122+
}
123+
124+
public getAttString(attributeName: string): string {
125+
this.flattenResponse = 'true';
126+
return this.apiCallResource.getAttString(`apiCallResponse.${attributeName}`);
127+
}
128+
129+
public expect(expected: ExpectedResult): void {
130+
this.expectedResult = expected.result;
131+
}
132+
133+
public abstract assertAtPath(path: string, expected: ExpectedResult): void;
134+
135+
public next(next: IApiCall): IApiCall {
136+
next.node.addDependency(this);
137+
return next;
138+
}
139+
}

packages/@aws-cdk/integ-tests/lib/assertions/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CustomResource } from '@aws-cdk/core';
2-
import { IAwsApiCall } from './sdk';
2+
import { IApiCall } from './api-call-base';
33

44
/**
55
* Represents the "actual" results to compare
@@ -17,7 +17,7 @@ export abstract class ActualResult {
1717
/**
1818
* Get the actual results from a AwsApiCall
1919
*/
20-
public static fromAwsApiCall(query: IAwsApiCall, attribute: string): ActualResult {
20+
public static fromAwsApiCall(query: IApiCall, attribute: string): ActualResult {
2121
return {
2222
result: query.getAttString(attribute),
2323
};

packages/@aws-cdk/integ-tests/lib/assertions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './assertions';
44
export * from './providers';
55
export * from './common';
66
export * from './match';
7+
export * from './api-call-base';

packages/@aws-cdk/integ-tests/lib/assertions/private/deploy-assert.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Stack } from '@aws-cdk/core';
22
import { Construct, IConstruct, Node } from 'constructs';
3+
import { IApiCall } from '../api-call-base';
34
import { EqualsAssertion } from '../assertions';
45
import { ExpectedResult, ActualResult } from '../common';
56
import { md5hash } from '../private/hash';
6-
import { AwsApiCall, LambdaInvokeFunction, IAwsApiCall, LambdaInvokeFunctionProps } from '../sdk';
7+
import { AwsApiCall, LambdaInvokeFunction, LambdaInvokeFunctionProps } from '../sdk';
78
import { IDeployAssert } from '../types';
89

910

@@ -49,15 +50,15 @@ export class DeployAssert extends Construct implements IDeployAssert {
4950
Object.defineProperty(this, DEPLOY_ASSERT_SYMBOL, { value: true });
5051
}
5152

52-
public awsApiCall(service: string, api: string, parameters?: any): IAwsApiCall {
53+
public awsApiCall(service: string, api: string, parameters?: any): IApiCall {
5354
return new AwsApiCall(this.scope, `AwsApiCall${service}${api}`, {
5455
api,
5556
service,
5657
parameters,
5758
});
5859
}
5960

60-
public invokeFunction(props: LambdaInvokeFunctionProps): IAwsApiCall {
61+
public invokeFunction(props: LambdaInvokeFunctionProps): IApiCall {
6162
const hash = md5hash(this.scope.resolve(props));
6263
return new LambdaInvokeFunction(this.scope, `LambdaInvoke${hash}`, props);
6364
}

packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts

Lines changed: 9 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,10 @@
11
import { ArnFormat, CfnResource, CustomResource, Lazy, Reference, Stack } from '@aws-cdk/core';
2-
import { Construct, IConstruct } from 'constructs';
2+
import { Construct } from 'constructs';
3+
import { ApiCallBase } from './api-call-base';
34
import { EqualsAssertion } from './assertions';
45
import { ActualResult, ExpectedResult } from './common';
56
import { AssertionsProvider, SDK_RESOURCE_TYPE_PREFIX } from './providers';
67

7-
/**
8-
* Interface for creating a custom resource that will perform
9-
* an API call using the AWS SDK
10-
*/
11-
export interface IAwsApiCall extends IConstruct {
12-
/**
13-
* access the AssertionsProvider. This can be used to add additional IAM policies
14-
* the the provider role policy
15-
*
16-
* @example
17-
* declare const apiCall: AwsApiCall;
18-
* apiCall.provider.addToRolePolicy({
19-
* Effect: 'Allow',
20-
* Action: ['s3:GetObject'],
21-
* Resource: ['*'],
22-
* });
23-
*/
24-
readonly provider: AssertionsProvider;
25-
26-
/**
27-
* Returns the value of an attribute of the custom resource of an arbitrary
28-
* type. Attributes are returned from the custom resource provider through the
29-
* `Data` map where the key is the attribute name.
30-
*
31-
* @param attributeName the name of the attribute
32-
* @returns a token for `Fn::GetAtt`. Use `Token.asXxx` to encode the returned `Reference` as a specific type or
33-
* use the convenience `getAttString` for string attributes.
34-
*/
35-
getAtt(attributeName: string): Reference;
36-
37-
/**
38-
* Returns the value of an attribute of the custom resource of type string.
39-
* Attributes are returned from the custom resource provider through the
40-
* `Data` map where the key is the attribute name.
41-
*
42-
* @param attributeName the name of the attribute
43-
* @returns a token for `Fn::GetAtt` encoded as a string.
44-
*/
45-
getAttString(attributeName: string): string;
46-
47-
/**
48-
* Assert that the ExpectedResult is equal
49-
* to the result of the AwsApiCall
50-
*
51-
* @example
52-
* declare const integ: IntegTest;
53-
* const invoke = integ.assertions.invokeFunction({
54-
* functionName: 'my-func',
55-
* });
56-
* invoke.expect(ExpectedResult.objectLike({ Payload: 'OK' }));
57-
*/
58-
expect(expected: ExpectedResult): void;
59-
60-
/**
61-
* Assert that the ExpectedResult is equal
62-
* to the result of the AwsApiCall at the given path.
63-
*
64-
* For example the SQS.receiveMessage api response would look
65-
* like:
66-
*
67-
* If you wanted to assert the value of `Body` you could do
68-
*
69-
* @example
70-
* const actual = {
71-
* Messages: [{
72-
* MessageId: '',
73-
* ReceiptHandle: '',
74-
* MD5OfBody: '',
75-
* Body: 'hello',
76-
* Attributes: {},
77-
* MD5OfMessageAttributes: {},
78-
* MessageAttributes: {}
79-
* }]
80-
* };
81-
*
82-
*
83-
* declare const integ: IntegTest;
84-
* const message = integ.assertions.awsApiCall('SQS', 'receiveMessage');
85-
*
86-
* message.assertAtPath('Messages.0.Body', ExpectedResult.stringLikeRegexp('hello'));
87-
*/
88-
assertAtPath(path: string, expected: ExpectedResult): void;
89-
}
90-
918
/**
929
* Options to perform an AWS JavaScript V2 API call
9310
*/
@@ -119,9 +36,8 @@ export interface AwsApiCallProps extends AwsApiCallOptions { }
11936
* Construct that creates a custom resource that will perform
12037
* a query using the AWS SDK
12138
*/
122-
export class AwsApiCall extends Construct implements IAwsApiCall {
123-
private readonly sdkCallResource: CustomResource;
124-
private flattenResponse: string = 'false';
39+
export class AwsApiCall extends ApiCallBase {
40+
protected readonly apiCallResource: CustomResource;
12541
private readonly name: string;
12642

12743
public readonly provider: AssertionsProvider;
@@ -133,7 +49,7 @@ export class AwsApiCall extends Construct implements IAwsApiCall {
13349
this.provider.addPolicyStatementFromSdkCall(props.service, props.api);
13450
this.name = `${props.service}${props.api}`;
13551

136-
this.sdkCallResource = new CustomResource(this, 'Default', {
52+
this.apiCallResource = new CustomResource(this, 'Default', {
13753
serviceToken: this.provider.serviceToken,
13854
properties: {
13955
service: props.service,
@@ -146,23 +62,23 @@ export class AwsApiCall extends Construct implements IAwsApiCall {
14662
});
14763

14864
// Needed so that all the policies set up by the provider should be available before the custom resource is provisioned.
149-
this.sdkCallResource.node.addDependency(this.provider);
65+
this.apiCallResource.node.addDependency(this.provider);
15066
}
15167

15268
public getAtt(attributeName: string): Reference {
15369
this.flattenResponse = 'true';
154-
return this.sdkCallResource.getAtt(`apiCallResponse.${attributeName}`);
70+
return this.apiCallResource.getAtt(`apiCallResponse.${attributeName}`);
15571
}
15672

15773
public getAttString(attributeName: string): string {
15874
this.flattenResponse = 'true';
159-
return this.sdkCallResource.getAttString(`apiCallResponse.${attributeName}`);
75+
return this.apiCallResource.getAttString(`apiCallResponse.${attributeName}`);
16076
}
16177

16278
public expect(expected: ExpectedResult): void {
16379
new EqualsAssertion(this, `AssertEquals${this.name}`, {
16480
expected,
165-
actual: ActualResult.fromCustomResource(this.sdkCallResource, 'apiCallResponse'),
81+
actual: ActualResult.fromCustomResource(this.apiCallResource, 'apiCallResponse'),
16682
});
16783
}
16884

0 commit comments

Comments
 (0)