diff --git a/java/http-proxy-apigateway/.gitignore b/java/http-proxy-apigateway/.gitignore
new file mode 100644
index 0000000000..1db21f1629
--- /dev/null
+++ b/java/http-proxy-apigateway/.gitignore
@@ -0,0 +1,13 @@
+.classpath.txt
+target
+.classpath
+.project
+.idea
+.settings
+.vscode
+*.iml
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
+
diff --git a/java/http-proxy-apigateway/README.md b/java/http-proxy-apigateway/README.md
new file mode 100644
index 0000000000..d06a316ae8
--- /dev/null
+++ b/java/http-proxy-apigateway/README.md
@@ -0,0 +1,56 @@
+# HTTP Proxy APIGateway
+
+
+
+---
+
+> **This is a stable example. It should successfully build out of the box**
+>
+> This example is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build.
+---
+
+
+
+This example creates an API Gateway with proxy resources for 2 HTTP backends.
+This is useful for scenarios when incoming requests must be routed to one or more backend API hosts.
+An HTTP proxy integration enables direct interactions between clients and backends without any intervention from the API Gateway after the API method is set up.
+
+> For more information on using HTTP proxy integrations with the APIGateway check out this [AWS tutorial](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-http.html).
+
+> For demonstration purposes this CDK example deploys a solution that routes to a couple of test HTTP APIs.
+> The 2 test HTTP APIs are implemented using lambdas exposed through function URLs.
+> This example can be modified though, if you prefer to use your own HTTP backend APIs.
+> To do that you can modify the `createHTTPTestAPIs` method in the [`HttpProxyApiGatewayStack`](src/main/java/com/myorg/HttpProxyApiGatewayStack.java) class to return a list of `ProxyResourceParameters` corresponding to your own resources.
+
+## Build
+
+To build this example, you need to be in this example's root directory. Then run the following:
+
+```bash
+ npm install -g aws-cdk
+ npm install
+ cdk synth
+```
+
+This will install the necessary CDK, then this example's dependencies, and then build the CloudFormation template. The resulting CloudFormation template will be in the `cdk.out` directory.
+
+## Deploy
+
+Run `cdk deploy`.
+This will deploy / redeploy the Stack to AWS.
+After the CDK deployment is successful, 2 URL examples will be available in the terminal console:
+
+- One for the `HttpProxyApiGatewayStack.HelloFunctionResourceExample` output
+- One for the `HttpProxyApiGatewayStack.ByeFunctionResourceExample` output
+
+At this point, you can copy each of the 2 URLs and paste them in the address bar of a browser to invoke the 2 APIs.
+Also note that both URLs have the same host (the DNS of the new API Gateway created).
+
+## Useful commands
+
+* `mvn package` compile and run tests
+* `cdk ls` list all stacks in the app
+* `cdk synth` emits the synthesized CloudFormation template
+* `cdk deploy` deploy this stack to your default AWS account/region
+* `cdk diff` compare deployed stack with current state
+* `cdk docs` open CDK documentation
diff --git a/java/http-proxy-apigateway/cdk.json b/java/http-proxy-apigateway/cdk.json
new file mode 100644
index 0000000000..1ce7118872
--- /dev/null
+++ b/java/http-proxy-apigateway/cdk.json
@@ -0,0 +1,76 @@
+{
+ "app": "mvn -e -q compile exec:java",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "target",
+ "pom.xml",
+ "src/test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
+ "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
+ "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
+ "@aws-cdk/aws-kms:aliasNameRef": true,
+ "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
+ "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
+ "@aws-cdk/aws-efs:denyAnonymousAccess": true,
+ "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
+ "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
+ "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
+ "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
+ "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
+ "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
+ "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
+ "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
+ "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
+ "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
+ "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
+ "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
+ "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
+ "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
+ "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
+ "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
+ "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
+ "@aws-cdk/aws-ec2:ec2SumTimeoutEnabled": true,
+ "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
+ "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
+ "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
+ "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
+ "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true
+ }
+}
diff --git a/java/http-proxy-apigateway/pom.xml b/java/http-proxy-apigateway/pom.xml
new file mode 100644
index 0000000000..ade7ee1e74
--- /dev/null
+++ b/java/http-proxy-apigateway/pom.xml
@@ -0,0 +1,62 @@
+
+
+ 4.0.0
+
+ com.myorg
+ http-proxy-apigateway
+ 0.1
+
+
+ UTF-8
+ 2.171.1
+ [10.0.0,11.0.0)
+ 5.7.1
+ 22
+ 22
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.myorg.HttpProxyApiGatewayApp
+
+
+
+
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+ ${cdk.version}
+
+
+
+ software.constructs
+ constructs
+ ${constructs.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
diff --git a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
new file mode 100644
index 0000000000..3c13b21c21
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayApp.java
@@ -0,0 +1,15 @@
+package com.myorg;
+
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+
+public class HttpProxyApiGatewayApp {
+ public static void main(final String[] args) {
+ var app = new App();
+ var stackProps = StackProps.builder()
+ .stackName("HttpProxyApiGatewayStack")
+ .build();
+ new HttpProxyApiGatewayStack(app, "HttpProxyApiGatewayStack", stackProps);
+ app.synth();
+ }
+}
diff --git a/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
new file mode 100644
index 0000000000..3597280eee
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/java/com/myorg/HttpProxyApiGatewayStack.java
@@ -0,0 +1,117 @@
+package com.myorg;
+
+import software.amazon.awscdk.CfnOutput;
+import software.amazon.awscdk.RemovalPolicy;
+import software.amazon.awscdk.Stack;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.services.apigateway.*;
+import software.amazon.awscdk.services.lambda.*;
+import software.constructs.Construct;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static software.amazon.awscdk.services.apigatewayv2.HttpMethod.ANY;
+import static software.amazon.awscdk.services.lambda.Runtime.PYTHON_3_12;
+
+public class HttpProxyApiGatewayStack extends Stack {
+
+ public record ProxyResourceParameters(String resourceId, String baseUrl, String httpMethod, String exampleRequest) {
+
+ }
+
+ private final RestApi restApi;
+
+ public HttpProxyApiGatewayStack(final Construct scope, final String id, final StackProps props) {
+ super(scope, id, props);
+ restApi = RestApi.Builder.create(this, "RestApi")
+ .restApiName("RestApi")
+ .cloudWatchRole(true)
+ .cloudWatchRoleRemovalPolicy(RemovalPolicy.DESTROY)
+ .endpointTypes(List.of(EndpointType.EDGE))
+ .build();
+ createHTTPTestAPIs().forEach(this::createProxyResource);
+ }
+
+ private List createHTTPTestAPIs() {
+ return Map.of(
+ "HelloFunction", "/hello?from=AWS",
+ "ByeFunction", "/bye?from=AWS"
+ ).entrySet().stream()
+ .map(functionEntry -> {
+ var functionName = functionEntry.getKey();
+ var parameters = functionEntry.getValue();
+ var function = Function.Builder.create(this, functionName)
+ .functionName(functionName)
+ .code(InlineCode.fromInline(getInlineCode("src/main/resources/lambdas/" + functionName + ".py")))
+ .handler("index.handler")
+ .runtime(PYTHON_3_12)
+ .build();
+ var lambdaFunctionAlias = Alias.Builder.create(this, functionName + "ProdAlias")
+ .aliasName("Prod")
+ .version(function.getCurrentVersion())
+ .build();
+ var functionURL = FunctionUrl.Builder.create(this, functionName + "Url")
+ .function(lambdaFunctionAlias)
+ .authType(FunctionUrlAuthType.NONE)
+ .invokeMode(InvokeMode.BUFFERED)
+ .cors(
+ FunctionUrlCorsOptions.builder()
+ .allowedOrigins(List.of("*"))
+ .allowedMethods(List.of(HttpMethod.GET))
+ .allowedHeaders(List.of("*"))
+ .build()
+ )
+ .build();
+ return new ProxyResourceParameters(functionName + "Resource", functionURL.getUrl(), ANY.name(), parameters);
+ }).collect(ArrayList::new, List::add, List::addAll);
+ }
+
+ private void createProxyResource(ProxyResourceParameters proxyResourceParameters) {
+ var parentProxyResource = restApi.getRoot().addResource(proxyResourceParameters.resourceId);
+ var proxyResource = ProxyResource.Builder.create(this, proxyResourceParameters.resourceId + "ProxyResource")
+ .parent(parentProxyResource)
+ .anyMethod(false)
+ .build();
+ var integrationOptions = IntegrationOptions.builder()
+ .requestParameters(
+ Map.of(
+ "integration.request.path.proxy", "method.request.path.proxy"
+ )
+ )
+ .build();
+ var httpIntegrationProps = HttpIntegrationProps.builder()
+ .proxy(true)
+ .httpMethod(proxyResourceParameters.httpMethod)
+ .options(integrationOptions)
+ .build();
+ var methodOptions = MethodOptions.builder()
+ .requestParameters(
+ Map.of(
+ "method.request.path.proxy", true
+ )
+ )
+ .build();
+ var httpIntegration = new HttpIntegration(proxyResourceParameters.baseUrl + "{proxy}", httpIntegrationProps);
+ proxyResource.addMethod(proxyResourceParameters.httpMethod, httpIntegration, methodOptions);
+ var proxyResourceUrl = restApi.urlForPath(proxyResource.getPath());
+ CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "ProxyUrl")
+ .value(proxyResourceUrl)
+ .build();
+ CfnOutput.Builder.create(this, proxyResourceParameters.resourceId + "Example")
+ .value(proxyResourceUrl.replace("/{proxy+}", proxyResourceParameters.exampleRequest))
+ .build();
+ }
+
+ private String getInlineCode(String path) {
+ try {
+ return new String(Files.readAllBytes(Path.of(path)));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py b/java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py
new file mode 100644
index 0000000000..b75e729d22
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/resources/lambdas/ByeFunction.py
@@ -0,0 +1,7 @@
+import json
+def handler(event, context):
+ fromValue = event.get('queryStringParameters', {}).get('from', 'Lambda')
+ return {
+ 'statusCode': 200,
+ 'body': json.dumps('Bye from ' + fromValue + '!')
+ }
diff --git a/java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py b/java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py
new file mode 100644
index 0000000000..847668e69c
--- /dev/null
+++ b/java/http-proxy-apigateway/src/main/resources/lambdas/HelloFunction.py
@@ -0,0 +1,7 @@
+import json
+def handler(event, context):
+ fromValue = event.get('queryStringParameters', {}).get('from', 'Lambda')
+ return {
+ 'statusCode': 200,
+ 'body': json.dumps('Hello from ' + fromValue + '!')
+ }
diff --git a/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java b/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
new file mode 100644
index 0000000000..e1d06d774c
--- /dev/null
+++ b/java/http-proxy-apigateway/src/test/java/com/myorg/HttpProxyApiGatewayTest.java
@@ -0,0 +1,334 @@
+package com.myorg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.StringUtils;
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.cxapi.CloudFormationStackArtifact;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+import static com.myorg.TestUtils.*;
+
+public class HttpProxyApiGatewayTest {
+
+ private static final Map stackResourcesMap = new HashMap<>();
+
+ @BeforeAll
+ public static void setUp() {
+ var app = App.Builder.create().build();
+ var stackProps = StackProps.builder()
+ .stackName("HttpProxyApiGatewayStack")
+ .build();
+ var httpProxyApiGatewayStack = new HttpProxyApiGatewayStack(app, "HttpProxyApiGatewayStack", stackProps);
+ Optional.of(app)
+ .map(App::synth)
+ .flatMap(
+ cloudAssembly -> Optional.of(httpProxyApiGatewayStack)
+ .map(HttpProxyApiGatewayStack::getArtifactId)
+ .map(cloudAssembly::getStackArtifact)
+ .map(CloudFormationStackArtifact::getTemplate)
+ ).flatMap(
+ template -> Optional.of(new ObjectMapper())
+ .map(objectMapper -> objectMapper.valueToTree(template))
+ .map(jsonNode -> jsonNode.at("/Resources"))
+ .map(JsonNode::fields)
+ ).ifPresent(stackResourceIterator -> stackResourceIterator.forEachRemaining(
+ stackResourceEntry -> {
+ if (stackResourceEntry != null &&
+ StringUtils.isNotBlank(stackResourceEntry.getKey()) &&
+ stackResourceEntry.getValue() != null &&
+ !stackResourceEntry.getValue().isEmpty()
+ ) {
+ stackResourcesMap.put(stackResourceEntry.getKey(), stackResourceEntry.getValue());
+ }
+ })
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected REST API is present in the resources of the stack.")
+ public void testRestApi() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi",
+ "/Properties/EndpointConfiguration/Types/0", "EDGE",
+ "/Properties/Name", "RestApi"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, restApiMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected IAM role for the REST API is present in the resources of the stack.")
+ public void testRestAPIIamRole() {
+ var iamRoleMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Effect", "Allow",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Principal/Service", "apigateway.amazonaws.com",
+ "/Properties/AssumeRolePolicyDocument/Version", "2012-10-17",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/0", "",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/0", "arn:",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/1/Ref", "AWS::Partition",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
+ "/DeletionPolicy", "Delete"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, iamRoleMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway account is present in the resources of the stack.")
+ public void testApiGatewayAccount() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var iamRoleMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Effect", "Allow",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Principal/Service", "apigateway.amazonaws.com",
+ "/Properties/AssumeRolePolicyDocument/Version", "2012-10-17",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/0", "",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/0", "arn:",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/1/Ref", "AWS::Partition",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
+ "/DeletionPolicy", "Delete"
+ );
+ var iamRoleId = findResourceId(stackResourcesMap, iamRoleMatchMap);
+ var apiGatewayAccountMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Account",
+ "/Properties/CloudWatchRoleArn/Fn::GetAtt/0", iamRoleId,
+ "/Properties/CloudWatchRoleArn/Fn::GetAtt/1", "Arn",
+ "/DependsOn/0", restApiId,
+ "/UpdateReplacePolicy", "Delete",
+ "/DeletionPolicy", "Delete"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayAccountMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway deployment is present in the resources of the stack.")
+ public void testApiGatewayDeployment() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayDeploymentMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Deployment",
+ "/Properties/RestApiId/Ref", restApiId
+ );
+ var apiGatewayDeployment = findResource(stackResourcesMap, apiGatewayDeploymentMatchMap);
+ Assertions.assertNotNull(apiGatewayDeployment);
+ var dependsOnActualIds = extractAsStringArray(apiGatewayDeployment, "/DependsOn");
+ var dependsOnExpectedIds = Stream.of(
+ findResourcesIds(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Resource")),
+ findResourcesIds(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Method"))
+ )
+ .flatMap(Set::stream)
+ .collect(HashSet::new, Set::add, Set::addAll);
+ Assertions.assertEquals(dependsOnExpectedIds, dependsOnActualIds);
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway stage is present in the resources of the stack.")
+ public void testApiGatewayStage() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayDeploymentMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Deployment"
+ );
+ var apiGatewayDeploymentId = findResourceId(stackResourcesMap, apiGatewayDeploymentMatchMap);
+ var apiGatewayAccount = Map.of(
+ "/Type", "AWS::ApiGateway::Account"
+ );
+ var apiGatewayAccountId = findResourceId(stackResourcesMap, apiGatewayAccount);
+ var stageMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Stage",
+ "/Properties/RestApiId/Ref", restApiId,
+ "/Properties/DeploymentId/Ref", apiGatewayDeploymentId,
+ "/Properties/StageName", "prod",
+ "/DependsOn/0", apiGatewayAccountId
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, stageMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway resources are present in the resources of the stack.")
+ public void testApiGatewayResources() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayResourcesMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Resource",
+ "/Properties/RestApiId/Ref", restApiId
+ );
+ Long expectedApiGatewayResources = 4L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, apiGatewayResourcesMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(resourcesCont -> resourcesCont.equals(expectedApiGatewayResources))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway methods are present in the resources of the stack.")
+ public void testApiGatewayMethods() {
+ var restApiMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::RestApi"
+ );
+ var restApiId = findResourceId(stackResourcesMap, restApiMatchMap);
+ var apiGatewayMethodsMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Method",
+ "/Properties/AuthorizationType", "NONE",
+ "/Properties/HttpMethod", "ANY",
+ "/Properties/Integration/IntegrationHttpMethod", "ANY",
+ "/Properties/Integration/RequestParameters/integration.request.path.proxy", "method.request.path.proxy",
+ "/Properties/Integration/Type", "HTTP_PROXY",
+ "/Properties/RequestParameters/method.request.path.proxy", "true",
+ "/Properties/RestApiId/Ref", restApiId
+ );
+ Long expectedApiGatewayMethodsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, apiGatewayMethodsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedApiGatewayMethodsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected IAM roles for the test lambdas are present in the resources of the stack.")
+ public void testLambdaFunctionsIAMRoles() {
+ var lambdaFunctionsIAMRolesMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Action", "sts:AssumeRole",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Effect", "Allow",
+ "/Properties/AssumeRolePolicyDocument/Statement/0/Principal/Service", "lambda.amazonaws.com",
+ "/Properties/AssumeRolePolicyDocument/Version", "2012-10-17",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/0", "",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/0", "arn:",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/1/Ref", "AWS::Partition",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ );
+ Long expectedLambdaFunctionsIAMRolesCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaFunctionsIAMRolesMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaFunctionsIAMRolesCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda functions are present in the resources of the stack.")
+ public void testLambdaFunctions() {
+ var lambdaFunctionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Function",
+ "/Properties/Runtime", "python3.12"
+ );
+ Long expectedLambdaFunctionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaFunctionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaFunctionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda versions are present in the resources of the stack.")
+ public void testLambdaVersions() {
+ var lambdaVersionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Version"
+ );
+ Long expectedLambdaVersionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaVersionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaVersionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda aliases are present in the resources of the stack.")
+ public void testLambdaAliases() {
+ var lambdaAliasesMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Alias",
+ "/Properties/Name", "Prod"
+ );
+ Long expectedLambdaAliasesCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaAliasesMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaAliasesCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda permissions are present in the resources of the stack.")
+ public void testLambdaPermissions() {
+ var lambdaPermissionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Permission",
+ "/Properties/Action", "lambda:InvokeFunctionUrl",
+ "/Properties/FunctionUrlAuthType", "NONE",
+ "/Properties/Principal", "*"
+ );
+ Long expectedLambdaPermissionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaPermissionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaPermissionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda function URLs are present in the resources of the stack.")
+ public void testLambdaFunctionURLs() {
+ var lambdaFunctionsURLsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Url",
+ "/Properties/AuthType", "NONE",
+ "/Properties/InvokeMode", "BUFFERED",
+ "/Properties/Qualifier", "Prod",
+ "/Properties/Cors/AllowHeaders/0", "*",
+ "/Properties/Cors/AllowMethods/0", "GET",
+ "/Properties/Cors/AllowOrigins/0", "*"
+ );
+ Long expectedLambdaFunctionsURLsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaFunctionsURLsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(methodsCont -> methodsCont.equals(expectedLambdaFunctionsURLsCount))
+ .isPresent()
+ );
+ }
+
+}
diff --git a/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java b/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
new file mode 100644
index 0000000000..dc945f3f79
--- /dev/null
+++ b/java/http-proxy-apigateway/src/test/java/com/myorg/TestUtils.java
@@ -0,0 +1,81 @@
+package com.myorg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static org.junit.platform.commons.util.StringUtils.isNotBlank;
+
+public class TestUtils {
+
+ public static String findResourceId(Map stackResources, Map matchMap) {
+ return Optional.ofNullable(findResource(stackResources, matchMap))
+ .map(Entry::getKey)
+ .orElse(null);
+ }
+
+ public static Entry findResource(Map stackResources, Map matchMap) {
+ return stackResources.entrySet().stream()
+ .filter(createResourcePredicate(matchMap))
+ .findAny()
+ .orElse(null);
+ }
+
+ public static Set findResourcesIds(Map stackResources, Map matchMap) {
+ return Optional.ofNullable(findResources(stackResources, matchMap))
+ .map(Map::keySet)
+ .orElse(null);
+ }
+
+ public static Map findResources(Map stackResources, Map matchMap) {
+ return stackResources.entrySet().stream()
+ .filter(createResourcePredicate(matchMap))
+ .collect(Collectors.toMap(
+ Entry::getKey,
+ Entry::getValue
+ ));
+ }
+
+ public static Set extractAsStringArray(Entry stackResource, String jsonPath) {
+ return Optional.ofNullable(stackResource)
+ .map(Entry::getValue)
+ .flatMap(
+ resource -> Optional.ofNullable(jsonPath)
+ .map(resource::at)
+ .map(JsonNode::elements)
+ .map(iterator -> {
+ var set = new HashSet();
+ iterator.forEachRemaining(item -> set.add(item.asText()));
+ return set;
+ })
+ ).orElseGet(HashSet::new);
+ }
+
+ public static Predicate> createResourcePredicate(String expectedJsonPath, String expectedJsonValue) {
+ return stackResourceEntry -> Optional.ofNullable(stackResourceEntry)
+ .map(Entry::getValue)
+ .flatMap(
+ resource -> Optional.ofNullable(expectedJsonPath)
+ .map(resource::at)
+ .map(JsonNode::asText)
+ ).filter(propertyValue -> propertyValue.equals(expectedJsonValue))
+ .isPresent();
+ }
+
+ public static Predicate> createResourcePredicate(Map matchMap) {
+ return Optional.ofNullable(matchMap)
+ .map(Map::entrySet)
+ .flatMap(
+ matchEntrySet -> matchEntrySet.stream()
+ .filter(Objects::nonNull)
+ .filter(matchEntry -> isNotBlank(matchEntry.getKey()))
+ .filter(matchEntry -> isNotBlank(matchEntry.getValue()))
+ .map(matchEntry -> createResourcePredicate(matchEntry.getKey(), matchEntry.getValue()))
+ .reduce(Predicate::and)
+ ).orElseGet(() -> entry -> false);
+}
+
+}