From ea06f7db4857e12e9b13508c64b5321a841e6dc4 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Tue, 10 Oct 2023 13:19:07 -0600 Subject: [PATCH] fix(integ-tests): cannot make two or more identical assertions (#27380) This PR resolves errors in the integration test library when users run identical assertions multiple times. This change ensures that each use of `awsApiCall`, `httpApiCall`, and `invokeFunction` has a unique identifier, even when multiple identical assertions are used in the same context. We introduced a `uniqueAssertionId` function in the `DeployAssert` construct that maintains backward compatibility with the old id rendering scheme, but prevents the use of conflicting construct ids by tracking the usage of ids and differentiating them where there's a conflict. Closes #22043, #23049 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/assertions/private/deploy-assert.ts | 25 +++++++- .../test/assertions/deploy-assert.test.ts | 45 +++++++++++++ .../Assertions.assets.json | 2 +- ...efaultTestDeployAssertDC0672BB.assets.json | 12 ++-- ...aultTestDeployAssertDC0672BB.template.json | 57 ++++++++++++++++- .../integ.assertions.js.snapshot/cdk.out | 2 +- .../integ.assertions.js.snapshot/integ.json | 2 +- .../manifest.json | 18 +++++- .../integ.assertions.js.snapshot/tree.json | 64 +++++++++++++++++-- .../assertions/providers/integ.assertions.ts | 16 ++++- 10 files changed, 221 insertions(+), 22 deletions(-) diff --git a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/private/deploy-assert.ts b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/private/deploy-assert.ts index e0bac3df6a8c5..3120e68d66a9a 100644 --- a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/private/deploy-assert.ts +++ b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/private/deploy-assert.ts @@ -50,6 +50,7 @@ export class DeployAssert extends Construct implements IDeployAssert { } public scope: Stack; + private assertionIdCounts = new Map(); constructor(scope: Construct, props?: DeployAssertProps) { super(scope, 'Default'); @@ -65,7 +66,7 @@ export class DeployAssert extends Construct implements IDeployAssert { hash = md5hash(this.scope.resolve(parameters)); } catch {} - return new AwsApiCall(this.scope, `AwsApiCall${service}${api}${hash}`, { + return new AwsApiCall(this.scope, this.uniqueAssertionId(`AwsApiCall${service}${api}${hash}`), { api, service, parameters, @@ -87,7 +88,7 @@ export class DeployAssert extends Construct implements IDeployAssert { const parsedUrl = new URL(url); append = `${parsedUrl.hostname}${parsedUrl.pathname}`; } - return new HttpApiCall(this.scope, `HttpApiCall${append}${hash}`, { + return new HttpApiCall(this.scope, this.uniqueAssertionId(`HttpApiCall${append}${hash}`), { url, fetchOptions: options, }); @@ -95,7 +96,7 @@ export class DeployAssert extends Construct implements IDeployAssert { public invokeFunction(props: LambdaInvokeFunctionProps): IApiCall { const hash = md5hash(this.scope.resolve(props)); - return new LambdaInvokeFunction(this.scope, `LambdaInvoke${hash}`, props); + return new LambdaInvokeFunction(this.scope, this.uniqueAssertionId(`LambdaInvoke${hash}`), props); } public expect(id: string, expected: ExpectedResult, actual: ActualResult): void { @@ -104,4 +105,22 @@ export class DeployAssert extends Construct implements IDeployAssert { actual, }); } + + /** + * Gets a unique logical id based on a proposed assertion id. + */ + private uniqueAssertionId(id: string): string { + const count = this.assertionIdCounts.get(id); + + if (count === undefined) { + // If we've never seen this id before, we'll return the id unchanged + // to maintain backward compatibility. + this.assertionIdCounts.set(id, 1); + return id; + } + + // Otherwise, we'll increment the counter and return a unique id. + this.assertionIdCounts.set(id, count + 1); + return `${id}${count}`; + } } diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts b/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts index 286eed10d47cc..88a449610b3d1 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts @@ -57,6 +57,21 @@ describe('DeployAssert', () => { }, }); }); + + test('multiple identical calls can be configured', () => { + // GIVEN + const app = new App(); + + // WHEN + const deployAssert = new DeployAssert(app); + deployAssert.invokeFunction({ functionName: 'my-func' }); + deployAssert.invokeFunction({ functionName: 'my-func' }); + + // THEN + const template = Template.fromStack(deployAssert.scope); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.resourceCountIs('Custom::DeployAssert@SdkCallLambdainvoke', 2); + }); }); describe('assertions', () => { @@ -145,6 +160,21 @@ describe('DeployAssert', () => { template.resourceCountIs('Custom::DeployAssert@SdkCallMyServiceMyApi2', 1); }); + test('multiple identical calls can be configured', () => { + // GIVEN + const app = new App(); + + // WHEN + const deployAssert = new DeployAssert(app); + deployAssert.awsApiCall('MyService', 'MyApi'); + deployAssert.awsApiCall('MyService', 'MyApi'); + + // THEN + const template = Template.fromStack(deployAssert.scope); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.resourceCountIs('Custom::DeployAssert@SdkCallMyServiceMyApi', 2); + }); + test('custom resource type length is truncated when greater than 60 characters', () => { // GIVEN const app = new App(); @@ -203,6 +233,21 @@ describe('DeployAssert', () => { template.resourceCountIs('Custom::DeployAssert@HttpCallexamplecomtest789', 1); }); + test('multiple identical calls can be configured', () => { + // GIVEN + const app = new App(); + + // WHEN + const deployAssert = new DeployAssert(app); + deployAssert.httpApiCall('https://example.com/test'); + deployAssert.httpApiCall('https://example.com/test'); + + // THEN + const template = Template.fromStack(deployAssert.scope); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.resourceCountIs('Custom::DeployAssert@HttpCallexamplecomtest', 2); + }); + test('call with fetch options', () => { // GIVEN const app = new App(); diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/Assertions.assets.json b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/Assertions.assets.json index f04bef943d95b..13c479a2e33d8 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/Assertions.assets.json +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/Assertions.assets.json @@ -1,5 +1,5 @@ { - "version": "32.0.0", + "version": "34.0.0", "files": { "95e75e09df57bf020e079977e10353bf8899e195d27f0b3dbb4b0e6b316d3fab": { "source": { diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.assets.json b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.assets.json index 0e849c4d2eb85..3e39b1129f6c0 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.assets.json +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.assets.json @@ -1,20 +1,20 @@ { - "version": "32.0.0", + "version": "34.0.0", "files": { - "855b18ac2a744f847c78764c4effd335c5b424915174596732358882002a9b6e": { + "725b0df96c2f47516947f7471b1187d8db70a7b45e4ae44c5e9430cdf9e75767": { "source": { - "path": "asset.855b18ac2a744f847c78764c4effd335c5b424915174596732358882002a9b6e.bundle", + "path": "asset.725b0df96c2f47516947f7471b1187d8db70a7b45e4ae44c5e9430cdf9e75767.bundle", "packaging": "zip" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "855b18ac2a744f847c78764c4effd335c5b424915174596732358882002a9b6e.zip", + "objectKey": "725b0df96c2f47516947f7471b1187d8db70a7b45e4ae44c5e9430cdf9e75767.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } }, - "2ef86b8cd519122b8b6ca4bb7cad1dd433d4750651d4bbf50ce5e52b44d6a361": { + "4e01d7d1d71b079819d9e433311d9475a6aaece447884c16b6b41f29619e0920": { "source": { "path": "AssertionsTestDefaultTestDeployAssertDC0672BB.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "2ef86b8cd519122b8b6ca4bb7cad1dd433d4750651d4bbf50ce5e52b44d6a361.json", + "objectKey": "4e01d7d1d71b079819d9e433311d9475a6aaece447884c16b6b41f29619e0920.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.template.json b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.template.json index 82e0e5ba5a8a3..4d7660a9dc833 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.template.json +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/AssertionsTestDefaultTestDeployAssertDC0672BB.template.json @@ -28,7 +28,7 @@ "WithDecryption": "true" }, "flattenResponse": "false", - "salt": "1689703949818" + "salt": "1696292331033" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -59,6 +59,15 @@ "PolicyDocument": { "Version": "2012-10-17", "Statement": [ + { + "Action": [ + "ssm:GetParameter" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, { "Action": [ "ssm:GetParameter" @@ -82,7 +91,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "855b18ac2a744f847c78764c4effd335c5b424915174596732358882002a9b6e.zip" + "S3Key": "725b0df96c2f47516947f7471b1187d8db70a7b45e4ae44c5e9430cdf9e75767.zip" }, "Timeout": 120, "Handler": "index.handler", @@ -93,6 +102,42 @@ ] } } + }, + "AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21": { + "Type": "Custom::DeployAssert@SdkCallSSMgetParameter", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "SSM", + "api": "getParameter", + "expected": "{\"$ObjectLike\":{\"Parameter\":{\"Type\":\"String\",\"Value\":\"ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ!\\\"#¤%&/()=?`´^*+~_-.,:;<>|\"}}}", + "parameters": { + "Name": { + "Fn::Join": [ + "", + [ + "\"", + { + "Fn::ImportValue": "Assertions:ExportsOutputRefUtf8Parameter528A4835" + }, + "\"" + ] + ] + }, + "WithDecryption": "true" + }, + "flattenResponse": "false", + "salt": "1696292331034" + }, + "DependsOn": [ + "AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f2" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } }, "Outputs": { @@ -103,6 +148,14 @@ "assertion" ] } + }, + "AssertionResultsAwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21": { + "Value": { + "Fn::GetAtt": [ + "AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21", + "assertion" + ] + } } }, "Parameters": { diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/cdk.out b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/cdk.out index f0b901e7c06e5..2313ab5436501 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/cdk.out +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"32.0.0"} \ No newline at end of file +{"version":"34.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/integ.json b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/integ.json index 2869af98f0e27..bb3d3fc159353 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/integ.json +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "32.0.0", + "version": "34.0.0", "testCases": { "AssertionsTest/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/manifest.json b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/manifest.json index 44c9c36e2b529..0a35d3dd6173e 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/manifest.json +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "32.0.0", + "version": "34.0.0", "artifacts": { "Assertions.assets": { "type": "cdk:asset-manifest", @@ -14,6 +14,7 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "Assertions.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", @@ -73,10 +74,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "AssertionsTestDefaultTestDeployAssertDC0672BB.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/2ef86b8cd519122b8b6ca4bb7cad1dd433d4750651d4bbf50ce5e52b44d6a361.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/4e01d7d1d71b079819d9e433311d9475a6aaece447884c16b6b41f29619e0920.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -117,6 +119,18 @@ "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" } ], + "/AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21" + } + ], + "/AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21" + } + ], "/AssertionsTest/DefaultTest/DeployAssert/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/tree.json b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/tree.json index 7974345e4ed6f..d21164a73ea85 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/tree.json +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.js.snapshot/tree.json @@ -31,7 +31,7 @@ }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.55" + "version": "10.2.70" } }, "BootstrapVersion": { @@ -69,7 +69,7 @@ "path": "AssertionsTest/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.55" + "version": "10.2.70" } }, "DeployAssert": { @@ -89,7 +89,7 @@ "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f2/SdkProvider/AssertionsProvider", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.55" + "version": "10.2.70" } } }, @@ -161,7 +161,61 @@ }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.55" + "version": "10.2.70" + } + }, + "AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21": { + "id": "AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21", + "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.70" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/Default", + "children": { + "Default": { + "id": "Default", + "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/Default/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "AssertionsTest/DefaultTest/DeployAssert/AwsApiCallSSMgetParametere2d1ba6ca5f8b296a8dfc6b4036a90f21/AssertionResults", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.AwsApiCall", + "version": "0.0.0" } }, "BootstrapVersion": { @@ -203,7 +257,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.55" + "version": "10.2.70" } } }, diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.ts b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.ts index 06c22f763de8b..bac88417f2e42 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.ts +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/providers/integ.assertions.ts @@ -16,7 +16,7 @@ const integ = new IntegTest(app, 'AssertionsTest', { testCases: [stack], }); -integ.assertions.awsApiCall('SSM', 'getParameter', { +const firstAssertion = integ.assertions.awsApiCall('SSM', 'getParameter', { Name: ssmParameter.ref, WithDecryption: true, }).expect( @@ -27,3 +27,17 @@ integ.assertions.awsApiCall('SSM', 'getParameter', { }, }), ); + +const secondAssertion = integ.assertions.awsApiCall('SSM', 'getParameter', { + Name: ssmParameter.ref, + WithDecryption: true, +}).expect( + ExpectedResult.objectLike({ + Parameter: { + Type: 'String', + Value: 'ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ!"#¤%&/()=?`´^*+~_-.,:;<>|', + }, + }), +); + +firstAssertion.next(secondAssertion); \ No newline at end of file