diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json index 46b3a9c480519..2eddeda7f15d0 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.assets.json @@ -1,7 +1,7 @@ { "version": "36.0.0", "files": { - "636fdd026b7f14567bc975abac73dfbac55058665c31d090375174adc715aab5": { + "53824d3d2f5a61e7b41fae900d4616b5151557c4b2cded050381cb51c5cb8461": { "source": { "path": "integ-aws-websocket-integration.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "636fdd026b7f14567bc975abac73dfbac55058665c31d090375174adc715aab5.json", + "objectKey": "53824d3d2f5a61e7b41fae900d4616b5151557c4b2cded050381cb51c5cb8461.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json index 683dde85dc2c3..f2b31c274ee0b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/integ-aws-websocket-integration.template.json @@ -78,6 +78,7 @@ }, "AuthorizationType": "NONE", "RouteKey": "$default", + "RouteResponseSelectionExpression": "$default", "Target": { "Fn::Join": [ "", @@ -91,13 +92,24 @@ } } }, - "mywsapiconnectRouteDynamodbPutItem9E189A39": { + "mywsapidefaultRouteResponse9B924040": { + "Type": "AWS::ApiGatewayV2::RouteResponse", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteId": { + "Ref": "mywsapidefaultRouteE9382DF8" + }, + "RouteResponseKey": "$default" + } + }, + "mywsapiputItemRouteDynamodbPutItem3BF52E5B": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { "Ref": "mywsapi32E6CE11" }, - "ContentHandlingStrategy": "CONVERT_TO_BINARY", "CredentialsArn": { "Fn::GetAtt": [ "ApiGatewayRoleD2518903", @@ -118,20 +130,17 @@ ] ] }, - "PassthroughBehavior": "WHEN_NO_TEMPLATES", - "RequestParameters": { - "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" - }, + "PassthroughBehavior": "NEVER", "RequestTemplates": { - "application/json": { + "$default": { "Fn::Join": [ "", [ - "{\"TableName\":\"", + "{\n\"TableName\": \"", { "Ref": "MyTable794EDED1" }, - "\",\"Item\":{\"id\":{\"S\":\"$context.requestId\"}}}" + "\",\n\"Item\": {\n\"id\": { \"S\": \"$context.requestId\" },\n\"userData\": { \"S\": $input.json('$.data') }\n}\n}" ] ] } @@ -140,27 +149,70 @@ "TimeoutInMillis": 10000 } }, - "mywsapiconnectRoute45A0ED6A": { + "mywsapiputItemRoutedefaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3FB3E47D1": { + "Type": "AWS::ApiGatewayV2::IntegrationResponse", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationId": { + "Ref": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" + }, + "IntegrationResponseKey": "$default", + "ResponseTemplates": { + "application/json": "{\"success\":true}" + } + } + }, + "mywsapiputItemRoute4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3F51371BC": { + "Type": "AWS::ApiGatewayV2::IntegrationResponse", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationId": { + "Ref": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" + }, + "IntegrationResponseKey": "/4\\d{2}/", + "ResponseTemplates": { + "application/json": "{\n\"error\": \"Bad request\",\n\"message\": $input.json('$.Message')\n}" + } + } + }, + "mywsapiputItemRoute3F16A651": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { "ApiId": { "Ref": "mywsapi32E6CE11" }, "AuthorizationType": "NONE", - "RouteKey": "$connect", + "RouteKey": "putItem", + "RouteResponseSelectionExpression": "$default", "Target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "mywsapiconnectRouteDynamodbPutItem9E189A39" + "Ref": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" } ] ] } } }, + "mywsapiputItemRouteResponse6A675D0F": { + "Type": "AWS::ApiGatewayV2::RouteResponse", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteId": { + "Ref": "mywsapiputItemRoute3F16A651" + }, + "RouteResponseKey": "$default" + } + }, "DevStage520A913F": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { @@ -172,6 +224,30 @@ } } }, + "Outputs": { + "ApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "wss://", + { + "Ref": "mywsapi32E6CE11" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + }, "Parameters": { "BootstrapVersion": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json index a472b3cafbe28..4fd57abacdf13 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/manifest.json @@ -18,7 +18,7 @@ "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}/636fdd026b7f14567bc975abac73dfbac55058665c31d090375174adc715aab5.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/53824d3d2f5a61e7b41fae900d4616b5151557c4b2cded050381cb51c5cb8461.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -72,16 +72,40 @@ "data": "mywsapidefaultRouteE9382DF8" } ], - "/integ-aws-websocket-integration/mywsapi/$connect-Route/DynamodbPutItem/Resource": [ + "/integ-aws-websocket-integration/mywsapi/$default-Route/Response": [ { "type": "aws:cdk:logicalId", - "data": "mywsapiconnectRouteDynamodbPutItem9E189A39" + "data": "mywsapidefaultRouteResponse9B924040" } ], - "/integ-aws-websocket-integration/mywsapi/$connect-Route/Resource": [ + "/integ-aws-websocket-integration/mywsapi/putItem-Route/DynamodbPutItem/Resource": [ { "type": "aws:cdk:logicalId", - "data": "mywsapiconnectRoute45A0ED6A" + "data": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" + } + ], + "/integ-aws-websocket-integration/mywsapi/putItem-Route/defaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiputItemRoutedefaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3FB3E47D1" + } + ], + "/integ-aws-websocket-integration/mywsapi/putItem-Route/4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiputItemRoute4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3F51371BC" + } + ], + "/integ-aws-websocket-integration/mywsapi/putItem-Route/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiputItemRoute3F16A651" + } + ], + "/integ-aws-websocket-integration/mywsapi/putItem-Route/Response": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapiputItemRouteResponse6A675D0F" } ], "/integ-aws-websocket-integration/DevStage/Resource": [ @@ -90,6 +114,12 @@ "data": "DevStage520A913F" } ], + "/integ-aws-websocket-integration/ApiEndpoint": [ + { + "type": "aws:cdk:logicalId", + "data": "ApiEndpoint" + } + ], "/integ-aws-websocket-integration/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json index 8daa67ba0998a..49205e9ad9300 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.js.snapshot/tree.json @@ -173,6 +173,7 @@ }, "authorizationType": "NONE", "routeKey": "$default", + "routeResponseSelectionExpression": "$default", "target": { "Fn::Join": [ "", @@ -190,6 +191,26 @@ "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRoute", "version": "0.0.0" } + }, + "Response": { + "id": "Response", + "path": "integ-aws-websocket-integration/mywsapi/$default-Route/Response", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::RouteResponse", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "routeId": { + "Ref": "mywsapidefaultRouteE9382DF8" + }, + "routeResponseKey": "$default" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRouteResponse", + "version": "0.0.0" + } } }, "constructInfo": { @@ -197,24 +218,23 @@ "version": "0.0.0" } }, - "$connect-Route": { - "id": "$connect-Route", - "path": "integ-aws-websocket-integration/mywsapi/$connect-Route", + "putItem-Route": { + "id": "putItem-Route", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route", "children": { "DynamodbPutItem": { "id": "DynamodbPutItem", - "path": "integ-aws-websocket-integration/mywsapi/$connect-Route/DynamodbPutItem", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/DynamodbPutItem", "children": { "Resource": { "id": "Resource", - "path": "integ-aws-websocket-integration/mywsapi/$connect-Route/DynamodbPutItem/Resource", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/DynamodbPutItem/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Integration", "aws:cdk:cloudformation:props": { "apiId": { "Ref": "mywsapi32E6CE11" }, - "contentHandlingStrategy": "CONVERT_TO_BINARY", "credentialsArn": { "Fn::GetAtt": [ "ApiGatewayRoleD2518903", @@ -235,20 +255,17 @@ ] ] }, - "passthroughBehavior": "WHEN_NO_TEMPLATES", - "requestParameters": { - "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" - }, + "passthroughBehavior": "NEVER", "requestTemplates": { - "application/json": { + "$default": { "Fn::Join": [ "", [ - "{\"TableName\":\"", + "{\n\"TableName\": \"", { "Ref": "MyTable794EDED1" }, - "\",\"Item\":{\"id\":{\"S\":\"$context.requestId\"}}}" + "\",\n\"Item\": {\n\"id\": { \"S\": \"$context.requestId\" },\n\"userData\": { \"S\": $input.json('$.data') }\n}\n}" ] ] } @@ -268,9 +285,75 @@ "version": "0.0.0" } }, + "defaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3": { + "id": "defaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/defaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/defaultintegawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::IntegrationResponse", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "integrationId": { + "Ref": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" + }, + "integrationResponseKey": "$default", + "responseTemplates": { + "application/json": "{\"success\":true}" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegrationResponse", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegrationResponse", + "version": "0.0.0" + } + }, + "4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3": { + "id": "4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/4d2integawswebsocketintegrationmywsapiputItemRouteDynamodbPutItem8D520CE3/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::IntegrationResponse", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "integrationId": { + "Ref": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" + }, + "integrationResponseKey": "/4\\d{2}/", + "responseTemplates": { + "application/json": "{\n\"error\": \"Bad request\",\n\"message\": $input.json('$.Message')\n}" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegrationResponse", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegrationResponse", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", - "path": "integ-aws-websocket-integration/mywsapi/$connect-Route/Resource", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Route", "aws:cdk:cloudformation:props": { @@ -278,14 +361,15 @@ "Ref": "mywsapi32E6CE11" }, "authorizationType": "NONE", - "routeKey": "$connect", + "routeKey": "putItem", + "routeResponseSelectionExpression": "$default", "target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "mywsapiconnectRouteDynamodbPutItem9E189A39" + "Ref": "mywsapiputItemRouteDynamodbPutItem3BF52E5B" } ] ] @@ -296,6 +380,26 @@ "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRoute", "version": "0.0.0" } + }, + "Response": { + "id": "Response", + "path": "integ-aws-websocket-integration/mywsapi/putItem-Route/Response", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::RouteResponse", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "routeId": { + "Ref": "mywsapiputItemRoute3F16A651" + }, + "routeResponseKey": "$default" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRouteResponse", + "version": "0.0.0" + } } }, "constructInfo": { @@ -337,6 +441,14 @@ "version": "0.0.0" } }, + "ApiEndpoint": { + "id": "ApiEndpoint", + "path": "integ-aws-websocket-integration/ApiEndpoint", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "integ-aws-websocket-integration/BootstrapVersion", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts index c31f9aa5c5133..f1988782b2df2 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.aws.ts @@ -1,13 +1,19 @@ -import { ContentHandling, HttpMethod, PassthroughBehavior, WebSocketApi, WebSocketStage } from 'aws-cdk-lib/aws-apigatewayv2'; +import { HttpMethod, PassthroughBehavior, WebSocketApi, WebSocketIntegrationResponseKey, WebSocketStage } from 'aws-cdk-lib/aws-apigatewayv2'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as iam from 'aws-cdk-lib/aws-iam'; -import { App, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { App, CfnOutput, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; import { WebSocketAwsIntegration, WebSocketMockIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; import { IntegTest } from '@aws-cdk/integ-tests-alpha'; /* * Stack verification steps: - * 1. Verify manually that the integration has type "MOCK" + * 1. Connect: 'wscat -c '. Should connect successfully + * 2. Sending: '> {"action":"putItem", "data": "valid"}' should return + * '< {"success": true}' + * and add an item to the table, with the userData field set to "valid" + * 3. Sending: '> {"action":"putItem", "data": 1}' should return + * '< {"error": "Bad request", "message": "NUMBER_VALUE cannot be converted to String"}' + * and not insert an item to the table */ const app = new App(); @@ -27,41 +33,60 @@ const apiRole = new iam.Role(stack, 'ApiGatewayRole', { }); const webSocketApi = new WebSocketApi(stack, 'mywsapi', { - defaultRouteOptions: { integration: new WebSocketMockIntegration('DefaultIntegration') }, + defaultRouteOptions: { + integration: new WebSocketMockIntegration('DefaultIntegration'), + returnResponse: true, + }, }); // Optionally, create a WebSocket stage -new WebSocketStage(stack, 'DevStage', { +const stage = new WebSocketStage(stack, 'DevStage', { webSocketApi: webSocketApi, stageName: 'dev', autoDeploy: true, }); -webSocketApi.addRoute('$connect', { +webSocketApi.addRoute('putItem', { integration: new WebSocketAwsIntegration('DynamodbPutItem', { integrationUri: `arn:aws:apigateway:${stack.region}:dynamodb:action/PutItem`, integrationMethod: HttpMethod.POST, credentialsRole: apiRole, - requestParameters: { - 'integration.request.header.Content-Type': '\'application/x-www-form-urlencoded\'', - }, requestTemplates: { - 'application/json': JSON.stringify({ - TableName: table.tableName, - Item: { - id: { - S: '$context.requestId', - }, - }, - }), + $default: json`{ + "TableName": "${table.tableName}", + "Item": { + "id": { "S": "$context.requestId" }, + "userData": { "S": $input.json('$.data') } + } + }`, }, + responses: [ + { + responseKey: WebSocketIntegrationResponseKey.default, + responseTemplates: { + 'application/json': JSON.stringify({ success: true }), + }, + }, + { + responseKey: WebSocketIntegrationResponseKey.clientError, + responseTemplates: { + 'application/json': + json`{ + "error": "Bad request", + "message": $input.json('$.Message') + }`, + }, + }, + ], templateSelectionExpression: '\\$default', - passthroughBehavior: PassthroughBehavior.WHEN_NO_TEMPLATES, - contentHandling: ContentHandling.CONVERT_TO_BINARY, + passthroughBehavior: PassthroughBehavior.NEVER, timeout: Duration.seconds(10), }), + returnResponse: true, }); +new CfnOutput(stack, 'ApiEndpoint', { value: stage.url }); + new IntegTest(app, 'apigatewayv2-aws-integration-integ-test', { testCases: [stack], cdkCommandOptions: { @@ -72,3 +97,10 @@ new IntegTest(app, 'apigatewayv2-aws-integration-integ-test', { }, }, }); + +// remove indentation +function json(inputs: TemplateStringsArray, ...variables: string[]) { + return inputs + .map((input, index) => input + (variables[index] ?? '')).join('') + .split('\n').map((line) => line.trim()).join('\n'); +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets.json new file mode 100644 index 0000000000000..9963729af1964 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/cdk.out index 8ecc185e9dbee..1f0068d32659a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"21.0.0"} \ No newline at end of file +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.assets.json index 89f3532813157..e321dbad7588e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "36.0.0", "files": { - "aabdbe840e6768f96ec51dd87886969be769aeca5a21773e27cd16f1a90367fe": { + "7e2a768753bb503ef2a3ff486e3dab6211b724491d63d2690f26df5e2147b3f6": { "source": { "path": "integ-mock-websocket-integration.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "aabdbe840e6768f96ec51dd87886969be769aeca5a21773e27cd16f1a90367fe.json", + "objectKey": "7e2a768753bb503ef2a3ff486e3dab6211b724491d63d2690f26df5e2147b3f6.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.template.json index 085584d49a05b..23db96a7cb884 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ-mock-websocket-integration.template.json @@ -24,8 +24,8 @@ "ApiId": { "Ref": "mywsapi32E6CE11" }, - "RouteKey": "$default", "AuthorizationType": "NONE", + "RouteKey": "$default", "Target": { "Fn::Join": [ "", @@ -39,7 +39,7 @@ } } }, - "mywsapisendmessageRouteSendMessageIntegrationD29E12F9": { + "mywsapisendmessageRouteDefaultIntegration702159AD": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -49,35 +49,63 @@ "IntegrationUri": "" } }, + "mywsapisendmessageRoutedefaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D2401A55BEB": { + "Type": "AWS::ApiGatewayV2::IntegrationResponse", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationId": { + "Ref": "mywsapisendmessageRouteDefaultIntegration702159AD" + }, + "IntegrationResponseKey": "$default", + "ResponseTemplates": { + "$default": "{\"success\":true}" + } + } + }, "mywsapisendmessageRouteAE873328": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { "ApiId": { "Ref": "mywsapi32E6CE11" }, - "RouteKey": "sendmessage", "AuthorizationType": "NONE", + "RouteKey": "sendmessage", + "RouteResponseSelectionExpression": "$default", "Target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "mywsapisendmessageRouteSendMessageIntegrationD29E12F9" + "Ref": "mywsapisendmessageRouteDefaultIntegration702159AD" } ] ] } } }, + "mywsapisendmessageRouteResponse2ED167D2": { + "Type": "AWS::ApiGatewayV2::RouteResponse", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteId": { + "Ref": "mywsapisendmessageRouteAE873328" + }, + "RouteResponseKey": "$default" + } + }, "mystage114C35EC": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { "ApiId": { "Ref": "mywsapi32E6CE11" }, - "StageName": "dev", - "AutoDeploy": true + "AutoDeploy": true, + "StageName": "dev" } } }, diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ.json index e38a89b873c9c..925cbd9e68810 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/integ.json @@ -1,14 +1,12 @@ { - "version": "20.0.0", + "version": "36.0.0", "testCases": { - "integ.mock": { + "apigatewayv2-mock-integration-integ-test/DefaultTest": { "stacks": [ "integ-mock-websocket-integration" ], - "diffAssets": false, - "stackUpdateWorkflow": true + "assertionStack": "apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert", + "assertionStackName": "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/manifest.json index cf17e2f2db70c..d07c7b34e3fc3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "36.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "integ-mock-websocket-integration.assets": { "type": "cdk:asset-manifest", "properties": { @@ -20,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "integ-mock-websocket-integration.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}/aabdbe840e6768f96ec51dd87886969be769aeca5a21773e27cd16f1a90367fe.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/7e2a768753bb503ef2a3ff486e3dab6211b724491d63d2690f26df5e2147b3f6.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -57,10 +52,16 @@ "data": "mywsapidefaultRouteE9382DF8" } ], - "/integ-mock-websocket-integration/mywsapi/sendmessage-Route/SendMessageIntegration/Resource": [ + "/integ-mock-websocket-integration/mywsapi/sendmessage-Route/DefaultIntegration/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapisendmessageRouteDefaultIntegration702159AD" + } + ], + "/integ-mock-websocket-integration/mywsapi/sendmessage-Route/defaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24/Resource": [ { "type": "aws:cdk:logicalId", - "data": "mywsapisendmessageRouteSendMessageIntegrationD29E12F9" + "data": "mywsapisendmessageRoutedefaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D2401A55BEB" } ], "/integ-mock-websocket-integration/mywsapi/sendmessage-Route/Resource": [ @@ -69,6 +70,12 @@ "data": "mywsapisendmessageRouteAE873328" } ], + "/integ-mock-websocket-integration/mywsapi/sendmessage-Route/Response": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapisendmessageRouteResponse2ED167D2" + } + ], "/integ-mock-websocket-integration/mystage/Resource": [ { "type": "aws:cdk:logicalId", @@ -92,9 +99,72 @@ "type": "aws:cdk:logicalId", "data": "CheckBootstrapVersion" } + ], + "mywsapisendmessageRouteintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24defaultIntegrationResponse72380ED8": [ + { + "type": "aws:cdk:logicalId", + "data": "mywsapisendmessageRouteintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24defaultIntegrationResponse72380ED8", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } ] }, "displayName": "integ-mock-websocket-integration" + }, + "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.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}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "apigatewayv2mockintegrationintegtestDefaultTestDeployAssert3D8FA778.assets" + ], + "metadata": { + "/apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/tree.json index 62c47b25cc7d3..c5ae8b0d5028f 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "integ-mock-websocket-integration": { "id": "integ-mock-websocket-integration", "path": "integ-mock-websocket-integration", @@ -32,7 +24,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.CfnApi", + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnApi", "version": "0.0.0" } }, @@ -58,13 +50,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.CfnIntegration", + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegration", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketIntegration", + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegration", "version": "0.0.0" } }, @@ -77,8 +69,8 @@ "apiId": { "Ref": "mywsapi32E6CE11" }, - "routeKey": "$default", "authorizationType": "NONE", + "routeKey": "$default", "target": { "Fn::Join": [ "", @@ -93,13 +85,13 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.CfnRoute", + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRoute", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketRoute", + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketRoute", "version": "0.0.0" } }, @@ -107,13 +99,13 @@ "id": "sendmessage-Route", "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route", "children": { - "SendMessageIntegration": { - "id": "SendMessageIntegration", - "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/SendMessageIntegration", + "DefaultIntegration": { + "id": "DefaultIntegration", + "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/DefaultIntegration", "children": { "Resource": { "id": "Resource", - "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/SendMessageIntegration/Resource", + "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/DefaultIntegration/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Integration", "aws:cdk:cloudformation:props": { @@ -125,13 +117,46 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.CfnIntegration", + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegration", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegration", + "version": "0.0.0" + } + }, + "defaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24": { + "id": "defaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24", + "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/defaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/defaultintegmockwebsocketintegrationmywsapisendmessageRouteDefaultIntegrationA39F8D24/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::IntegrationResponse", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "integrationId": { + "Ref": "mywsapisendmessageRouteDefaultIntegration702159AD" + }, + "integrationResponseKey": "$default", + "responseTemplates": { + "$default": "{\"success\":true}" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnIntegrationResponse", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketIntegration", + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketIntegrationResponse", "version": "0.0.0" } }, @@ -144,15 +169,16 @@ "apiId": { "Ref": "mywsapi32E6CE11" }, - "routeKey": "sendmessage", "authorizationType": "NONE", + "routeKey": "sendmessage", + "routeResponseSelectionExpression": "$default", "target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "mywsapisendmessageRouteSendMessageIntegrationD29E12F9" + "Ref": "mywsapisendmessageRouteDefaultIntegration702159AD" } ] ] @@ -160,19 +186,39 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.CfnRoute", + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRoute", + "version": "0.0.0" + } + }, + "Response": { + "id": "Response", + "path": "integ-mock-websocket-integration/mywsapi/sendmessage-Route/Response", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::RouteResponse", + "aws:cdk:cloudformation:props": { + "apiId": { + "Ref": "mywsapi32E6CE11" + }, + "routeId": { + "Ref": "mywsapisendmessageRouteAE873328" + }, + "routeResponseKey": "$default" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnRouteResponse", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketRoute", + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketRoute", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketApi", + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketApi", "version": "0.0.0" } }, @@ -189,18 +235,18 @@ "apiId": { "Ref": "mywsapi32E6CE11" }, - "stageName": "dev", - "autoDeploy": true + "autoDeploy": true, + "stageName": "dev" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.CfnStage", + "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnStage", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-apigatewayv2.WebSocketStage", + "fqn": "aws-cdk-lib.aws_apigatewayv2.WebSocketStage", "version": "0.0.0" } }, @@ -208,20 +254,98 @@ "id": "ApiEndpoint", "path": "integ-mock-websocket-integration/ApiEndpoint", "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "aws-cdk-lib.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-mock-websocket-integration/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-mock-websocket-integration/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "apigatewayv2-mock-integration-integ-test": { + "id": "apigatewayv2-mock-integration-integ-test", + "path": "apigatewayv2-mock-integration-integ-test", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "apigatewayv2-mock-integration-integ-test/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "apigatewayv2-mock-integration-integ-test/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "apigatewayv2-mock-integration-integ-test/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" } } }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts index 5ae9e8d7556be..e31f0fc02ead6 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2-integrations/test/websocket/integ.mock.ts @@ -1,10 +1,12 @@ -import { WebSocketApi, WebSocketStage } from 'aws-cdk-lib/aws-apigatewayv2'; +import { WebSocketApi, WebSocketIntegrationResponseKey, WebSocketStage } from 'aws-cdk-lib/aws-apigatewayv2'; import { App, CfnOutput, Stack } from 'aws-cdk-lib'; import { WebSocketMockIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; /* * Stack verification steps: - * 1. Verify manually that the integration has type "MOCK" + * 1. Connect: 'wscat -c '. Should connect successfully + * 2. Sending: '> {"action": "sendmessage"}' should return {success: true} */ const app = new App(); @@ -19,6 +21,22 @@ const stage = new WebSocketStage(stack, 'mystage', { autoDeploy: true, }); -webSocketApi.addRoute('sendmessage', { integration: new WebSocketMockIntegration('SendMessageIntegration') }); +webSocketApi.addRoute('sendmessage', { + integration: new WebSocketMockIntegration('DefaultIntegration', { + responses: [ + { + responseKey: WebSocketIntegrationResponseKey.default, + responseTemplates: { + $default: JSON.stringify({ success: true }), + }, + }, + ], + }), + returnResponse: true, +}); new CfnOutput(stack, 'ApiEndpoint', { value: stage.url }); + +new IntegTest(app, 'apigatewayv2-mock-integration-integ-test', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md index 9fd07172268e6..80fa80273b4c9 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/README.md @@ -11,6 +11,7 @@ - [WebSocket APIs](#websocket-apis) - [Lambda WebSocket Integration](#lambda-websocket-integration) - [AWS WebSocket Integration](#aws-websocket-integration) + - [Integration Responses](#integration-responses) - [Import Issues](#import-issues) - [DotNet Namespace](#dotnet-namespace) - [Java Package](#java-package) @@ -312,6 +313,52 @@ webSocketApi.addRoute('$connect', { You can also set additional properties to change the behavior of your integration, such as `contentHandling`. See [Working with binary media types for WebSocket APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-binary-media-types.html). +### Integration Responses + +You can set up your integrations to send responses to your WebSocket client using the [`returnResponse`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigatewayv2.WebSocketRouteOptions.html#returnresponse) field. + +Additionally, some integrations allow you to manipulate and customize your responses, mapped by HTTP response code. This can be done via the `responses` field or the `addResponse` method. +See [Setting up a WebSocket API integration responses in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html). + +Similar to `requestTemplates`, `responseTemplates` can use context or response dependent variables. In the case of an integration response, `$input` will be replaced by the response contents. See [API Gateway WebSocket API mapping template reference](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-mapping-template-reference.html) + +```ts +import { WebSocketAwsIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; + +const webSocketApi = new apigwv2.WebSocketApi(this, 'mywsapi'); +new apigwv2.WebSocketStage(this, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +declare const integration: WebSocketAwsIntegration; + +// Default response key, will be used if no other matched +integration.addResponse(apigwv2.WebSocketIntegrationResponseKey.default); + +integration.addResponse( + // Success response key, will match all 2xx response HTTP status codes + apigwv2.WebSocketIntegrationResponseKey.success, + { responseTemplates: { 'application/json': JSON.stringify({ success: true }) } }, +); + +integration.addResponse( + // You can also create custom response integrations for specific status codes + apigwv2.WebSocketIntegrationResponseKey.fromStatusCode(410), + { + responseTemplates: { + 'application/json': `{ + "error": "Gone", + "message": $input.json('$.Message') + }`, + }, + }, +); + +webSocketApi.addRoute('putItem', { integration, returnResponse: true }); +``` + ## Import Issues `jsiirc.json` file is missing during the stablization process of this module, which caused import issues for DotNet and Java users who attempt to use this module. Unfortunately, to guarantee backward compatibility, we cannot simply correct the namespace for DotNet or package for Java. The following outlines the workaround. diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts index dc265fb223b6c..f9bd90ee6461c 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/aws.ts @@ -1,10 +1,11 @@ import { - WebSocketRouteIntegration, WebSocketIntegrationType, WebSocketRouteIntegrationConfig, WebSocketRouteIntegrationBindOptions, PassthroughBehavior, ContentHandling, + CustomResponseWebSocketRoute, + InternalWebSocketIntegrationResponseProps, } from '../../../aws-apigatewayv2'; import { IRole } from '../../../aws-iam'; import { Duration } from '../../../core'; @@ -56,9 +57,18 @@ export interface WebSocketAwsIntegrationProps { * ``` * * @default - No request template provided to the integration. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-mapping-template-reference.html */ readonly requestTemplates?: { [contentType: string]: string }; + /** + * Integration responses configuration + * + * @default - No response configuration provided. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ + readonly responses?: InternalWebSocketIntegrationResponseProps[]; + /** * The template selection expression for the integration. * @@ -89,7 +99,7 @@ export interface WebSocketAwsIntegrationProps { /** * AWS WebSocket AWS Type Integration */ -export class WebSocketAwsIntegration extends WebSocketRouteIntegration { +export class WebSocketAwsIntegration extends CustomResponseWebSocketRoute { /** * @param id id of the underlying integration construct */ @@ -106,6 +116,7 @@ export class WebSocketAwsIntegration extends WebSocketRouteIntegration { credentialsRole: this.props.credentialsRole, requestParameters: this.props.requestParameters, requestTemplates: this.props.requestTemplates, + responses: this.props.responses, passthroughBehavior: this.props.passthroughBehavior, templateSelectionExpression: this.props.templateSelectionExpression, timeout: this.props.timeout, diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/lambda.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/lambda.ts index 3ae153e63ec79..a249ae7e1fa19 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/lambda.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/lambda.ts @@ -1,9 +1,9 @@ import { - WebSocketRouteIntegration, WebSocketIntegrationType, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, ContentHandling, + WebSocketRouteIntegration, } from '../../../aws-apigatewayv2'; import { ServicePrincipal } from '../../../aws-iam'; import { IFunction } from '../../../aws-lambda'; @@ -34,7 +34,6 @@ export interface WebSocketLambdaIntegrationProps { * Lambda WebSocket Integration */ export class WebSocketLambdaIntegration extends WebSocketRouteIntegration { - private readonly _id: string; /** diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/mock.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/mock.ts index 3ebf7411930c7..84970d7fc8942 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/mock.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/lib/websocket/mock.ts @@ -1,19 +1,33 @@ import { - WebSocketRouteIntegration, WebSocketIntegrationType, WebSocketRouteIntegrationConfig, WebSocketRouteIntegrationBindOptions, + CustomResponseWebSocketRoute, + InternalWebSocketIntegrationResponseProps, } from '../../../aws-apigatewayv2'; +/** + * Props for Mock type integration for a WebSocket Api. + */ +export interface WebSocketMockIntegrationProps { + /** + * Integration responses configuration + * + * @default - No response configuration provided. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ + readonly responses?: InternalWebSocketIntegrationResponseProps[]; +} + /** * Mock WebSocket Integration */ -export class WebSocketMockIntegration extends WebSocketRouteIntegration { +export class WebSocketMockIntegration extends CustomResponseWebSocketRoute { /** * @param id id of the underlying integration construct */ - constructor(id: string) { + constructor(id: string, private readonly props: WebSocketMockIntegrationProps = {}) { super(id); } @@ -22,6 +36,7 @@ export class WebSocketMockIntegration extends WebSocketRouteIntegration { return { type: WebSocketIntegrationType.MOCK, uri: '', + responses: this.props.responses, }; } } diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts index 5294734854aba..c1a4d79d8bea7 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/aws.test.ts @@ -1,6 +1,6 @@ import { WebSocketAwsIntegration } from './../../lib/websocket/aws'; import { Template } from '../../../assertions'; -import { ContentHandling, PassthroughBehavior, WebSocketApi } from '../../../aws-apigatewayv2'; +import { ContentHandling, PassthroughBehavior, WebSocketApi, WebSocketIntegrationResponseKey } from '../../../aws-apigatewayv2'; import * as iam from '../../../aws-iam'; import { Duration, Stack } from '../../../core'; @@ -47,7 +47,9 @@ describe('AwsWebSocketIntegration', () => { passthroughBehavior: PassthroughBehavior.WHEN_NO_TEMPLATES, contentHandling: ContentHandling.CONVERT_TO_BINARY, timeout: Duration.seconds(10), + responses: [{ responseKey: WebSocketIntegrationResponseKey.success }], }), + returnResponse: true, }, }); @@ -66,5 +68,10 @@ describe('AwsWebSocketIntegration', () => { ContentHandlingStrategy: 'CONVERT_TO_BINARY', TimeoutInMillis: 10000, }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteAwsIntegration9C4CC31F' }, + IntegrationResponseKey: '/2\\d{2}/', + }); }); }); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/integration-response.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/integration-response.test.ts new file mode 100644 index 0000000000000..ac91d91c41bb5 --- /dev/null +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/integration-response.test.ts @@ -0,0 +1,341 @@ +import { Match, Template } from '../../../assertions'; +import { ContentHandling, InternalWebSocketIntegrationResponseProps, WebSocketApi, WebSocketIntegrationResponse, WebSocketIntegrationResponseKey, WebSocketIntegrationType, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, CustomResponseWebSocketRoute, InternalWebSocketIntegrationResponseOptions } from '../../../aws-apigatewayv2'; +import * as iam from '../../../aws-iam'; +import { Stack } from '../../../core'; + +interface WebSocketTestRouteIntegrationConfig { + /** + * Integration URI. + */ + readonly integrationUri: string; + + /** + * Integration responses configuration + * + * @default - No response configuration provided. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ + readonly responses?: InternalWebSocketIntegrationResponseProps[]; +} + +class WebSocketTestIntegration extends CustomResponseWebSocketRoute { + constructor(id: string, private readonly props: WebSocketTestRouteIntegrationConfig) { + super(id); + } + + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: 'TEST' as WebSocketIntegrationType, + uri: this.props.integrationUri, + responses: this.props.responses, + }; + } + + /** + * Add a response to this integration + * + * @param responseKey The response key to add + * @param options Optional properties to add to the response + */ + addResponse( + responseKey: WebSocketIntegrationResponseKey, + options: InternalWebSocketIntegrationResponseOptions = {}) { + super.addResponse(responseKey, options); + } +} + +describe('WebSocketIntegrationResponseKey', () => { + test('constant response key values are correct', () => { + expect(WebSocketIntegrationResponseKey.default.key).toEqual('$default'); + expect(WebSocketIntegrationResponseKey.success.key).toEqual('/2\\d{2}/'); + expect(WebSocketIntegrationResponseKey.clientError.key).toEqual('/4\\d{2}/'); + expect(WebSocketIntegrationResponseKey.serverError.key).toEqual('/5\\d{2}/'); + + expect(WebSocketIntegrationResponseKey.ok.key).toEqual('/200/'); + expect(WebSocketIntegrationResponseKey.noContent.key).toEqual('/204/'); + expect(WebSocketIntegrationResponseKey.badRequest.key).toEqual('/400/'); + expect(WebSocketIntegrationResponseKey.unauthorized.key).toEqual('/401/'); + expect(WebSocketIntegrationResponseKey.forbidden.key).toEqual('/403/'); + expect(WebSocketIntegrationResponseKey.notFound.key).toEqual('/404/'); + expect(WebSocketIntegrationResponseKey.internalServerError.key).toEqual('/500/'); + }); + + test('can generate fromStatusCode', () => { + // GIVEN + const { key } = WebSocketIntegrationResponseKey.fromStatusCode(404); + + // THEN + expect(key).toEqual('/404/'); + }); + + test('can generate fromKeys', () => { + // GIVEN + const { key } = WebSocketIntegrationResponseKey.fromKeys( + WebSocketIntegrationResponseKey.ok, + WebSocketIntegrationResponseKey.fromStatusCode(201), + WebSocketIntegrationResponseKey.clientError, + ); + + // THEN + expect(key).toEqual('/200|201|4\\d{2}/'); + }); + + test('throws is fromKeys includes a non-regex key', () => { + expect( + () => WebSocketIntegrationResponseKey.fromKeys( + WebSocketIntegrationResponseKey.default, + WebSocketIntegrationResponseKey.clientError, + ), + ).toThrow('Cannot use the $default key in a list of keys'); + }); + + test('can generate fromStatusRegExp', () => { + // GIVEN + const { key } = WebSocketIntegrationResponseKey.fromStatusRegExp(/4\d{2}/.source); + + // THEN + expect(key).toEqual('/4\\d{2}/'); + }); + + test('fromStatusRegExp throws if invalid RegExp', () => { + expect( + () => WebSocketIntegrationResponseKey.fromStatusRegExp('('), + ).toThrow(/Invalid regular expression/); + }); +}); + +describe('WebSocketIntegrationResponse from constructor', () => { + test('can create an integration response', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + api.addRoute('$default', { integration, returnResponse: true }); + + // WHEN + new WebSocketIntegrationResponse(stack, 'IntegrationResponse', { + integration, + responseKey: WebSocketIntegrationResponseKey.default, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'TEST', + IntegrationUri: 'https://example.com', + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteTestIntegration7AF569F1' }, + IntegrationResponseKey: '$default', + TemplateSelectionExpression: Match.absent(), + ContentHandling: Match.absent(), + ResponseParameters: Match.absent(), + ResponseTemplates: Match.absent(), + }); + }); + + test('throws if addRoute has not been ran', () => { + + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + + // THEN + expect(() => + new WebSocketIntegrationResponse(stack, 'IntegrationResponse', { + integration, + responseKey: WebSocketIntegrationResponseKey.default, + }), + ).toThrow(/This integration has not been associated to an API route/); + }); +}); + +describe('WebSocketIntegrationResponse from properties', () => { + test('can set an integration response', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + + api.addRoute('$default', { integration, returnResponse: true }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'TEST', + IntegrationUri: 'https://example.com', + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteTestIntegration7AF569F1' }, + IntegrationResponseKey: '$default', + TemplateSelectionExpression: Match.absent(), + ContentHandling: Match.absent(), + ResponseParameters: Match.absent(), + ResponseTemplates: Match.absent(), + }); + }); + + test('can set custom integration response properties', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { + responseKey: WebSocketIntegrationResponseKey.fromStatusCode(404), + contentHandling: ContentHandling.CONVERT_TO_BINARY, + responseParameters: { + 'method.response.header.Accept': "'application/json'", + }, + templateSelectionExpression: '$default', + responseTemplates: { + 'application/json': '{ "message": $context.error.message, "statusCode": 404 }', + }, + }, + ], + }); + + api.addRoute('$default', { integration, returnResponse: true }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'TEST', + IntegrationUri: 'https://example.com', + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteTestIntegration7AF569F1' }, + IntegrationResponseKey: '/404/', + ContentHandlingStrategy: 'CONVERT_TO_BINARY', + TemplateSelectionExpression: '$default', + ResponseParameters: { + 'method.response.header.Accept': "'application/json'", + }, + ResponseTemplates: { + 'application/json': '{ "message": $context.error.message, "statusCode": 404 }', + }, + }); + }); + + test('can add integration responses', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + integration.addResponse(WebSocketIntegrationResponseKey.clientError); + + api.addRoute('$default', { integration, returnResponse: true }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'TEST', + IntegrationUri: 'https://example.com', + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteTestIntegration7AF569F1' }, + IntegrationResponseKey: '$default', + TemplateSelectionExpression: Match.absent(), + ContentHandling: Match.absent(), + ResponseParameters: Match.absent(), + ResponseTemplates: Match.absent(), + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteTestIntegration7AF569F1' }, + IntegrationResponseKey: '/4\\d{2}/', + TemplateSelectionExpression: Match.absent(), + ContentHandling: Match.absent(), + ResponseParameters: Match.absent(), + ResponseTemplates: Match.absent(), + }); + }); + + test('throws if duplicate response key is set', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + + // THEN + expect( + () => api.addRoute('$default', { integration, returnResponse: true }), + ).toThrow(/Duplicate integration response key/); + }); + + test('throws if duplicate response key is added', () => { + // GIVEN + const stack = new Stack(); + const role = new iam.Role(stack, 'MyRole', { assumedBy: new iam.ServicePrincipal('foo') }); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + integration.addResponse(WebSocketIntegrationResponseKey.default); + + // THEN + expect( + () => api.addRoute('$default', { integration, returnResponse: true }), + ).toThrow(/Duplicate integration response key/); + }); + + test('throws if returnResponse is not set to true', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const integration = new WebSocketTestIntegration('TestIntegration', { + integrationUri: 'https://example.com', + responses: [ + { responseKey: WebSocketIntegrationResponseKey.default }, + ], + }); + + // THEN + expect( + () => api.addRoute('$default', { integration }), + ).toThrow(/Setting up integration responses without setting up returnResponse to true will have no effect/); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts index 8b3298ac425c1..ca8159e2165cb 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts @@ -1,6 +1,6 @@ import { WebSocketLambdaIntegration } from './../../lib/websocket/lambda'; import { Template } from '../../../assertions'; -import { ContentHandling, WebSocketApi } from '../../../aws-apigatewayv2'; +import { ContentHandling, WebSocketApi, WebSocketIntegrationResponseKey } from '../../../aws-apigatewayv2'; import { Code, Function } from '../../../aws-lambda'; import * as lambda from '../../../aws-lambda'; import { Duration, Stack } from '../../../core'; @@ -65,6 +65,7 @@ describe('LambdaWebSocketIntegration', () => { contentHandling: ContentHandling.CONVERT_TO_TEXT, }, ), + returnResponse: true, }, }); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/mock.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/mock.test.ts index 3f08838edfb32..70e0366efbb3a 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/mock.test.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2-integrations/test/websocket/mock.test.ts @@ -1,6 +1,6 @@ import { WebSocketMockIntegration } from './../../lib/websocket/mock'; import { Template } from '../../../assertions'; -import { WebSocketApi } from '../../../aws-apigatewayv2'; +import { WebSocketApi, WebSocketIntegrationResponseKey } from '../../../aws-apigatewayv2'; import { Stack } from '../../../core'; describe('MockWebSocketIntegration', () => { @@ -19,4 +19,30 @@ describe('MockWebSocketIntegration', () => { IntegrationUri: '', }); }); + + test('can set custom properties', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'Api', { + defaultRouteOptions: { + integration: new WebSocketMockIntegration('DefaultIntegration', { + responses: [{ responseKey: WebSocketIntegrationResponseKey.success }], + }), + returnResponse: true, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'MOCK', + IntegrationUri: '', + }); + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', { + ApiId: { Ref: 'ApiF70053CD' }, + IntegrationId: { Ref: 'ApidefaultRouteDefaultIntegrationE3602C1B' }, + IntegrationResponseKey: '/2\\d{2}/', + }); + }); }); diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/index.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/index.ts index 4fe65943cbb8b..68cfe69160291 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/index.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/index.ts @@ -2,4 +2,5 @@ export * from './api'; export * from './route'; export * from './stage'; export * from './integration'; +export * from './integration-response'; export * from './authorizer'; diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration-response.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration-response.ts new file mode 100644 index 0000000000000..dbda6a900ccef --- /dev/null +++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration-response.ts @@ -0,0 +1,258 @@ +import { Construct } from 'constructs'; +import { ContentHandling, WebSocketRouteIntegration } from './integration'; +import { IResource, Resource } from '../../../core'; +import { CfnIntegrationResponse } from '../apigatewayv2.generated'; + +/** + * WebSocket integration response key helper class + */ +export class WebSocketIntegrationResponseKey { + /** + * Match all responses + */ + public static default = new WebSocketIntegrationResponseKey('$default'); + + /** + * Match all 2xx responses (HTTP success codes) + */ + public static success = WebSocketIntegrationResponseKey.fromStatusRegExp(/2\d{2}/.source); + + /** + * Match all 4xx responses (HTTP client error codes) + */ + public static clientError = WebSocketIntegrationResponseKey.fromStatusRegExp(/4\d{2}/.source); + + /** + * Match all 5xx responses (HTTP server error codes) + */ + public static serverError = WebSocketIntegrationResponseKey.fromStatusRegExp(/5\d{2}/.source); + + /** + * Match 200 OK status code + */ + public static ok = WebSocketIntegrationResponseKey.fromStatusCode(200); + + /** + * Match 204 No Content status code + */ + public static noContent = WebSocketIntegrationResponseKey.fromStatusCode(204); + + /** + * Match 400 Bad Request status code + */ + public static badRequest = WebSocketIntegrationResponseKey.fromStatusCode(400); + + /** + * Match 401 Unauthorized status code + */ + public static unauthorized = WebSocketIntegrationResponseKey.fromStatusCode(401); + + /** + * Match 403 Forbidden status code + */ + public static forbidden = WebSocketIntegrationResponseKey.fromStatusCode(403); + + /** + * Match 404 Not Found status code + */ + public static notFound = WebSocketIntegrationResponseKey.fromStatusCode(404); + + /** + * Match 500 Internal Server Error status code + */ + public static internalServerError = WebSocketIntegrationResponseKey.fromStatusCode(500); + + /** + * Generate an integration response key from an HTTP status code + * + * @example + * // Match 409 Conflict status code + * apigwv2.WebSocketIntegrationResponseKey.fromStatusCode(409) + * + * @param httpStatusCode HTTP status code of the mapped response + */ + public static fromStatusCode(httpStatusCode: number): WebSocketIntegrationResponseKey { + return new WebSocketIntegrationResponseKey(`/${httpStatusCode}/`); + } + + /** + * Generate an integration response key from a list of keys + * @param keys keys to generate the key from + * + * @example + * // Match 200 OK, 201 Created, and all 4xx Client Error status codes + * apigwv2.WebSocketIntegrationResponseKey.fromKeys( + * apigwv2.WebSocketIntegrationResponseKey.ok, + * apigwv2.WebSocketIntegrationResponseKey.fromStatusCode(201), + * apigwv2.WebSocketIntegrationResponseKey.clientError + * ) + */ + public static fromKeys(...keys: WebSocketIntegrationResponseKey[]): WebSocketIntegrationResponseKey { + if (keys.includes(WebSocketIntegrationResponseKey.default)) { + throw new Error('Cannot use the $default key in a list of keys'); + } + + return WebSocketIntegrationResponseKey.fromStatusRegExp( + keys.map(({ key }) => key.slice(1, -1)).join('|'), + ); + } + + /** + * Generate an integration response key from a regular expression matching HTTP status codes + * + * @example + * // Match all HTTP client and server error status codes + * apigwv2.WebSocketIntegrationResponseKey.fromStatusRegExp('4\\d{2}|5\\d{2}') + * + * // Match all HTTP client and server error status codes, + * // using the RegExp built-in object + * apigwv2.WebSocketIntegrationResponseKey.fromStatusRegExp(/4\d{2}|5\d{2}/.source) + * + * @param httpStatusRegExpStr HTTP status code regular expression string representation + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp + * + * @throws an error if {@link httpStatusRegExpStr} is not a valid regular expression string + */ + public static fromStatusRegExp(httpStatusRegExpStr: string): WebSocketIntegrationResponseKey { + const httpStatusRegExp = new RegExp(httpStatusRegExpStr); + + return new WebSocketIntegrationResponseKey(`/${httpStatusRegExp.source}/`); + } + + /** + * WebSocket integration response private constructor + * + * @param key The key of the integration response + */ + private constructor(readonly key: string) {} + + /** String representation of the integration response key */ + public toString(): string { + return this.key; + } +} + +/** + * WebSocket integration response properties, used internally for Integration implementations + * The integration will add itself these props during the bind process + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ +export interface InternalWebSocketIntegrationResponseOptions { + /** + * The templates that are used to transform the integration response body. + * Specify templates as key-value pairs, with a content type as the key and + * a template as the value. + * + * @default - No response templates + */ + readonly responseTemplates?: { [contentType: string]: string }; + + /** + * Specifies how to handle response payload content type conversions. + * + * @default - The response payload will be passed through from the integration response to + * the route response or method response without modification. + */ + readonly contentHandling?: ContentHandling; + + /** + * The response parameters from the backend response that API Gateway sends + * to the method response. + * + * Use the destination as the key and the source as the value: + * + * - The destination must be an existing response parameter in the + * MethodResponse property. + * - The source must be an existing method request parameter or a static + * value. You must enclose static values in single quotation marks and + * pre-encode these values based on the destination specified in the + * request. + * + * @default - No response parameters + */ + readonly responseParameters?: { [key: string]: string }; + + /** + * The template selection expression for the integration response. + * + * @default - No template selection expression + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-data-transformations.html#apigateway-websocket-api-integration-response-selection-expressions + */ + readonly templateSelectionExpression?: string; +} + +/** + * WebSocket integration response properties, used internally for Integration implementations + * The integration will add itself these props during the bind process + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ +export interface InternalWebSocketIntegrationResponseProps extends InternalWebSocketIntegrationResponseOptions { + /** + * The HTTP status code or regular expression the response will be mapped to + */ + readonly responseKey: WebSocketIntegrationResponseKey; +} + +/** + * WebSocket integration response properties + * + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ +export interface WebSocketIntegrationResponseProps extends InternalWebSocketIntegrationResponseProps { + /** + * The WebSocket Integration to associate the response with + */ + readonly integration: WebSocketRouteIntegration; +} + +/** + * Represents an Integration Response for an WebSocket API. + */ +export interface IWebSocketIntegrationResponse extends IResource { + /** The WebSocket Integration associated with this Response */ + readonly integration: WebSocketRouteIntegration; + + /** + * Id of the integration response. + * @attribute + */ + readonly integrationResponseId: string; +} + +/** + * WebSocket Integration Response resource class + * @resource AWS::ApiGatewayV2::IntegrationResponse + */ +export class WebSocketIntegrationResponse extends Resource implements IWebSocketIntegrationResponse { + public readonly integrationResponseId: string; + public readonly integration: WebSocketRouteIntegration; + + /** + * Generate an array of WebSocket Integration Response resources from a map + * and associate them with a given WebSocket Integration + * + * @param scope The parent construct + * @param id The name of the integration response construct + * @param props The configuration properties to create WebSocket Integration Responses from + */ + constructor( + scope: Construct, + id: string, + props: WebSocketIntegrationResponseProps, + ) { + super(scope, id); + const { ref } = new CfnIntegrationResponse(this, 'Resource', { + apiId: props.integration.webSocketApiId, + integrationId: props.integration.integrationId, + integrationResponseKey: props.responseKey.key, + responseTemplates: props.responseTemplates, + contentHandlingStrategy: props.contentHandling, + responseParameters: props.responseParameters, + templateSelectionExpression: props.templateSelectionExpression, + }); + this.integrationResponseId = ref; + this.integration = props.integration; + } +} diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts index ab2ed9d55817c..4cc269637ad68 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/integration.ts @@ -1,9 +1,10 @@ import { Construct } from 'constructs'; import { IWebSocketApi } from './api'; +import { InternalWebSocketIntegrationResponseOptions, InternalWebSocketIntegrationResponseProps, WebSocketIntegrationResponse, WebSocketIntegrationResponseKey } from './integration-response'; import { IWebSocketRoute } from './route'; import { CfnIntegration } from '.././index'; import { IRole } from '../../../aws-iam'; -import { Duration, Resource } from '../../../core'; +import { Duration, Names, Resource } from '../../../core'; import { IIntegration } from '../common'; /** @@ -130,6 +131,7 @@ export interface WebSocketIntegrationProps { * ``` * * @default - No request templates required. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-mapping-template-reference.html */ readonly requestTemplates?: { [contentType: string]: string }; @@ -208,13 +210,20 @@ export interface WebSocketRouteIntegrationBindOptions { * this will be used as their parent scope. */ readonly scope: Construct; + + /** + * Should the route send a response to the client + * @default false + */ + readonly returnResponse?: boolean; } /** - * The interface that various route integration classes will inherit. + * The abstract class that all route integration classes will implement. */ export abstract class WebSocketRouteIntegration { - private integration?: WebSocketIntegration; + protected integration?: WebSocketIntegration; + protected config?: WebSocketRouteIntegrationConfig; /** * Initialize an integration for a route on websocket api. @@ -232,20 +241,20 @@ export abstract class WebSocketRouteIntegration { } if (!this.integration) { - const config = this.bind(options); + this.config = this.bind(options); this.integration = new WebSocketIntegration(options.scope, this.id, { webSocketApi: options.route.webSocketApi, - integrationType: config.type, - integrationUri: config.uri, - integrationMethod: config.method, - contentHandling: config.contentHandling, - credentialsRole: config.credentialsRole, - requestTemplates: config.requestTemplates, - requestParameters: config.requestParameters, - timeout: config.timeout, - passthroughBehavior: config.passthroughBehavior, - templateSelectionExpression: config.templateSelectionExpression, + integrationType: this.config.type, + integrationUri: this.config.uri, + integrationMethod: this.config.method, + contentHandling: this.config.contentHandling, + credentialsRole: this.config.credentialsRole, + requestTemplates: this.config.requestTemplates, + requestParameters: this.config.requestParameters, + timeout: this.config.timeout, + passthroughBehavior: this.config.passthroughBehavior, + templateSelectionExpression: this.config.templateSelectionExpression, }); } @@ -256,6 +265,101 @@ export abstract class WebSocketRouteIntegration { * Bind this integration to the route. */ public abstract bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig; + + /** + * The WebSocket API identifier + * @throws an error if this integration has not been bound to a route first + */ + public get webSocketApiId(): string { + if (!this.integration) { + throw new Error('This integration has not been associated to an API route'); + } + return this.integration.webSocketApi.apiId; + } + + /** + * The WebSocket Integration identifier + * @throws an error if this integration has not been bound to a route first + */ + public get integrationId(): string { + if (!this.integration) { + throw new Error('This integration has not been associated to an API route'); + } + + return this.integration.integrationId; + } +} + +/** + * The abstract class that two-way communication route integration classes + * with customized responses will implement. + */ +export abstract class CustomResponseWebSocketRoute extends WebSocketRouteIntegration { + private responses: InternalWebSocketIntegrationResponseProps[] = []; + + /** + * Initialize an integration for a route on websocket api. + * @param id id of the underlying `WebSocketIntegration` construct. + */ + constructor(id: string) { + super(id); + } + + /** + * Internal method called when binding this integration to the route. + * @internal + */ + public _bindToRoute(options: WebSocketRouteIntegrationBindOptions): { readonly integrationId: string } { + const requiresBinding = !this.integration; + const result = super._bindToRoute(options); + + if (requiresBinding) { + // This should never happen, super._bindToRoute must have set up the integration + if (!this.config || !this.integration) { + throw new Error('Missing integration setup during WebSocketRouteIntegration._bindToRoute'); + } + + this.responses.push(...this.config.responses ?? []); + if (this.responses.length && !options.returnResponse) { + // FIXME change to a warning? + throw new Error('Setting up integration responses without setting up returnResponse to true will have no effect, and is likely a mistake.'); + } + + this.responses.reduce<{ [key: string]: string }>((acc, props) => { + if (props.responseKey.key in acc) { + throw new Error(`Duplicate integration response key: "${props.responseKey.key}"`); + } + + const key = props.responseKey.key; + acc[key] = props.responseKey.key; + return acc; + }, {}); + + for (const responseProps of this.responses) { + const prefix = slugify(responseProps.responseKey.key); + + new WebSocketIntegrationResponse( + options.scope, + `${prefix}${Names.uniqueResourceName(this.integration, { maxLength: 256 - prefix.length })}`, + { ...responseProps, integration: this }, + ); + } + } + + return result; + } + + /** + * Add a response to this integration + * + * @param responseKey The response key to add + * @param options Optional properties to add to the response + */ + addResponse( + responseKey: WebSocketIntegrationResponseKey, + options: InternalWebSocketIntegrationResponseOptions = {}) { + this.responses.push({ ...options, responseKey }); + } } /** @@ -308,6 +412,14 @@ export interface WebSocketRouteIntegrationConfig { */ readonly requestParameters?: { [dest: string]: string }; + /** + * Integration responses configuration + * + * @default - No response configuration provided. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-responses.html + */ + readonly responses?: InternalWebSocketIntegrationResponseProps[]; + /** * Template selection expression * @@ -330,3 +442,7 @@ export interface WebSocketRouteIntegrationConfig { */ readonly passthroughBehavior?: PassthroughBehavior; } + +function slugify(x: string): string { + return x.replace(/[^a-zA-Z0-9]/g, ''); +} diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/route.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/route.ts index 0114e13c50e0d..e3e900b92cb1e 100644 --- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/route.ts +++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/websocket/route.ts @@ -75,10 +75,11 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { public readonly routeId: string; public readonly webSocketApi: IWebSocketApi; public readonly routeKey: string; - /** - * Integration response ID - */ + * Integration response ID + * + * @deprecated - Use `WebSocketIntegrationResponse` instead + */ public readonly integrationResponseId?: string; constructor(scope: Construct, id: string, props: WebSocketRouteProps) { @@ -94,6 +95,7 @@ export class WebSocketRoute extends Resource implements IWebSocketRoute { const config = props.integration._bindToRoute({ route: this, scope: this, + returnResponse: props.returnResponse, }); const authorizer = props.authorizer ?? new WebSocketNoneAuthorizer(); // must be explicitly NONE (not undefined) for stack updates to work correctly diff --git a/packages/aws-cdk-lib/awslint.json b/packages/aws-cdk-lib/awslint.json index 8db48224a89b1..aa2f446e0a418 100644 --- a/packages/aws-cdk-lib/awslint.json +++ b/packages/aws-cdk-lib/awslint.json @@ -899,10 +899,12 @@ "props-physical-name:aws-cdk-lib.aws_apigatewayv2.HttpRouteProps", "props-physical-name:aws-cdk-lib.aws_apigatewayv2.WebSocketIntegrationProps", "props-physical-name:aws-cdk-lib.aws_apigatewayv2.WebSocketRouteProps", + "props-physical-name:aws-cdk-lib.aws_apigatewayv2.WebSocketIntegrationResponseProps", "from-method:aws-cdk-lib.aws_apigatewayv2.HttpIntegration", "from-method:aws-cdk-lib.aws_apigatewayv2.HttpRoute", "from-method:aws-cdk-lib.aws_apigatewayv2.WebSocketIntegration", "from-method:aws-cdk-lib.aws_apigatewayv2.WebSocketRoute", + "from-method:aws-cdk-lib.aws_apigatewayv2.WebSocketIntegrationResponse", "docs-public-apis:aws-cdk-lib.aws_appconfig.ConfigurationSourceType.S3", "docs-public-apis:aws-cdk-lib.aws_appconfig.ConfigurationSourceType.SSM_DOCUMENT", "docs-public-apis:aws-cdk-lib.aws_appconfig.ConfigurationSourceType.SSM_PARAMETER",