Skip to content

Commit

Permalink
fix(integ-test): limit api response to avoid 4k limit (aws#23102)
Browse files Browse the repository at this point in the history
fixes aws#22059

follows precedent set in aws#14041

also added filtering to assertAtPath to reduce duplication and reduce the chance of hitting the error for developers using the library

also added test for assertAtPath

----

### All Submissions:

* [x] 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*
  • Loading branch information
jak authored and Brennan Ho committed Jan 20, 2023
1 parent 82edd24 commit 19cc9ba
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 3 deletions.
2 changes: 2 additions & 0 deletions packages/@aws-cdk/integ-tests/lib/assertions/api-call-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface IApiCall extends IConstruct {
* Assert that the ExpectedResult is equal
* to the result of the AwsApiCall at the given path.
*
* Providing a path will filter the output of the initial API call.
*
* For example the SQS.receiveMessage api response would look
* like:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ export class DeployAssert extends Construct implements IDeployAssert {
Object.defineProperty(this, DEPLOY_ASSERT_SYMBOL, { value: true });
}

public awsApiCall(service: string, api: string, parameters?: any): IApiCall {
public awsApiCall(service: string, api: string, parameters?: any, outputPaths?: string[]): IApiCall {
return new AwsApiCall(this.scope, `AwsApiCall${service}${api}`, {
api,
service,
parameters,
outputPaths,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,28 @@ export class AwsApiCallHandler extends CustomResourceHandler<AwsApiCallRequest,
...flatten(respond),
};

const resp = request.flattenResponse === 'true' ? flatData : respond;
let resp: AwsApiCallResult | { [key: string]: string } = respond;
if (request.outputPaths) {
resp = filterKeys(flatData, request.outputPaths!);
} else if (request.flattenResponse === 'true') {
resp = flatData;
}
console.log(`Returning result ${JSON.stringify(resp)}`);
return resp;
}
}

function filterKeys(object: object, searchStrings: string[]): { [key: string]: string } {
return Object.entries(object).reduce((filteredObject: { [key: string]: string }, [key, value]) => {
for (const searchString of searchStrings) {
if (key.startsWith(`apiCallResponse.${searchString}`)) {
filteredObject[key] = value;
}
}
return filteredObject;
}, {});
}

function isJsonString(value: string): any {
try {
return JSON.parse(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export interface AwsApiCallRequest {
* @default 'false'
*/
readonly flattenResponse?: string;

/**
* Restrict the data returned by the API call to specific paths in
* the API response. Use this to limit the data returned by the custom
* resource if working with API calls that could potentially result in custom
* response objects exceeding the hard limit of 4096 bytes.
*
* @default - return all data
*/
readonly outputPaths?: string[];
}

/**
Expand Down
14 changes: 14 additions & 0 deletions packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export interface AwsApiCallOptions {
* @default - no parameters
*/
readonly parameters?: any;

/**
* Restrict the data returned by the API call to specific paths in
* the API response. Use this to limit the data returned by the custom
* resource if working with API calls that could potentially result in custom
* response objects exceeding the hard limit of 4096 bytes.
*
* @default - return all data
*/
readonly outputPaths?: string[];
}

/**
Expand Down Expand Up @@ -59,6 +69,7 @@ export class AwsApiCall extends ApiCallBase {
private readonly name: string;

private _assertAtPath?: string;
private _outputPaths?: string[];
private readonly api: string;
private readonly service: string;

Expand All @@ -70,6 +81,7 @@ export class AwsApiCall extends ApiCallBase {
this.name = `${props.service}${props.api}`;
this.api = props.api;
this.service = props.service;
this._outputPaths = props.outputPaths;

this.apiCallResource = new CustomResource(this, 'Default', {
serviceToken: this.provider.serviceToken,
Expand All @@ -81,6 +93,7 @@ export class AwsApiCall extends ApiCallBase {
stateMachineArn: Lazy.string({ produce: () => this.stateMachineArn }),
parameters: this.provider.encode(props.parameters),
flattenResponse: Lazy.string({ produce: () => this.flattenResponse }),
outputPaths: Lazy.list({ produce: () => this._outputPaths }),
salt: Date.now().toString(),
},
resourceType: `${SDK_RESOURCE_TYPE_PREFIX}${this.name}`.substring(0, 60),
Expand All @@ -107,6 +120,7 @@ export class AwsApiCall extends ApiCallBase {

public assertAtPath(path: string, expected: ExpectedResult): IApiCall {
this._assertAtPath = path;
this._outputPaths = [path];
this.expectedResult = expected.result;
this.flattenResponse = 'true';
return this;
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/integ-tests/lib/assertions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface IDeployAssert {
* Messages: [{ Body: 'hello' }],
* }));
*/
awsApiCall(service: string, api: string, parameters?: any): IApiCall;
awsApiCall(service: string, api: string, parameters?: any, outputPaths?: string[]): IApiCall;

/**
* Invoke a lambda function and return the response which can be asserted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,40 @@ describe('SdkHandler', () => {
sinon.assert.calledWith(fake, { DryRun: false });
});
});

test('restrict output path', async () => {
// GIVEN
const responseFake = {
Name: 'bucket-name',
Contents: [
{
Key: 'first-key',
ETag: 'first-key-etag',
},
{
Key: 'second-key',
ETag: 'second-key-etag',
},
],
} as SDK.S3.ListObjectsOutput;
AWS.mock('S3', 'listObjects', sinon.fake.resolves(responseFake));
const handler = sdkHandler() as any;
const request: AwsApiCallRequest = {
service: 'S3',
api: 'listObjects',
parameters: {
Bucket: 'myBucket',
},
outputPaths: ['Name', 'Contents.0.Key'],
};

// WHEN
const response: AwsApiCallResult = await handler.processEvent(request);

// THEN
expect(response).toEqual({
'apiCallResponse.Name': 'bucket-name',
'apiCallResponse.Contents.0.Key': 'first-key',
});
});
});
57 changes: 57 additions & 0 deletions packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,63 @@ describe('AwsApiCall', () => {

});

test('restrict output paths', () => {
// GIVEN
const app = new App();
const deplossert = new DeployAssert(app);

// WHEN
deplossert.awsApiCall('MyService', 'MyApi', {
param1: 'val1',
param2: 2,
}, ['path1', 'path2']);

// THEN
const template = Template.fromStack(deplossert.scope);
template.resourceCountIs('AWS::Lambda::Function', 1);
template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', {
service: 'MyService',
api: 'MyApi',
parameters: {
param1: 'val1',
param2: 2,
},
outputPaths: [
'path1',
'path2',
],
});
});

test('assert at path', () => {
// GIVEN
const app = new App();
const deplossert = new DeployAssert(app);

// WHEN
deplossert.awsApiCall('MyService', 'MyApi', {
param1: 'val1',
param2: 2,
}).assertAtPath('Messages.0.Key', ExpectedResult.exact('first-key'));

// THEN
const template = Template.fromStack(deplossert.scope);
template.resourceCountIs('AWS::Lambda::Function', 1);
template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', {
service: 'MyService',
api: 'MyApi',
parameters: {
param1: 'val1',
param2: 2,
},
flattenResponse: 'true',
outputPaths: [
'Messages.0.Key',
],
expected: JSON.stringify({ $Exact: 'first-key' }),
});
});

test('add policy to provider', () => {
// GIVEN
const app = new App();
Expand Down

0 comments on commit 19cc9ba

Please sign in to comment.