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. + + + + +--- + +![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) + +> **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); + } +}