From 24b48c0ec8b7d36bdd4298f5ce59761feae529f6 Mon Sep 17 00:00:00 2001
From: sobrab <170367248+sobrab@users.noreply.github.com>
Date: Sun, 22 Sep 2024 12:24:26 +0300
Subject: [PATCH] added a java example for an ApiGateway backed by a lambda
behind a Cognito user pool
---
java/cognito-api-lambda/.gitignore | 13 +
java/cognito-api-lambda/README.md | 55 ++++
java/cognito-api-lambda/cdk.json | 68 +++++
java/cognito-api-lambda/pom.xml | 60 +++++
.../java/com/myorg/CognitoApiLambdaApp.java | 14 ++
.../java/com/myorg/CognitoApiLambdaStack.java | 76 ++++++
.../src/main/resources/lambda/hello-world.py | 8 +
.../java/com/myorg/CognitoApiLambdaTest.java | 237 ++++++++++++++++++
.../src/test/java/com/myorg/TestUtils.java | 61 +++++
9 files changed, 592 insertions(+)
create mode 100644 java/cognito-api-lambda/.gitignore
create mode 100644 java/cognito-api-lambda/README.md
create mode 100644 java/cognito-api-lambda/cdk.json
create mode 100644 java/cognito-api-lambda/pom.xml
create mode 100644 java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaApp.java
create mode 100644 java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaStack.java
create mode 100644 java/cognito-api-lambda/src/main/resources/lambda/hello-world.py
create mode 100644 java/cognito-api-lambda/src/test/java/com/myorg/CognitoApiLambdaTest.java
create mode 100644 java/cognito-api-lambda/src/test/java/com/myorg/TestUtils.java
diff --git a/java/cognito-api-lambda/.gitignore b/java/cognito-api-lambda/.gitignore
new file mode 100644
index 0000000000..1db21f1629
--- /dev/null
+++ b/java/cognito-api-lambda/.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/cognito-api-lambda/README.md b/java/cognito-api-lambda/README.md
new file mode 100644
index 0000000000..82b127eecb
--- /dev/null
+++ b/java/cognito-api-lambda/README.md
@@ -0,0 +1,55 @@
+# APIGateway backed by Lambda and protected by a Cognito User Pool.
+
+
+
+
+---
+
+
+
+> **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 an example of an APIGateway that is protected with a Cognito User Pool, pointing to a Hello World Lambda.
+
+## 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 deployment, the URL of the Rest API created will be available in the outputs of the CloudFormation stack and can be used to invoke the lambda function.
+At this point, if an HTTP GET request is attempted on the Rest API without including and `Authorization` header, a `401 - Unauthorized` response will be returned.
+In order for the authorization to succeed when the lambda function is invoked through the API Gateway, each request must include an `Authorization` HTTP header containing an access token obtained for the specific user from the user pool.
+
+## The Component Structure
+
+The main resources of the component are:
+
+- A Lambda Function that returns the string "Hello world!"
+- A Rest API with a GET method that points to the Lambda Function
+- A Cognito User Pool
+- An Authorizer for the Rest API with the User Pool attached.
+
+## 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/cognito-api-lambda/cdk.json b/java/cognito-api-lambda/cdk.json
new file mode 100644
index 0000000000..e94ff85128
--- /dev/null
+++ b/java/cognito-api-lambda/cdk.json
@@ -0,0 +1,68 @@
+{
+ "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
+ }
+}
diff --git a/java/cognito-api-lambda/pom.xml b/java/cognito-api-lambda/pom.xml
new file mode 100644
index 0000000000..35197d226f
--- /dev/null
+++ b/java/cognito-api-lambda/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ com.myorg
+ cognito-api-lambda
+ 0.1
+
+
+ UTF-8
+ 2.158.0
+ [10.0.0,11.0.0)
+ 5.7.1
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.myorg.CognitoApiLambdaApp
+
+
+
+
+
+
+
+
+ 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/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaApp.java b/java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaApp.java
new file mode 100644
index 0000000000..0db8c2b64d
--- /dev/null
+++ b/java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaApp.java
@@ -0,0 +1,14 @@
+package com.myorg;
+
+import software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+
+public class CognitoApiLambdaApp {
+ public static void main(final String... args) {
+ App app = new App();
+ StackProps stackProps = StackProps.builder().build();
+ new CognitoApiLambdaStack(app, "CognitoApiLambdaStack", stackProps);
+ app.synth();
+ }
+}
+
diff --git a/java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaStack.java b/java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaStack.java
new file mode 100644
index 0000000000..5a040a82c1
--- /dev/null
+++ b/java/cognito-api-lambda/src/main/java/com/myorg/CognitoApiLambdaStack.java
@@ -0,0 +1,76 @@
+package com.myorg;
+
+import org.jetbrains.annotations.NotNull;
+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.cognito.SignInAliases;
+import software.amazon.awscdk.services.cognito.UserPool;
+import software.amazon.awscdk.services.lambda.InlineCode;
+import software.amazon.awscdk.services.lambda.Runtime;
+import software.amazon.awscdk.services.lambda.SingletonFunction;
+import software.constructs.Construct;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+public class CognitoApiLambdaStack extends Stack {
+
+ private record Authorizer(CfnAuthorizer authorizer) implements IAuthorizer {
+ @Override
+ public @NotNull String getAuthorizerId() {
+ return authorizer.getRef();
+ }
+ }
+
+ public CognitoApiLambdaStack(final Construct scope, final String id, final StackProps props) {
+ super(scope, id, props);
+ var helloWorldFunction = SingletonFunction.Builder.create(this, "helloWorldFunction")
+ .functionName("helloWorldFunction")
+ .code(InlineCode.fromInline(getInlineCode()))
+ .handler("index.handler")
+ .runtime(Runtime.PYTHON_3_12)
+ .uuid("")
+ .build();
+ var helloWorldLambdaRestApi = LambdaRestApi.Builder.create(this, "helloWorldLambdaRestApi")
+ .restApiName("Hello World API")
+ .cloudWatchRole(true)
+ .cloudWatchRoleRemovalPolicy(RemovalPolicy.DESTROY)
+ .handler(helloWorldFunction)
+ .proxy(false)
+ .build();
+ var signInAliases = SignInAliases.builder()
+ .email(true)
+ .build();
+ var userPool = UserPool.Builder.create(this, "userPool")
+ .signInAliases(signInAliases)
+ .removalPolicy(RemovalPolicy.DESTROY)
+ .build();
+ var authorizer = new Authorizer(
+ CfnAuthorizer.Builder.create(this, "cfnAuth")
+ .restApiId(helloWorldLambdaRestApi.getRestApiId())
+ .name("HelloWorldAPIAuthorizer")
+ .type("COGNITO_USER_POOLS")
+ .identitySource("method.request.header.Authorization")
+ .providerArns(List.of(userPool.getUserPoolArn()))
+ .build()
+ );
+ var helloResource = helloWorldLambdaRestApi.getRoot().addResource("HELLO");
+ var methodOptions = MethodOptions.builder()
+ .authorizationType(AuthorizationType.COGNITO)
+ .authorizer(authorizer)
+ .build();
+ helloResource.addMethod("GET", new LambdaIntegration(helloWorldFunction), methodOptions);
+ }
+
+ private String getInlineCode() {
+ try {
+ return new String(Files.readAllBytes(Path.of("src/main/resources/lambda/hello-world.py")));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/java/cognito-api-lambda/src/main/resources/lambda/hello-world.py b/java/cognito-api-lambda/src/main/resources/lambda/hello-world.py
new file mode 100644
index 0000000000..cd353c8367
--- /dev/null
+++ b/java/cognito-api-lambda/src/main/resources/lambda/hello-world.py
@@ -0,0 +1,8 @@
+import json
+
+def handler(event, context):
+ print(event)
+ return {
+ 'statusCode': 200,
+ 'body': 'Hello world!'
+ }
diff --git a/java/cognito-api-lambda/src/test/java/com/myorg/CognitoApiLambdaTest.java b/java/cognito-api-lambda/src/test/java/com/myorg/CognitoApiLambdaTest.java
new file mode 100644
index 0000000000..e7ec144267
--- /dev/null
+++ b/java/cognito-api-lambda/src/test/java/com/myorg/CognitoApiLambdaTest.java
@@ -0,0 +1,237 @@
+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 software.amazon.awscdk.App;
+import software.amazon.awscdk.StackProps;
+import software.amazon.awscdk.cxapi.CloudFormationStackArtifact;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static com.myorg.TestUtils.*;
+import static org.junit.platform.commons.util.StringUtils.isNotBlank;
+
+public class CognitoApiLambdaTest {
+
+ private static final Map stackResourcesMap = new HashMap<>();
+
+ @BeforeAll
+ public static void setUp() {
+ var app = App.Builder.create().build();
+ var stackProps = StackProps.builder().build();
+ var cognitoApiLambdaStack = new CognitoApiLambdaStack(app, "test", stackProps);
+ Optional.of(app)
+ .map(App::synth)
+ .flatMap(
+ cloudAssembly -> Optional.of(cognitoApiLambdaStack)
+ .map(CognitoApiLambdaStack::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 &&
+ isNotBlank(stackResourceEntry.getKey()) &&
+ stackResourceEntry.getValue() != null &&
+ !stackResourceEntry.getValue().isEmpty()
+ ) {
+ stackResourcesMap.put(stackResourceEntry.getKey(), stackResourceEntry.getValue());
+ }
+ })
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected IAM role required for the lambda function is present in the resources of the stack.")
+ public void testLambdaFunctionRole() {
+ var lambdaIAMRoleMatchMap = 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/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, lambdaIAMRoleMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected lambda function is present in the resources of the stack.")
+ public void testLambdaFunction() {
+ var lambdaIAMRoleMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+ );
+ var lambdaIAMRoleId = findResourceId(stackResourcesMap, lambdaIAMRoleMatchMap);
+ var lambdaMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Function",
+ "/Properties/FunctionName", "helloWorldFunction",
+ "/Properties/Role/Fn::GetAtt/0", lambdaIAMRoleId,
+ "/Properties/Handler", "index.handler",
+ "/Properties/Runtime", "python3.12"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, lambdaMatchMap));
+ }
+
+ @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/Name", "Hello World API"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, restAPIMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected IAM Role required for the Rest API is present in the resources of the stack.")
+ public void testRestApiRole() {
+ var restAPIRoleMatchMap = 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/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, restAPIRoleMatchMap));
+ }
+
+ @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 restAPIRoleMatchMap = Map.of(
+ "/Type", "AWS::IAM::Role",
+ "/Properties/ManagedPolicyArns/0/Fn::Join/1/2", ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
+ );
+ var restAPIRoleId = findResourceId(stackResourcesMap, restAPIRoleMatchMap);
+ var apiGatewayAccountMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Account",
+ "/Properties/CloudWatchRoleArn/Fn::GetAtt/0", restAPIRoleId,
+ "/DependsOn/0", restAPIId
+ );
+ 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 apiGatewayResourceId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Resource"));
+ var apiGatewayMethodId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Method"));
+ var restAPIId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::RestApi"));
+ var apiGatewayDeploymentMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Deployment",
+ "/Properties/RestApiId/Ref", restAPIId,
+ "/DependsOn/0", apiGatewayMethodId,
+ "/DependsOn/1", apiGatewayResourceId
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayDeploymentMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway stage is present in the resources of the stack.")
+ public void testAPIGatewayStage() {
+ var apiGatewayAccountId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Account"));
+ var apiGatewayDeploymentId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Deployment"));
+ var restAPIId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::RestApi"));
+ var apiGatewayStageMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Stage",
+ "/Properties/DeploymentId/Ref", apiGatewayDeploymentId,
+ "/Properties/RestApiId/Ref", restAPIId,
+ "/Properties/StageName", "prod",
+ "/DependsOn/0", apiGatewayAccountId
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayStageMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway resource is present in the resources of the stack.")
+ public void testAPIGatewayResource() {
+ var restAPIId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::RestApi"));
+ var apiGatewayResourceMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Resource",
+ "/Properties/ParentId/Fn::GetAtt/0", restAPIId,
+ "/Properties/PathPart", "HELLO",
+ "/Properties/RestApiId/Ref", restAPIId
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayResourceMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected Lambda permissions are present in the resources of the stack.")
+ public void testLambdaPermissions() {
+ var lambdaFunctionId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::Lambda::Function"));
+ var lambdaPermissionsMatchMap = Map.of(
+ "/Type", "AWS::Lambda::Permission",
+ "/Properties/Action", "lambda:InvokeFunction",
+ "/Properties/FunctionName/Fn::GetAtt/0", lambdaFunctionId
+ );
+ Long expectedPermissionsCount = 2L;
+ Assertions.assertTrue(
+ Optional.ofNullable(findResources(stackResourcesMap, lambdaPermissionsMatchMap))
+ .map(Map::entrySet)
+ .map(Set::stream)
+ .map(Stream::count)
+ .filter(permissionsCont -> permissionsCont.equals(expectedPermissionsCount))
+ .isPresent()
+ );
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway method is present in the resources of the stack.")
+ public void testAPIGatewayMethod() {
+ var apiGatewayResourceId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Resource"));
+ var restAPIId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::RestApi"));
+ var authorizerId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::Authorizer"));
+ var apiGatewayMethodMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Method",
+ "/Properties/AuthorizationType", "COGNITO_USER_POOLS",
+ "/Properties/AuthorizerId/Ref", authorizerId,
+ "/Properties/HttpMethod", "GET",
+ "/Properties/ResourceId/Ref", apiGatewayResourceId,
+ "/Properties/RestApiId/Ref", restAPIId
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayMethodMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected Cognito user pool is present in the resources of the stack.")
+ public void testCognitoUserPool() {
+ var cognitoUserPoolMatchMap = Map.of(
+ "/Type", "AWS::Cognito::UserPool",
+ "/Properties/AutoVerifiedAttributes/0", "email",
+ "/Properties/UsernameAttributes/0", "email"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, cognitoUserPoolMatchMap));
+ }
+
+ @Test
+ @DisplayName("Test if the expected API Gateway authorizer is present in the resources of the stack.")
+ public void testAPIGatewayAuthorizer() {
+ var restAPIId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::ApiGateway::RestApi"));
+ var cognitoUserPoolId = findResourceId(stackResourcesMap, Map.of("/Type", "AWS::Cognito::UserPool"));
+ var apiGatewayAuthorizerMatchMap = Map.of(
+ "/Type", "AWS::ApiGateway::Authorizer",
+ "/Properties/IdentitySource", "method.request.header.Authorization",
+ "/Properties/Name", "HelloWorldAPIAuthorizer",
+ "/Properties/ProviderARNs/0/Fn::GetAtt/0", cognitoUserPoolId,
+ "/Properties/RestApiId/Ref", restAPIId,
+ "/Properties/Type", "COGNITO_USER_POOLS"
+ );
+ Assertions.assertNotNull(findResource(stackResourcesMap, apiGatewayAuthorizerMatchMap));
+ }
+}
diff --git a/java/cognito-api-lambda/src/test/java/com/myorg/TestUtils.java b/java/cognito-api-lambda/src/test/java/com/myorg/TestUtils.java
new file mode 100644
index 0000000000..e1f157bd7e
--- /dev/null
+++ b/java/cognito-api-lambda/src/test/java/com/myorg/TestUtils.java
@@ -0,0 +1,61 @@
+package com.myorg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Optional;
+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 Map findResources(Map stackResources, Map matchMap) {
+ return stackResources.entrySet().stream()
+ .filter(createResourcePredicate(matchMap))
+ .collect(Collectors.toMap(
+ Entry::getKey,
+ Entry::getValue
+ ));
+ }
+
+ 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(() -> stackResourceEntry -> false);
+ }
+}