From 702b929003392f7b2876ee672f7011510ad33d64 Mon Sep 17 00:00:00 2001 From: Chris Woolum Date: Thu, 19 Sep 2024 12:27:06 -0700 Subject: [PATCH] Fix tag handling to account for tagging permissions failures (#29) * Fixes for tests * fix: add specific failure for tag based access denial * Update to Java 17 for the build * Dedupe Maven configuration --------- Co-authored-by: Christopher Woolum Co-authored-by: Albert Winberg --- .github/workflows/java-ci-with-maven.yml | 4 +- .tool-versions | 2 +- .vscode/settings.json | 2 +- README.md | 7 +- aws-amplifyuibuilder-common/pom.xml | 58 +- .../common/ClientWrapper.java | 10 +- .../common/TaggingHelpers.java | 86 ++ aws-amplifyuibuilder-component/.rpdk-config | 2 +- .../aws-amplifyuibuilder-component.json | 6 +- aws-amplifyuibuilder-component/pom.xml | 164 +--- .../component/BaseHandlerStd.java | 16 + .../component/CreateHandler.java | 81 +- .../component/DeleteHandler.java | 3 + .../component/UpdateHandler.java | 90 +- .../component/CreateHandlerTest.java | 798 +++++++++--------- .../component/DeleteHandlerTest.java | 38 + .../component/UpdateHandlerTest.java | 92 +- aws-amplifyuibuilder-component/template.yml | 4 +- aws-amplifyuibuilder-form/.rpdk-config | 2 +- .../aws-amplifyuibuilder-form.json | 13 +- aws-amplifyuibuilder-form/pom.xml | 171 +--- .../amplifyuibuilder/form/BaseHandlerStd.java | 17 + .../amplifyuibuilder/form/CreateHandler.java | 3 + .../amplifyuibuilder/form/DeleteHandler.java | 3 + .../amplifyuibuilder/form/UpdateHandler.java | 41 +- .../form/CreateHandlerTest.java | 407 +++++---- .../form/DeleteHandlerTest.java | 37 + .../form/UpdateHandlerTest.java | 80 +- aws-amplifyuibuilder-form/template.yml | 4 +- aws-amplifyuibuilder-theme/.rpdk-config | 2 +- .../aws-amplifyuibuilder-theme.json | 6 +- aws-amplifyuibuilder-theme/pom.xml | 164 +--- .../theme/BaseHandlerStd.java | 15 + .../amplifyuibuilder/theme/CreateHandler.java | 3 + .../amplifyuibuilder/theme/DeleteHandler.java | 3 + .../amplifyuibuilder/theme/UpdateHandler.java | 33 +- .../theme/CreateHandlerTest.java | 43 + .../theme/DeleteHandlerTest.java | 43 +- .../theme/UpdateHandlerTest.java | 295 ++++--- aws-amplifyuibuilder-theme/template.yml | 4 +- pom.xml | 196 ++++- 41 files changed, 1748 insertions(+), 1300 deletions(-) create mode 100644 aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/TaggingHelpers.java diff --git a/.github/workflows/java-ci-with-maven.yml b/.github/workflows/java-ci-with-maven.yml index b7c112d..b05518b 100644 --- a/.github/workflows/java-ci-with-maven.yml +++ b/.github/workflows/java-ci-with-maven.yml @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Set up Python 3.8 uses: actions/setup-python@v2 with: diff --git a/.tool-versions b/.tool-versions index 971e388..91abe95 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -java 11 +java 17 diff --git a/.vscode/settings.json b/.vscode/settings.json index ec23d14..72dc390 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "java.compile.nullAnalysis.mode": "disabled", - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "automatic" } diff --git a/README.md b/README.md index 1b4e448..ca1b48f 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ TODO: Fill this README out! -Be sure to: +1. Create a new S3 bucket in your account to store your test artifacts. The step functions that will be executing tests -* Change the title in this README -* Edit your repository description on GitHub +```shell +aws s3api create-bucket --bucket {bucketName} --region {bucketRegion} --create-bucket-configuration LocationConstraint={bucketRegion} +``` ## Security diff --git a/aws-amplifyuibuilder-common/pom.xml b/aws-amplifyuibuilder-common/pom.xml index 09aa8c4..3ef05cb 100644 --- a/aws-amplifyuibuilder-common/pom.xml +++ b/aws-amplifyuibuilder-common/pom.xml @@ -1,90 +1,57 @@ - + 4.0.0 + + software.amazon.amplifyuibuilder + aws-amplifyuibuilder-handlers + 1.0 + + software.amazon.amplifyuibuilder.common aws-amplifyuibuilder-common aws-amplifyuibuilder-common 1.0 jar - - 11 - 11 - 11 - UTF-8 - UTF-8 - - - - - - software.amazon.awssdk - bom - 2.17.201 - pom - import - - - - - software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok lombok - 1.18.4 provided - org.apache.commons commons-collections4 - 4.4 - org.assertj assertj-core - 3.12.2 test - org.junit.jupiter junit-jupiter - 5.5.0-M1 test - org.mockito mockito-core - 3.6.0 test - org.mockito mockito-junit-jupiter - 3.6.0 test - software.amazon.awssdk amplifyuibuilder - 2.20.81 @@ -93,26 +60,17 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 - - - -Xlint:all,-options,-processing - - org.apache.maven.plugins maven-resources-plugin - 2.4 maven-surefire-plugin - 3.0.0-M3 org.jacoco jacoco-maven-plugin - 0.8.6 diff --git a/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/ClientWrapper.java b/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/ClientWrapper.java index 44ceea1..be0056d 100644 --- a/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/ClientWrapper.java +++ b/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/ClientWrapper.java @@ -49,13 +49,13 @@ public static AwsResp if (e.statusCode() == HttpStatus.SC_NOT_FOUND) { throw new CfnNotFoundException(resourceTypeName, e.getMessage()); } - if (e.statusCode() == HttpStatus.SC_FORBIDDEN) { - throw new CfnAccessDeniedException(resourceTypeName, e); - } - throw new CfnGeneralServiceException(resourceTypeName, e); + throw e; } catch (AwsServiceException e) { logger.log("ERROR: " + e.getMessage()); - throw new CfnGeneralServiceException(e.getMessage(), e); + throw e; + } catch (Exception e) { + logger.log("GENERAL ERROR: " + e.getMessage()); + throw e; } } diff --git a/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/TaggingHelpers.java b/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/TaggingHelpers.java new file mode 100644 index 0000000..58a7e47 --- /dev/null +++ b/aws-amplifyuibuilder-common/src/main/java/software/amazon/amplifyuibuilder/common/TaggingHelpers.java @@ -0,0 +1,86 @@ +package software.amazon.amplifyuibuilder.common; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; +import software.amazon.awssdk.services.amplifyuibuilder.model.TagResourceRequest; +import software.amazon.awssdk.services.amplifyuibuilder.model.UntagResourceRequest; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.Logger; + +public class TaggingHelpers { + protected static final String ACCESS_DENIED_ERROR_CODE = "AccessDeniedException"; + protected static final String ACESSS_DENIED_MESSAGE_MATCHER = "is not authorized to perform"; + protected static final String[] TAGGING_PERMISSIONS = { "amplifyuibuilder:TagResource", + "amplifyuibuilder:UntagResource" }; + + public static final String SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE = "An error occurred (AccessDeniedException) when calling the CreateCustomActionType operation: User: arn:aws:sts::0123456789:assumed-role/admin/someUser is not authorized to perform: amplifyuibuilder:TagResource on resource: arn:aws:amplifyuibuilder:us-west-2:0123456789:actiontype:Custom/Source/TestingCustomSource/2 with an explicit deny in an identity-based policy"; + + static String getErrorCode(Exception e) { + if (e instanceof AwsServiceException && ((AwsServiceException) e).awsErrorDetails() != null) { + return ((AwsServiceException) e).awsErrorDetails().errorCode(); + } + return e.getMessage(); + } + + public static Boolean isTagBasedAccessDenied(final Exception e) { + String exceptionMessage = e.getMessage(); + boolean isAccessDeniedException = ACCESS_DENIED_ERROR_CODE.equals(getErrorCode(e)) + && exceptionMessage != null + && exceptionMessage.contains(ACESSS_DENIED_MESSAGE_MATCHER); + + if (isAccessDeniedException) { + for (String permission : TAGGING_PERMISSIONS) { + if (e.getMessage().contains(permission)) { + return true; + } + } + } + return false; + } + + public static String generateArn(final String region, final String accountId, final String appId, final String environmentName, final String resource, final String id) { + return String.format("arn:aws:amplifyuibuilder:%s:%s:app/%s/environment/%s/%s/%s",region, accountId, appId, environmentName, resource, id); + } + + public static void updateTags(final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final String appId, + final String resourceArn, + final String typeName, + final Map existingTags, + final Map desiredTags, + final Logger logger) { + + Map safeExistingTags = existingTags == null ? Map.of() : existingTags; + Map safeDesiredTags = desiredTags == null ? Map.of() : desiredTags; + + Map tagsToAdd = safeDesiredTags.entrySet().stream() + .filter(entry -> !entry.getValue().equals(safeExistingTags.get(entry.getKey()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Map tagsToRemove = safeExistingTags.entrySet().stream() + .filter(entry -> !safeDesiredTags.containsKey(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (tagsToRemove.size() > 0) { + Collection tagKeys = tagsToRemove.keySet(); + final UntagResourceRequest untagResourceRequest = UntagResourceRequest.builder().resourceArn(resourceArn) + .tagKeys(tagKeys).build(); + ClientWrapper.execute(proxy, untagResourceRequest, proxyClient.client()::untagResource, typeName, + appId, logger); + } + + if (tagsToAdd.size() > 0) { + final TagResourceRequest tagResourceRequest = TagResourceRequest.builder() + .resourceArn(resourceArn).tags(tagsToAdd).build(); + ClientWrapper.execute(proxy, tagResourceRequest, proxyClient.client()::tagResource, typeName, + appId, logger); + } + logger.log("INFO: Successfully Updated Tags"); + } +} diff --git a/aws-amplifyuibuilder-component/.rpdk-config b/aws-amplifyuibuilder-component/.rpdk-config index b5da838..f053591 100644 --- a/aws-amplifyuibuilder-component/.rpdk-config +++ b/aws-amplifyuibuilder-component/.rpdk-config @@ -2,7 +2,7 @@ "artifact_type": "RESOURCE", "typeName": "AWS::AmplifyUIBuilder::Component", "language": "java", - "runtime": "java11", + "runtime": "java17", "entrypoint": "software.amazon.amplifyuibuilder.component.HandlerWrapper::handleRequest", "testEntrypoint": "software.amazon.amplifyuibuilder.component.HandlerWrapper::testEntrypoint", "settings": { diff --git a/aws-amplifyuibuilder-component/aws-amplifyuibuilder-component.json b/aws-amplifyuibuilder-component/aws-amplifyuibuilder-component.json index 9582767..6c459f4 100644 --- a/aws-amplifyuibuilder-component/aws-amplifyuibuilder-component.json +++ b/aws-amplifyuibuilder-component/aws-amplifyuibuilder-component.json @@ -576,7 +576,11 @@ "tagOnCreate": true, "tagUpdatable": true, "cloudFormationSystemTags": true, - "tagProperty": "/properties/Tags" + "tagProperty": "/properties/Tags", + "permissions": [ + "amplifyuibuilder:TagResource", + "amplifyuibuilder:UntagResource" + ] }, "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-amplifyuibuilder", "additionalProperties": false diff --git a/aws-amplifyuibuilder-component/pom.xml b/aws-amplifyuibuilder-component/pom.xml index 043f89e..7708228 100644 --- a/aws-amplifyuibuilder-component/pom.xml +++ b/aws-amplifyuibuilder-component/pom.xml @@ -1,8 +1,6 @@ - + 4.0.0 software.amazon.amplifyuibuilder.component @@ -11,98 +9,49 @@ 1.0-SNAPSHOT jar - - 11 - 11 - UTF-8 - UTF-8 - - - - - - - software.amazon.awssdk - bom - 2.20.81 - pom - import - - - + + software.amazon.amplifyuibuilder + aws-amplifyuibuilder-handlers + 1.0 + software.amazon.amplifyuibuilder.common aws-amplifyuibuilder-common - 1.0 compile - - software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok lombok - 1.18.4 provided - - - org.apache.logging.log4j - log4j-api - 2.17.1 - - - - org.apache.logging.log4j - log4j-core - 2.17.1 - - - - org.apache.logging.log4j - log4j-slf4j-impl - 2.17.1 - software.amazon.awssdk amplifyuibuilder - 2.20.81 - - org.assertj assertj-core - 3.12.2 test - org.junit.jupiter junit-jupiter - 5.5.0-M1 test - org.mockito mockito-core - 3.6.0 test - org.mockito mockito-junit-jupiter - 3.6.0 test @@ -112,128 +61,29 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 - - - -Xlint:all,-options,-processing - -Werror - - org.apache.maven.plugins maven-shade-plugin - 2.3 - - false - - - - package - - shade - - - org.codehaus.mojo exec-maven-plugin - 1.6.0 - - - generate - generate-sources - - exec - - - cfn - generate - ${project.basedir} - - - org.codehaus.mojo build-helper-maven-plugin - 3.0.0 - - - add-source - generate-sources - - add-source - - - - ${project.basedir}/target/generated-sources/rpdk - - - - org.apache.maven.plugins maven-resources-plugin - 2.4 maven-surefire-plugin - 3.0.0-M3 org.jacoco jacoco-maven-plugin - 0.8.4 - - - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - - - - - - prepare-agent - - - - report - test - - report - - - - jacoco-check - - check - - - - - PACKAGE - - - BRANCH - COVEREDRATIO - 0.8 - - - INSTRUCTION - COVEREDRATIO - 0.8 - - - - - - - diff --git a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/BaseHandlerStd.java b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/BaseHandlerStd.java index 2d41a7d..60aecf7 100644 --- a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/BaseHandlerStd.java +++ b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/BaseHandlerStd.java @@ -1,7 +1,9 @@ package software.amazon.amplifyuibuilder.component; +import software.amazon.amplifyuibuilder.common.TaggingHelpers; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; @@ -10,6 +12,20 @@ // Functionality that could be shared across Create/Read/Update/Delete/List Handlers public abstract class BaseHandlerStd extends BaseHandler { + protected static ProgressEvent handleErrorInternal( + ResourceHandlerRequest awsRequest, + Exception exception, + ProxyClient client, + ResourceModel model, + CallbackContext context) { + if (TaggingHelpers.isTagBasedAccessDenied(exception)) { + return ProgressEvent.failed(model, context, HandlerErrorCode.UnauthorizedTaggingOperation, + exception.getMessage()); + } + + return ProgressEvent.failed(model, context, HandlerErrorCode.InternalFailure, exception.getMessage()); + } + @Override public final ProgressEvent handleRequest( final AmazonWebServicesClientProxy proxy, diff --git a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/CreateHandler.java b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/CreateHandler.java index 3af75f7..2338eed 100644 --- a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/CreateHandler.java +++ b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/CreateHandler.java @@ -11,45 +11,48 @@ public class CreateHandler extends BaseHandlerStd { - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { - ResourceModel model = request.getDesiredResourceState(); - logger.log("CreateHandler invoked"); + ResourceModel model = request.getDesiredResourceState(); + logger.log("CreateHandler invoked"); - return ProgressEvent.progress(model, callbackContext) - .then(progress -> proxy - .initiate( - "AWS-AmplifyUIBuilder-Component::Create", - proxyClient, - request.getDesiredResourceState(), - callbackContext) - .translateToServiceRequest(resourceModel -> Translator - .translateToCreateRequest(resourceModel, - TagHelper.getNewDesiredTags(request), - request.getClientRequestToken())) - .makeServiceCall((createComponentRequest, proxyInvocation) -> { - CreateComponentResponse response = (CreateComponentResponse) ClientWrapper - .execute( - proxy, - createComponentRequest, - proxyInvocation.client()::createComponent, - ResourceModel.TYPE_NAME, - model.getId(), - logger); - logger.log("Successfully created component with id: " - + response.entity().id()); - // Set the ID from the created component to do a read request - // next - model.setId(response.entity().id()); - return response; - }) - .progress()) - .then(progress -> new ReadHandler() - .handleRequest(proxy, request, callbackContext, proxyClient, logger)); - } + return ProgressEvent.progress(model, callbackContext) + .then(progress -> proxy + .initiate( + "AWS-AmplifyUIBuilder-Component::Create", + proxyClient, + request.getDesiredResourceState(), + callbackContext) + .translateToServiceRequest(resourceModel -> Translator + .translateToCreateRequest(resourceModel, + TagHelper.getNewDesiredTags(request), + request.getClientRequestToken())) + .makeServiceCall((createComponentRequest, proxyInvocation) -> { + CreateComponentResponse response = (CreateComponentResponse) ClientWrapper + .execute( + proxy, + createComponentRequest, + proxyInvocation.client()::createComponent, + ResourceModel.TYPE_NAME, + model.getId(), + logger); + logger.log("Successfully created component with id: " + + response.entity().id()); + // Set the ID from the created component to do a read request + // next + model.setId(response.entity().id()); + return response; + }) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) + .progress()) + .then(progress -> new ReadHandler() + .handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } } diff --git a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/DeleteHandler.java b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/DeleteHandler.java index 054bb07..a9d343a 100644 --- a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/DeleteHandler.java +++ b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/DeleteHandler.java @@ -39,6 +39,9 @@ protected ProgressEvent handleRequest( model.getId(), logger ))) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) .progress() ) // Return the successful progress event without resource model diff --git a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/UpdateHandler.java b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/UpdateHandler.java index 25af8fa..e812161 100644 --- a/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/UpdateHandler.java +++ b/aws-amplifyuibuilder-component/src/main/java/software/amazon/amplifyuibuilder/component/UpdateHandler.java @@ -1,6 +1,7 @@ package software.amazon.amplifyuibuilder.component; import software.amazon.amplifyuibuilder.common.ClientWrapper; +import software.amazon.amplifyuibuilder.common.TaggingHelpers; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.UpdateComponentResponse; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; @@ -11,41 +12,56 @@ public class UpdateHandler extends BaseHandlerStd { - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - - ResourceModel model = request.getDesiredResourceState(); - - return ProgressEvent - .progress(request.getDesiredResourceState(), callbackContext) - .then( - progress -> proxy - .initiate( - "AWS-AmplifyUIBuilder-Component::Update", - proxyClient, - progress.getResourceModel(), - progress.getCallbackContext()) - .translateToServiceRequest( - resourceModel -> Translator.translateToUpdateRequest(resourceModel, - request.getDesiredResourceTags(), request.getClientRequestToken())) - .makeServiceCall((updateComponentRequest, proxyInvocation) -> { - UpdateComponentResponse response = (UpdateComponentResponse) ClientWrapper.execute( - proxy, - updateComponentRequest, - proxyInvocation.client()::updateComponent, - ResourceModel.TYPE_NAME, - model.getId(), - logger); - logger.log("Successfully updated component with ID: " + response.entity().id()); - return response; - }) - .progress()) - .then( - progress -> new ReadHandler() - .handleRequest(proxy, request, callbackContext, proxyClient, logger)); - } + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + ResourceModel model = request.getDesiredResourceState(); + + return ProgressEvent + .progress(request.getDesiredResourceState(), callbackContext) + .then( + progress -> proxy + .initiate( + "AWS-AmplifyUIBuilder-Component::Update", + proxyClient, + progress.getResourceModel(), + progress.getCallbackContext()) + .translateToServiceRequest( + resourceModel -> Translator.translateToUpdateRequest(resourceModel, + request.getDesiredResourceTags(), request.getClientRequestToken())) + .makeServiceCall((updateComponentRequest, proxyInvocation) -> { + UpdateComponentResponse response = (UpdateComponentResponse) ClientWrapper.execute( + proxy, + updateComponentRequest, + proxyInvocation.client()::updateComponent, + ResourceModel.TYPE_NAME, + model.getId(), + logger); + logger.log("Successfully updated component with ID: " + response.entity().id()); + + final String componentArn = TaggingHelpers.generateArn( + request.getRegion(), + request.getAwsAccountId(), + updateComponentRequest.appId(), + updateComponentRequest.environmentName(), + "components", + updateComponentRequest.id()); + + TaggingHelpers.updateTags(proxy, proxyInvocation, model.getAppId(), componentArn, ResourceModel.TYPE_NAME, + response.entity().tags(), model.getTags(), logger); + + return response; + }) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) + .progress()) + .then( + progress -> new ReadHandler() + .handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } } diff --git a/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/CreateHandlerTest.java b/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/CreateHandlerTest.java index 6425c74..13464e4 100644 --- a/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/CreateHandlerTest.java +++ b/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/CreateHandlerTest.java @@ -7,6 +7,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.CreateComponentRequest; import software.amazon.awssdk.services.amplifyuibuilder.model.CreateComponentResponse; @@ -29,384 +33,418 @@ @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { - @Mock - @Getter - private AmazonWebServicesClientProxy proxy; - - @Mock - @Getter - private ProxyClient proxyClient; - - @Mock - AmplifyUiBuilderClient sdkClient; - - @BeforeEach - public void setup() { - proxy = new AmazonWebServicesClientProxy( - logger, - MOCK_CREDENTIALS, - () -> Duration.ofSeconds(600).toMillis()); - sdkClient = mock(AmplifyUiBuilderClient.class); - proxyClient = MOCK_PROXY(proxy, sdkClient); - } - - @AfterEach - public void tear_down() { - verify(sdkClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(sdkClient); - } - - @Test - public void handleRequest_SimpleSuccess() { - final CreateHandler handler = new CreateHandler(); - - final GetComponentResponse getResponse = GetComponentResponse - .builder() - .component( - software.amazon.awssdk.services.amplifyuibuilder.model.Component - .builder() - .name(NAME) - .id(ID) - .appId(APP_ID) - .environmentName(ENV_NAME) - .componentType(TYPE) - .variants(transformList(VARIANT_CFN, - Translator::translateVariantFromCFNToSDK)) - .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, - Translator::translateBindingPropertyFromCFNToSDK)) - .overrides(OVERRIDES) - .createdAt(Instant.now()) - .modifiedAt(Instant.now()) - .properties(transformMap(PROPERTIES_CFN, - Translator::translateComponentPropertyFromCFNToSDK)) - .collectionProperties(transformMap( - COLLECTION_PROPERTIES_CFN, - Translator::translateCollectionPropertyFromCFNToSDK)) - .children(transformList(CHILDREN_CFN, - Translator::translateChildComponentFromCFNToSDK)) - .events(transformMap(EVENTS_CFN, - Translator::translateEventFromCFNToSDK)) - .schemaVersion(SCHEMA_VERSION) - .tags(TAGS) - .sourceId("123456") - .build()) - .build(); - - when(proxyClient.client().getComponent(any(GetComponentRequest.class))) - .thenReturn(getResponse); - - final CreateComponentResponse createResponse = CreateComponentResponse - .builder() - .entity( - (software.amazon.awssdk.services.amplifyuibuilder.model.Component - .builder() - // Use this returned ID to pass to read handler after - // component is created - .id(ID) - .build())) - .build(); - - when( - proxyClient.client().createComponent(any(CreateComponentRequest.class))) - .thenReturn(createResponse); - - final ResourceModel model = ResourceModel - .builder() - .environmentName(ENV_NAME) - .appId(APP_ID) - .name(NAME) - .componentType(TYPE) - .variants(VARIANT_CFN) - .bindingProperties(BINDING_PROPERTIES_CFN) - .overrides(OVERRIDES) - .properties(PROPERTIES_CFN) - .tags(TAGS) - .children(CHILDREN_CFN) - .collectionProperties(COLLECTION_PROPERTIES_CFN) - .events(EVENTS_CFN) - .schemaVersion(SCHEMA_VERSION) - .sourceId("123456") - .build(); - - CallbackContext context = new CallbackContext(); - - final ResourceHandlerRequest request = ResourceHandlerRequest - .builder() - .desiredResourceState(model) - .build(); - - final ProgressEvent response = handler.handleRequest( - proxy, - request, - context, - proxyClient, - logger); - ResourceModel component = response.getResourceModel(); - - assertThat(response).isNotNull(); - - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); - assertThat(component.getProperties().keySet()).isEqualTo(model.getProperties().keySet()); - assertThat(component.getVariants().size()).isEqualTo(model.getVariants().size()); - assertThat(component.getBindingProperties().keySet()).isEqualTo(model.getBindingProperties().keySet()); - assertThat(component.getOverrides()).isEqualTo(model.getOverrides()); - assertThat(component.getCollectionProperties().keySet()) - .isEqualTo(model.getCollectionProperties().keySet()); - assertThat(component.getTags()).isEqualTo(model.getTags()); - assertThat(component.getSourceId()).isEqualTo(model.getSourceId()); - assertThat(component.getComponentType()).isEqualTo(model.getComponentType()); - assertThat(component.getEvents().keySet()).isEqualTo(model.getEvents().keySet()); - assertThat(component.getSchemaVersion()).isEqualTo(model.getSchemaVersion()); - } - - @Test - public void handleRequest_NullTags() { - final CreateHandler handler = new CreateHandler(); - - final GetComponentResponse getResponse = GetComponentResponse - .builder() - .component( - software.amazon.awssdk.services.amplifyuibuilder.model.Component - .builder() - .name(NAME) - .id(ID) - .appId(APP_ID) - .environmentName(ENV_NAME) - .componentType(TYPE) - .variants(transformList(VARIANT_CFN, - Translator::translateVariantFromCFNToSDK)) - .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, - Translator::translateBindingPropertyFromCFNToSDK)) - .overrides(OVERRIDES) - .createdAt(Instant.now()) - .modifiedAt(Instant.now()) - .properties(transformMap(PROPERTIES_CFN, - Translator::translateComponentPropertyFromCFNToSDK)) - .collectionProperties(transformMap( - COLLECTION_PROPERTIES_CFN, - Translator::translateCollectionPropertyFromCFNToSDK)) - .children(transformList(CHILDREN_CFN, - Translator::translateChildComponentFromCFNToSDK)) - .events(transformMap(EVENTS_CFN, - Translator::translateEventFromCFNToSDK)) - .schemaVersion(SCHEMA_VERSION) - .build()) - .build(); - - when(proxyClient.client().getComponent(any(GetComponentRequest.class))) - .thenReturn(getResponse); - - final CreateComponentResponse createResponse = CreateComponentResponse - .builder() - .entity( - (software.amazon.awssdk.services.amplifyuibuilder.model.Component - .builder() - // Use this returned ID to pass to read handler after - // component is created - .id(ID) - .build())) - .build(); - - when( - proxyClient.client().createComponent(any(CreateComponentRequest.class))) - .thenReturn(createResponse); - - final ResourceModel model = ResourceModel - .builder() - .environmentName(ENV_NAME) - .appId(APP_ID) - .name(NAME) - .componentType(TYPE) - .variants(VARIANT_CFN) - .bindingProperties(BINDING_PROPERTIES_CFN) - .overrides(OVERRIDES) - .properties(PROPERTIES_CFN) - .children(CHILDREN_CFN) - .collectionProperties(COLLECTION_PROPERTIES_CFN) - .events(EVENTS_CFN) - .schemaVersion(SCHEMA_VERSION) - .build(); - - CallbackContext context = new CallbackContext(); - - final ResourceHandlerRequest request = ResourceHandlerRequest - .builder() - .desiredResourceState(model) - .build(); - - final ProgressEvent response = handler.handleRequest( - proxy, - request, - context, - proxyClient, - logger); - ResourceModel component = response.getResourceModel(); - - assertThat(response).isNotNull(); - - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); - assertThat(component.getProperties().keySet()).isEqualTo(model.getProperties().keySet()); - assertThat(component.getVariants().size()).isEqualTo(model.getVariants().size()); - assertThat(component.getBindingProperties().keySet()).isEqualTo(model.getBindingProperties().keySet()); - assertThat(component.getOverrides()).isEqualTo(model.getOverrides()); - assertThat(component.getCollectionProperties().keySet()) - .isEqualTo(model.getCollectionProperties().keySet()); - assertThat(component.getTags()).isEqualTo(new HashMap<>()); - assertThat(component.getSourceId()).isEqualTo(""); - assertThat(component.getComponentType()).isEqualTo(model.getComponentType()); - assertThat(component.getEvents().keySet()).isEqualTo(model.getEvents().keySet()); - assertThat(component.getSchemaVersion()).isEqualTo(model.getSchemaVersion()); - } - - @Test - public void handleRequest_WithSystemAndPreviousTags() { - final CreateHandler handler = new CreateHandler(); - - final GetComponentResponse getResponse = GetComponentResponse - .builder() - .component( - software.amazon.awssdk.services.amplifyuibuilder.model.Component - .builder() - .name(NAME) - .id(ID) - .appId(APP_ID) - .environmentName(ENV_NAME) - .componentType(TYPE) - .variants(transformList(VARIANT_CFN, - Translator::translateVariantFromCFNToSDK)) - .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, - Translator::translateBindingPropertyFromCFNToSDK)) - .overrides(OVERRIDES) - .createdAt(Instant.now()) - .modifiedAt(Instant.now()) - .properties(transformMap(PROPERTIES_CFN, - Translator::translateComponentPropertyFromCFNToSDK)) - .collectionProperties(transformMap( - COLLECTION_PROPERTIES_CFN, - Translator::translateCollectionPropertyFromCFNToSDK)) - .children(transformList(CHILDREN_CFN, - Translator::translateChildComponentFromCFNToSDK)) - .events(transformMap(EVENTS_CFN, - Translator::translateEventFromCFNToSDK)) - .schemaVersion(SCHEMA_VERSION) - .build()) - .build(); - - when(proxyClient.client().getComponent(any(GetComponentRequest.class))) - .thenReturn(getResponse); - - final CreateComponentResponse createResponse = CreateComponentResponse - .builder() - .entity( - (software.amazon.awssdk.services.amplifyuibuilder.model.Component - .builder() - // Use this returned ID to pass to read handler after - // component is created - .id(ID) - .build())) - .build(); - - when( - proxyClient.client().createComponent(any(CreateComponentRequest.class))) - .thenReturn(createResponse); - - final ResourceModel model = ResourceModel - .builder() - .environmentName(ENV_NAME) - .appId(APP_ID) - .name(NAME) - .componentType(TYPE) - .variants(VARIANT_CFN) - .bindingProperties(BINDING_PROPERTIES_CFN) - .overrides(OVERRIDES) - .properties(PROPERTIES_CFN) - .children(CHILDREN_CFN) - .collectionProperties(COLLECTION_PROPERTIES_CFN) - .events(EVENTS_CFN) - .schemaVersion(SCHEMA_VERSION) - .build(); - - CallbackContext context = new CallbackContext(); - - final ResourceHandlerRequest request = ResourceHandlerRequest - .builder() - .previousSystemTags(Map.of("system-tag-key-1", "system-tag-value-1")) - .previousResourceTags(Map.of("resource-tag-key-1", "resource-tag-value-1")) - .desiredResourceState(model) - .systemTags(Map.of("system-tag-key-3", "system-tag-value-4")) - .desiredResourceTags(Map.of("resource-tag-key-3", "resource-tag-value-4")) - .build(); - - final ProgressEvent response = handler.handleRequest( - proxy, - request, - context, - proxyClient, - logger); - ResourceModel component = response.getResourceModel(); - - assertThat(response).isNotNull(); - - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); - assertThat(component.getProperties().keySet()).isEqualTo(model.getProperties().keySet()); - assertThat(component.getVariants().size()).isEqualTo(model.getVariants().size()); - assertThat(component.getBindingProperties().keySet()).isEqualTo(model.getBindingProperties().keySet()); - assertThat(component.getOverrides()).isEqualTo(model.getOverrides()); - assertThat(component.getCollectionProperties().keySet()) - .isEqualTo(model.getCollectionProperties().keySet()); - assertThat(component.getTags()).isEqualTo(new HashMap<>()); - assertThat(component.getSourceId()).isEqualTo(""); - assertThat(component.getComponentType()).isEqualTo(model.getComponentType()); - assertThat(component.getEvents().keySet()).isEqualTo(model.getEvents().keySet()); - assertThat(component.getSchemaVersion()).isEqualTo(model.getSchemaVersion()); - } - - // Tests resource model with null properties - @Test - public void handleRequest_NullProperties() { - final CreateHandler handler = new CreateHandler(); - - when(proxyClient.client().createComponent(any(CreateComponentRequest.class))) - .thenThrow(new CfnInvalidRequestException("Invalid parameters")); - - final ResourceModel model = ResourceModel - .builder() - .environmentName(ENV_NAME) - .appId(APP_ID) - .name(NAME) - .componentType(TYPE) - .build(); - - CallbackContext context = new CallbackContext(); - - final ResourceHandlerRequest request = ResourceHandlerRequest - .builder() - .desiredResourceState(model) - .build(); - - Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest( - proxy, - request, - context, - proxyClient, - logger)); - } + @Mock + @Getter + private AmazonWebServicesClientProxy proxy; + + @Mock + @Getter + private ProxyClient proxyClient; + + @Mock + AmplifyUiBuilderClient sdkClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy( + logger, + MOCK_CREDENTIALS, + () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(AmplifyUiBuilderClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final CreateHandler handler = new CreateHandler(); + + final GetComponentResponse getResponse = GetComponentResponse + .builder() + .component( + software.amazon.awssdk.services.amplifyuibuilder.model.Component + .builder() + .name(NAME) + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .componentType(TYPE) + .variants(transformList(VARIANT_CFN, + Translator::translateVariantFromCFNToSDK)) + .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, + Translator::translateBindingPropertyFromCFNToSDK)) + .overrides(OVERRIDES) + .createdAt(Instant.now()) + .modifiedAt(Instant.now()) + .properties(transformMap(PROPERTIES_CFN, + Translator::translateComponentPropertyFromCFNToSDK)) + .collectionProperties(transformMap( + COLLECTION_PROPERTIES_CFN, + Translator::translateCollectionPropertyFromCFNToSDK)) + .children(transformList(CHILDREN_CFN, + Translator::translateChildComponentFromCFNToSDK)) + .events(transformMap(EVENTS_CFN, + Translator::translateEventFromCFNToSDK)) + .schemaVersion(SCHEMA_VERSION) + .tags(TAGS) + .sourceId("123456") + .build()) + .build(); + + when(proxyClient.client().getComponent(any(GetComponentRequest.class))) + .thenReturn(getResponse); + + final CreateComponentResponse createResponse = CreateComponentResponse + .builder() + .entity( + (software.amazon.awssdk.services.amplifyuibuilder.model.Component + .builder() + // Use this returned ID to pass to read handler after + // component is created + .id(ID) + .build())) + .build(); + + when( + proxyClient.client().createComponent(any(CreateComponentRequest.class))) + .thenReturn(createResponse); + + final ResourceModel model = ResourceModel + .builder() + .environmentName(ENV_NAME) + .appId(APP_ID) + .name(NAME) + .componentType(TYPE) + .variants(VARIANT_CFN) + .bindingProperties(BINDING_PROPERTIES_CFN) + .overrides(OVERRIDES) + .properties(PROPERTIES_CFN) + .tags(TAGS) + .children(CHILDREN_CFN) + .collectionProperties(COLLECTION_PROPERTIES_CFN) + .events(EVENTS_CFN) + .schemaVersion(SCHEMA_VERSION) + .sourceId("123456") + .build(); + + CallbackContext context = new CallbackContext(); + + final ResourceHandlerRequest request = ResourceHandlerRequest + .builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest( + proxy, + request, + context, + proxyClient, + logger); + ResourceModel component = response.getResourceModel(); + + assertThat(response).isNotNull(); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); + assertThat(component.getProperties().keySet()).isEqualTo(model.getProperties().keySet()); + assertThat(component.getVariants().size()).isEqualTo(model.getVariants().size()); + assertThat(component.getBindingProperties().keySet()).isEqualTo(model.getBindingProperties().keySet()); + assertThat(component.getOverrides()).isEqualTo(model.getOverrides()); + assertThat(component.getCollectionProperties().keySet()) + .isEqualTo(model.getCollectionProperties().keySet()); + assertThat(component.getTags()).isEqualTo(model.getTags()); + assertThat(component.getSourceId()).isEqualTo(model.getSourceId()); + assertThat(component.getComponentType()).isEqualTo(model.getComponentType()); + assertThat(component.getEvents().keySet()).isEqualTo(model.getEvents().keySet()); + assertThat(component.getSchemaVersion()).isEqualTo(model.getSchemaVersion()); + } + + @Test + public void handleRequest_NullTags() { + final CreateHandler handler = new CreateHandler(); + + final GetComponentResponse getResponse = GetComponentResponse + .builder() + .component( + software.amazon.awssdk.services.amplifyuibuilder.model.Component + .builder() + .name(NAME) + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .componentType(TYPE) + .variants(transformList(VARIANT_CFN, + Translator::translateVariantFromCFNToSDK)) + .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, + Translator::translateBindingPropertyFromCFNToSDK)) + .overrides(OVERRIDES) + .createdAt(Instant.now()) + .modifiedAt(Instant.now()) + .properties(transformMap(PROPERTIES_CFN, + Translator::translateComponentPropertyFromCFNToSDK)) + .collectionProperties(transformMap( + COLLECTION_PROPERTIES_CFN, + Translator::translateCollectionPropertyFromCFNToSDK)) + .children(transformList(CHILDREN_CFN, + Translator::translateChildComponentFromCFNToSDK)) + .events(transformMap(EVENTS_CFN, + Translator::translateEventFromCFNToSDK)) + .schemaVersion(SCHEMA_VERSION) + .build()) + .build(); + + when(proxyClient.client().getComponent(any(GetComponentRequest.class))) + .thenReturn(getResponse); + + final CreateComponentResponse createResponse = CreateComponentResponse + .builder() + .entity( + (software.amazon.awssdk.services.amplifyuibuilder.model.Component + .builder() + // Use this returned ID to pass to read handler after + // component is created + .id(ID) + .build())) + .build(); + + when( + proxyClient.client().createComponent(any(CreateComponentRequest.class))) + .thenReturn(createResponse); + + final ResourceModel model = ResourceModel + .builder() + .environmentName(ENV_NAME) + .appId(APP_ID) + .name(NAME) + .componentType(TYPE) + .variants(VARIANT_CFN) + .bindingProperties(BINDING_PROPERTIES_CFN) + .overrides(OVERRIDES) + .properties(PROPERTIES_CFN) + .children(CHILDREN_CFN) + .collectionProperties(COLLECTION_PROPERTIES_CFN) + .events(EVENTS_CFN) + .schemaVersion(SCHEMA_VERSION) + .build(); + + CallbackContext context = new CallbackContext(); + + final ResourceHandlerRequest request = ResourceHandlerRequest + .builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest( + proxy, + request, + context, + proxyClient, + logger); + ResourceModel component = response.getResourceModel(); + + assertThat(response).isNotNull(); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); + assertThat(component.getProperties().keySet()).isEqualTo(model.getProperties().keySet()); + assertThat(component.getVariants().size()).isEqualTo(model.getVariants().size()); + assertThat(component.getBindingProperties().keySet()).isEqualTo(model.getBindingProperties().keySet()); + assertThat(component.getOverrides()).isEqualTo(model.getOverrides()); + assertThat(component.getCollectionProperties().keySet()) + .isEqualTo(model.getCollectionProperties().keySet()); + assertThat(component.getTags()).isEqualTo(new HashMap<>()); + assertThat(component.getSourceId()).isEqualTo(""); + assertThat(component.getComponentType()).isEqualTo(model.getComponentType()); + assertThat(component.getEvents().keySet()).isEqualTo(model.getEvents().keySet()); + assertThat(component.getSchemaVersion()).isEqualTo(model.getSchemaVersion()); + } + + @Test + public void handleRequest_WithSystemAndPreviousTags() { + final CreateHandler handler = new CreateHandler(); + + final GetComponentResponse getResponse = GetComponentResponse + .builder() + .component( + software.amazon.awssdk.services.amplifyuibuilder.model.Component + .builder() + .name(NAME) + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .componentType(TYPE) + .variants(transformList(VARIANT_CFN, + Translator::translateVariantFromCFNToSDK)) + .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, + Translator::translateBindingPropertyFromCFNToSDK)) + .overrides(OVERRIDES) + .createdAt(Instant.now()) + .modifiedAt(Instant.now()) + .properties(transformMap(PROPERTIES_CFN, + Translator::translateComponentPropertyFromCFNToSDK)) + .collectionProperties(transformMap( + COLLECTION_PROPERTIES_CFN, + Translator::translateCollectionPropertyFromCFNToSDK)) + .children(transformList(CHILDREN_CFN, + Translator::translateChildComponentFromCFNToSDK)) + .events(transformMap(EVENTS_CFN, + Translator::translateEventFromCFNToSDK)) + .schemaVersion(SCHEMA_VERSION) + .build()) + .build(); + + when(proxyClient.client().getComponent(any(GetComponentRequest.class))) + .thenReturn(getResponse); + + final CreateComponentResponse createResponse = CreateComponentResponse + .builder() + .entity( + (software.amazon.awssdk.services.amplifyuibuilder.model.Component + .builder() + // Use this returned ID to pass to read handler after + // component is created + .id(ID) + .build())) + .build(); + + when( + proxyClient.client().createComponent(any(CreateComponentRequest.class))) + .thenReturn(createResponse); + + final ResourceModel model = ResourceModel + .builder() + .environmentName(ENV_NAME) + .appId(APP_ID) + .name(NAME) + .componentType(TYPE) + .variants(VARIANT_CFN) + .bindingProperties(BINDING_PROPERTIES_CFN) + .overrides(OVERRIDES) + .properties(PROPERTIES_CFN) + .children(CHILDREN_CFN) + .collectionProperties(COLLECTION_PROPERTIES_CFN) + .events(EVENTS_CFN) + .schemaVersion(SCHEMA_VERSION) + .build(); + + CallbackContext context = new CallbackContext(); + + final ResourceHandlerRequest request = ResourceHandlerRequest + .builder() + .previousSystemTags(Map.of("system-tag-key-1", "system-tag-value-1")) + .previousResourceTags(Map.of("resource-tag-key-1", "resource-tag-value-1")) + .desiredResourceState(model) + .systemTags(Map.of("system-tag-key-3", "system-tag-value-4")) + .desiredResourceTags(Map.of("resource-tag-key-3", "resource-tag-value-4")) + .build(); + + final ProgressEvent response = handler.handleRequest( + proxy, + request, + context, + proxyClient, + logger); + ResourceModel component = response.getResourceModel(); + + assertThat(response).isNotNull(); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); + assertThat(component.getProperties().keySet()).isEqualTo(model.getProperties().keySet()); + assertThat(component.getVariants().size()).isEqualTo(model.getVariants().size()); + assertThat(component.getBindingProperties().keySet()).isEqualTo(model.getBindingProperties().keySet()); + assertThat(component.getOverrides()).isEqualTo(model.getOverrides()); + assertThat(component.getCollectionProperties().keySet()) + .isEqualTo(model.getCollectionProperties().keySet()); + assertThat(component.getTags()).isEqualTo(new HashMap<>()); + assertThat(component.getSourceId()).isEqualTo(""); + assertThat(component.getComponentType()).isEqualTo(model.getComponentType()); + assertThat(component.getEvents().keySet()).isEqualTo(model.getEvents().keySet()); + assertThat(component.getSchemaVersion()).isEqualTo(model.getSchemaVersion()); + } + + // Tests resource model with null properties + @Test + public void handleRequest_NullProperties() { + final CreateHandler handler = new CreateHandler(); + + when(proxyClient.client().createComponent(any(CreateComponentRequest.class))) + .thenThrow(new CfnInvalidRequestException("Invalid parameters")); + + final ResourceModel model = ResourceModel + .builder() + .environmentName(ENV_NAME) + .appId(APP_ID) + .name(NAME) + .componentType(TYPE) + .build(); + + CallbackContext context = new CallbackContext(); + + final ResourceHandlerRequest request = ResourceHandlerRequest + .builder() + .desiredResourceState(model) + .build(); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest( + proxy, + request, + context, + proxyClient, + logger)); + } + + @Test + public void handleRequest_taggingError_failure() { + final ResourceModel model = ResourceModel + .builder() + .environmentName(ENV_NAME) + .appId(APP_ID) + .name(NAME) + .componentType(TYPE) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().createComponent(any(CreateComponentRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new CreateHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/DeleteHandlerTest.java b/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/DeleteHandlerTest.java index 8ea143a..3d51ed9 100644 --- a/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/DeleteHandlerTest.java +++ b/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/DeleteHandlerTest.java @@ -6,6 +6,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.DeleteComponentRequest; import software.amazon.awssdk.services.amplifyuibuilder.model.DeleteComponentResponse; @@ -14,6 +18,7 @@ import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -85,4 +90,37 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } + + + @Test + public void handleRequest_taggingError_failure() { + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .id(ID) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().deleteComponent(any(DeleteComponentRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new DeleteHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/UpdateHandlerTest.java b/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/UpdateHandlerTest.java index 28581de..6d26809 100644 --- a/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/UpdateHandlerTest.java +++ b/aws-amplifyuibuilder-component/src/test/java/software/amazon/amplifyuibuilder/component/UpdateHandlerTest.java @@ -6,9 +6,17 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.*; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; +import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; @@ -24,6 +32,7 @@ import static software.amazon.amplifyuibuilder.common.Transformer.transformMap; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class UpdateHandlerTest extends AbstractTestBase { @Mock @@ -50,7 +59,7 @@ public void setup() { @AfterEach public void tear_down() { verify(sdkClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(sdkClient); + verifyNoMoreInteractions(ignoreStubs(sdkClient)); } @Test @@ -146,4 +155,85 @@ public void handleRequest_SimpleSuccess() { assertThat(component.getCollectionProperties().keySet()).isEqualTo(model.getCollectionProperties().keySet()); assertThat(component.getChildren().size()).isEqualTo(model.getChildren().size()); } + + @Test + public void handleRequest_taggingError_failure() { + final UpdateHandler handler = new UpdateHandler(); + + final GetComponentResponse getResponse = GetComponentResponse + .builder() + .component( + Component + .builder() + .name(NAME) + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .componentType(TYPE) + .variants(transformList(VARIANT_CFN, Translator::translateVariantFromCFNToSDK)) + .bindingProperties(transformMap(BINDING_PROPERTIES_CFN, Translator::translateBindingPropertyFromCFNToSDK)) + .overrides(OVERRIDES) + .createdAt(Instant.now()) + .modifiedAt(Instant.now()) + .properties(transformMap(PROPERTIES_CFN, Translator::translateComponentPropertyFromCFNToSDK)) + .collectionProperties(transformMap(COLLECTION_PROPERTIES_CFN, Translator::translateCollectionPropertyFromCFNToSDK)) + .children(transformList(CHILDREN_CFN, Translator::translateChildComponentFromCFNToSDK)) + .tags(TAGS) + .schemaVersion(SCHEMA_VERSION) + .build() + ) + .build(); + + when(proxyClient.client().getComponent(any(GetComponentRequest.class))) + .thenReturn(getResponse); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().updateComponent(any(UpdateComponentRequest.class))) + .thenThrow(e); + + final ResourceModel model = ResourceModel + .builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .id(ID) + .name(NAME) + .id(ID) + .componentType(TYPE) + .children(new ArrayList<>()) + .variants(VARIANT_CFN) + .bindingProperties(BINDING_PROPERTIES_CFN) + .overrides(OVERRIDES) + .properties(PROPERTIES_CFN) + .collectionProperties(COLLECTION_PROPERTIES_CFN) + .children(CHILDREN_CFN) + .schemaVersion(SCHEMA_VERSION) + .build(); + + CallbackContext context = new CallbackContext(); + + final ResourceHandlerRequest request = ResourceHandlerRequest + .builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest( + proxy, + request, + context, + proxyClient, + logger + ); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-component/template.yml b/aws-amplifyuibuilder-component/template.yml index c32d41b..924290a 100644 --- a/aws-amplifyuibuilder-component/template.yml +++ b/aws-amplifyuibuilder-component/template.yml @@ -12,12 +12,12 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: software.amazon.amplifyuibuilder.component.HandlerWrapper::handleRequest - Runtime: java11 + Runtime: java17 CodeUri: ./target/aws-amplifyuibuilder-component-handler-1.0-SNAPSHOT.jar TestEntrypoint: Type: AWS::Serverless::Function Properties: Handler: software.amazon.amplifyuibuilder.component.HandlerWrapper::testEntrypoint - Runtime: java11 + Runtime: java17 CodeUri: ./target/aws-amplifyuibuilder-component-handler-1.0-SNAPSHOT.jar diff --git a/aws-amplifyuibuilder-form/.rpdk-config b/aws-amplifyuibuilder-form/.rpdk-config index d2ea6e4..a31a6db 100644 --- a/aws-amplifyuibuilder-form/.rpdk-config +++ b/aws-amplifyuibuilder-form/.rpdk-config @@ -2,7 +2,7 @@ "artifact_type": "RESOURCE", "typeName": "AWS::AmplifyUIBuilder::Form", "language": "java", - "runtime": "java11", + "runtime": "java17", "entrypoint": "software.amazon.amplifyuibuilder.form.HandlerWrapper::handleRequest", "testEntrypoint": "software.amazon.amplifyuibuilder.form.HandlerWrapper::testEntrypoint", "settings": { diff --git a/aws-amplifyuibuilder-form/aws-amplifyuibuilder-form.json b/aws-amplifyuibuilder-form/aws-amplifyuibuilder-form.json index 8277667..ab98465 100644 --- a/aws-amplifyuibuilder-form/aws-amplifyuibuilder-form.json +++ b/aws-amplifyuibuilder-form/aws-amplifyuibuilder-form.json @@ -536,15 +536,13 @@ "amplify:GetApp", "amplifyuibuilder:CreateForm", "amplifyuibuilder:GetForm", - "amplifyuibuilder:TagResource", - "amplifyuibuilder:UntagResource" + "amplifyuibuilder:TagResource" ] }, "read": { "permissions": [ "amplify:GetApp", - "amplifyuibuilder:GetForm", - "amplifyuibuilder:TagResource" + "amplifyuibuilder:GetForm" ] }, "update": { @@ -560,7 +558,6 @@ "permissions": [ "amplify:GetApp", "amplifyuibuilder:DeleteForm", - "amplifyuibuilder:TagResource", "amplifyuibuilder:UntagResource" ] }, @@ -590,7 +587,11 @@ "tagOnCreate": true, "tagUpdatable": true, "cloudFormationSystemTags": true, - "tagProperty": "/properties/Tags" + "tagProperty": "/properties/Tags", + "permissions": [ + "amplifyuibuilder:TagResource", + "amplifyuibuilder:UntagResource" + ] }, "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-amplifyuibuilder", "additionalProperties": false diff --git a/aws-amplifyuibuilder-form/pom.xml b/aws-amplifyuibuilder-form/pom.xml index b8b6600..6da1ec8 100644 --- a/aws-amplifyuibuilder-form/pom.xml +++ b/aws-amplifyuibuilder-form/pom.xml @@ -1,8 +1,6 @@ - + 4.0.0 software.amazon.amplifyuibuilder.form @@ -11,95 +9,49 @@ 1.0-SNAPSHOT jar - - 11 - 11 - UTF-8 - UTF-8 - - + + software.amazon.amplifyuibuilder + aws-amplifyuibuilder-handlers + 1.0 + - - - software.amazon.awssdk - bom - 2.20.81 - pom - import - - - - software.amazon.awssdk - amplifyuibuilder - 2.20.81 - - software.amazon.amplifyuibuilder.common aws-amplifyuibuilder-common - 1.0 compile - software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok lombok - 1.18.30 provided - - - org.apache.logging.log4j - log4j-api - 2.17.1 - - - - org.apache.logging.log4j - log4j-core - 2.17.1 - - - org.apache.logging.log4j - log4j-slf4j-impl - 2.13.3 + software.amazon.awssdk + amplifyuibuilder - - org.assertj assertj-core - 3.12.2 test - org.junit.jupiter junit-jupiter - 5.5.0-M1 test - org.mockito mockito-core - 3.6.0 test - org.mockito mockito-junit-jupiter - 3.6.0 test @@ -109,75 +61,18 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 - - - -Xlint:all,-options,-processing - -Werror - - org.apache.maven.plugins maven-shade-plugin - 2.3 - - false - - - *:* - - **/Log4j2Plugins.dat - - - - - - - package - - shade - - - org.codehaus.mojo exec-maven-plugin - 1.6.0 - - - generate - generate-sources - - exec - - - cfn - generate ${cfn.generate.args} - ${project.basedir} - - - org.codehaus.mojo build-helper-maven-plugin - 3.0.0 - - - add-source - generate-sources - - add-source - - - - ${project.basedir}/target/generated-sources/rpdk - - - - org.apache.maven.plugins @@ -191,54 +86,6 @@ org.jacoco jacoco-maven-plugin - 0.8.4 - - - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - - - - - - prepare-agent - - - - report - test - - report - - - - jacoco-check - - check - - - - - PACKAGE - - - BRANCH - COVEREDRATIO - 0.8 - - - INSTRUCTION - COVEREDRATIO - 0.8 - - - - - - - diff --git a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/BaseHandlerStd.java b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/BaseHandlerStd.java index 2fe044b..1a4fab9 100644 --- a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/BaseHandlerStd.java +++ b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/BaseHandlerStd.java @@ -1,14 +1,31 @@ package software.amazon.amplifyuibuilder.form; +import software.amazon.amplifyuibuilder.common.TaggingHelpers; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; // Functionality that could be shared across Create/Read/Update/Delete/List Handlers + public abstract class BaseHandlerStd extends BaseHandler { + protected static ProgressEvent handleErrorInternal( + ResourceHandlerRequest awsRequest, + Exception exception, + ProxyClient client, + ResourceModel model, + CallbackContext context) { + if (TaggingHelpers.isTagBasedAccessDenied(exception)) { + return ProgressEvent.failed(model, context, HandlerErrorCode.UnauthorizedTaggingOperation, + exception.getMessage()); + } + + return ProgressEvent.failed(model, context, HandlerErrorCode.InternalFailure, exception.getMessage()); + } + @Override public final ProgressEvent handleRequest( final AmazonWebServicesClientProxy proxy, diff --git a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/CreateHandler.java b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/CreateHandler.java index 701cfe2..af62e6b 100644 --- a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/CreateHandler.java +++ b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/CreateHandler.java @@ -36,6 +36,9 @@ public ProgressEvent handleRequest( model.setId(response.entity().id()); return response; }) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) .progress()) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } diff --git a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/DeleteHandler.java b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/DeleteHandler.java index eeed0a4..2734d24 100644 --- a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/DeleteHandler.java +++ b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/DeleteHandler.java @@ -28,6 +28,9 @@ public ProgressEvent handleRequest( model.getId(), logger )) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) .progress() ) .then(progress -> ProgressEvent.defaultSuccessHandler(null)); diff --git a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/UpdateHandler.java b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/UpdateHandler.java index 02b6b89..7c864c8 100644 --- a/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/UpdateHandler.java +++ b/aws-amplifyuibuilder-form/src/main/java/software/amazon/amplifyuibuilder/form/UpdateHandler.java @@ -1,6 +1,7 @@ package software.amazon.amplifyuibuilder.form; import software.amazon.amplifyuibuilder.common.ClientWrapper; +import software.amazon.amplifyuibuilder.common.TaggingHelpers; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.UpdateFormResponse; import software.amazon.cloudformation.exceptions.CfnNotFoundException; @@ -23,18 +24,34 @@ public ProgressEvent handleRequest( } return ProgressEvent.progress(model, callbackContext) - .then(progress -> - proxy.initiate("AWS-AmplifyUIBuilder-Form::Update", proxyClient, model, progress.getCallbackContext()) - .translateToServiceRequest(Translator::translateToUpdateRequest) - .makeServiceCall((updateFormRequest, proxyInvocation) -> (UpdateFormResponse) ClientWrapper.execute( - proxy, - updateFormRequest, - proxyInvocation.client()::updateForm, - ResourceModel.TYPE_NAME, - model.getId(), - logger - )) - .progress()) + .then(progress -> proxy + .initiate("AWS-AmplifyUIBuilder-Form::Update", proxyClient, model, progress.getCallbackContext()) + .translateToServiceRequest(Translator::translateToUpdateRequest) + .makeServiceCall((updateFormRequest, proxyInvocation) -> { + UpdateFormResponse response = (UpdateFormResponse) ClientWrapper.execute( + proxy, + updateFormRequest, + proxyInvocation.client()::updateForm, + ResourceModel.TYPE_NAME, + model.getId(), + logger); + + final String formArn = TaggingHelpers.generateArn( + request.getRegion(), + request.getAwsAccountId(), + updateFormRequest.appId(), + updateFormRequest.environmentName(), + "forms", + updateFormRequest.id()); + + TaggingHelpers.updateTags(proxy, proxyInvocation, model.getAppId(), formArn, ResourceModel.TYPE_NAME, + response.entity().tags(), model.getTags(), logger); + return response; + }) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) + .progress()) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } } diff --git a/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/CreateHandlerTest.java b/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/CreateHandlerTest.java index d3be28d..65f9c42 100644 --- a/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/CreateHandlerTest.java +++ b/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/CreateHandlerTest.java @@ -6,162 +6,277 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.*; import software.amazon.cloudformation.proxy.*; import java.time.Duration; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { - @Mock - AmplifyUiBuilderClient sdkClient; - @Mock - private AmazonWebServicesClientProxy proxy; - @Mock - private ProxyClient proxyClient; - - @BeforeEach - public void setup() { - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, - () -> Duration.ofSeconds(600).toMillis()); - sdkClient = mock(AmplifyUiBuilderClient.class); - proxyClient = MOCK_PROXY(proxy, sdkClient); - } - - @AfterEach - public void tear_down() { - verify(sdkClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(sdkClient); - } - - @Test - public void handleRequest_SimpleSuccess() { - final CreateHandler handler = new CreateHandler(); - - final GetFormResponse getResponse = GetFormResponse.builder() - .form(Form.builder() - .id(ID) - .appId(APP_ID) - .environmentName(ENV_NAME) - .name(NAME) - .cta(Translator.mapCtaCFNToSDK(CTA)) - .labelDecorator(LABEL_DECORATOR) - .fields(MODEL_FIELDS) - .tags(TAGS) - .build()) - .build(); - - when(proxyClient.client().getForm(any(GetFormRequest.class))) - .thenReturn(getResponse); - - final CreateFormResponse createResponse = CreateFormResponse.builder() - .entity(Form.builder() - // Only need the ID because it's assigned to the model then used in the - // ReadHandler - .id(ID) - .build()) - .build(); - - when(proxyClient.client().createForm(any(CreateFormRequest.class))) - .thenReturn(createResponse); - - final ResourceModel model = ResourceModel.builder() - .appId(APP_ID) - .environmentName(ENV_NAME) - .name(NAME) - .dataType(DATA_TYPE) - .formActionType(ACTION_TYPE) - .fields(FIELDS) - .style(STYLES) - .sectionalElements(SECTIONAL_ELEMENTS) - .cta(CTA) - .labelDecorator(LABEL_DECORATOR) - .schemaVersion(SCHEMA_VERSION) - .tags(TAGS) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); - - final ProgressEvent response = handler.handleRequest(proxy, request, - new CallbackContext(), proxyClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel().getAppId()) - .isEqualTo(request.getDesiredResourceState().getAppId()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - @Test - public void handleRequest_NoTags() { - final CreateHandler handler = new CreateHandler(); - - final GetFormResponse getResponse = GetFormResponse.builder() - .form(Form.builder() - .id(ID) - .appId(APP_ID) - .environmentName(ENV_NAME) - .name(NAME) - .cta(Translator.mapCtaCFNToSDK(CTA)) - .labelDecorator(LABEL_DECORATOR) - .fields(MODEL_FIELDS) - .build()) - .build(); - - when(proxyClient.client().getForm(any(GetFormRequest.class))) - .thenReturn(getResponse); - - final CreateFormResponse createResponse = CreateFormResponse.builder() - .entity(Form.builder() - // Only need the ID because it's assigned to the model then used in the - // ReadHandler - .id(ID) - .build()) - .build(); - - when(proxyClient.client().createForm(any(CreateFormRequest.class))) - .thenReturn(createResponse); - - final ResourceModel model = ResourceModel.builder() - .appId(APP_ID) - .environmentName(ENV_NAME) - .name(NAME) - .dataType(DATA_TYPE) - .formActionType(ACTION_TYPE) - .fields(FIELDS) - .style(STYLES) - .sectionalElements(SECTIONAL_ELEMENTS) - .cta(CTA) - .labelDecorator(LABEL_DECORATOR) - .schemaVersion(SCHEMA_VERSION) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); - - final ProgressEvent response = handler.handleRequest(proxy, request, - new CallbackContext(), proxyClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel().getAppId()) - .isEqualTo(request.getDesiredResourceState().getAppId()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } + @Mock + AmplifyUiBuilderClient sdkClient; + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, + () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(AmplifyUiBuilderClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @AfterEach + public void tear_down() { + verify(sdkClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final CreateHandler handler = new CreateHandler(); + + final GetFormResponse getResponse = GetFormResponse.builder() + .form(Form.builder() + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .cta(Translator.mapCtaCFNToSDK(CTA)) + .labelDecorator(LABEL_DECORATOR) + .fields(MODEL_FIELDS) + .tags(TAGS) + .build()) + .build(); + + when(proxyClient.client().getForm(any(GetFormRequest.class))) + .thenReturn(getResponse); + + final CreateFormResponse createResponse = CreateFormResponse.builder() + .entity(Form.builder() + // Only need the ID because it's assigned to the model then used in the + // ReadHandler + .id(ID) + .build()) + .build(); + + when(proxyClient.client().createForm(any(CreateFormRequest.class))) + .thenReturn(createResponse); + + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .dataType(DATA_TYPE) + .formActionType(ACTION_TYPE) + .fields(FIELDS) + .style(STYLES) + .sectionalElements(SECTIONAL_ELEMENTS) + .cta(CTA) + .labelDecorator(LABEL_DECORATOR) + .schemaVersion(SCHEMA_VERSION) + .tags(TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel().getAppId()) + .isEqualTo(request.getDesiredResourceState().getAppId()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_WithSystemAndPreviousTags() { + final CreateHandler handler = new CreateHandler(); + + final GetFormResponse getResponse = GetFormResponse.builder() + .form(Form.builder() + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .cta(Translator.mapCtaCFNToSDK(CTA)) + .labelDecorator(LABEL_DECORATOR) + .fields(MODEL_FIELDS) + .tags(TAGS) + .build()) + .build(); + + when(proxyClient.client().getForm(any(GetFormRequest.class))) + .thenReturn(getResponse); + + final CreateFormResponse createResponse = CreateFormResponse.builder() + .entity(Form.builder() + // Only need the ID because it's assigned to the model then used in the + // ReadHandler + .id(ID) + .build()) + .build(); + + when(proxyClient.client().createForm(any(CreateFormRequest.class))) + .thenReturn(createResponse); + + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .dataType(DATA_TYPE) + .formActionType(ACTION_TYPE) + .fields(FIELDS) + .style(STYLES) + .sectionalElements(SECTIONAL_ELEMENTS) + .cta(CTA) + .labelDecorator(LABEL_DECORATOR) + .schemaVersion(SCHEMA_VERSION) + .tags(TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .previousSystemTags(Map.of("system-tag-key-1", "system-tag-value-1")) + .previousResourceTags(Map.of("resource-tag-key-1", "resource-tag-value-1")) + .desiredResourceState(model) + .systemTags(Map.of("system-tag-key-3", "system-tag-value-4")) + .desiredResourceTags(Map.of("resource-tag-key-3", "resource-tag-value-4")) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel().getAppId()) + .isEqualTo(request.getDesiredResourceState().getAppId()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_NoTags() { + final CreateHandler handler = new CreateHandler(); + + final GetFormResponse getResponse = GetFormResponse.builder() + .form(Form.builder() + .id(ID) + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .cta(Translator.mapCtaCFNToSDK(CTA)) + .labelDecorator(LABEL_DECORATOR) + .fields(MODEL_FIELDS) + .build()) + .build(); + + when(proxyClient.client().getForm(any(GetFormRequest.class))) + .thenReturn(getResponse); + + final CreateFormResponse createResponse = CreateFormResponse.builder() + .entity(Form.builder() + // Only need the ID because it's assigned to the model then used in the + // ReadHandler + .id(ID) + .build()) + .build(); + + when(proxyClient.client().createForm(any(CreateFormRequest.class))) + .thenReturn(createResponse); + + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .dataType(DATA_TYPE) + .formActionType(ACTION_TYPE) + .fields(FIELDS) + .style(STYLES) + .sectionalElements(SECTIONAL_ELEMENTS) + .cta(CTA) + .labelDecorator(LABEL_DECORATOR) + .schemaVersion(SCHEMA_VERSION) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel().getAppId()) + .isEqualTo(request.getDesiredResourceState().getAppId()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_taggingError_failure() { + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .dataType(DATA_TYPE) + .formActionType(ACTION_TYPE) + .fields(FIELDS) + .style(STYLES) + .sectionalElements(SECTIONAL_ELEMENTS) + .cta(CTA) + .labelDecorator(LABEL_DECORATOR) + .schemaVersion(SCHEMA_VERSION) + .tags(TAGS) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().createForm(any(CreateFormRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new CreateHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/DeleteHandlerTest.java b/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/DeleteHandlerTest.java index f4df038..c8d5265 100644 --- a/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/DeleteHandlerTest.java +++ b/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/DeleteHandlerTest.java @@ -6,6 +6,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.DeleteFormRequest; import software.amazon.awssdk.services.amplifyuibuilder.model.DeleteFormResponse; @@ -14,6 +18,7 @@ import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -72,4 +77,36 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } + + @Test + public void handleRequest_taggingError_failure() { + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .id(ID) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().deleteForm(any(DeleteFormRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new DeleteHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/UpdateHandlerTest.java b/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/UpdateHandlerTest.java index c91b0b3..21f0aa1 100644 --- a/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/UpdateHandlerTest.java +++ b/aws-amplifyuibuilder-form/src/test/java/software/amazon/amplifyuibuilder/form/UpdateHandlerTest.java @@ -1,22 +1,33 @@ package software.amazon.amplifyuibuilder.form; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.*; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.*; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class UpdateHandlerTest extends AbstractTestBase { @Mock @@ -50,12 +61,20 @@ public void handleRequest_SimpleSuccess() { .thenReturn(getResponse); final UpdateFormResponse updateResponse = UpdateFormResponse.builder() - .entity(Form.builder().build()) + .entity(Form.builder().tags(new HashMap()).build()) .build(); when(proxyClient.client().updateForm(any(UpdateFormRequest.class))) .thenReturn(updateResponse); + final TagResourceResponse tagResourceResponse = TagResourceResponse.builder().build(); + when(proxyClient.client().tagResource(any(TagResourceRequest.class))) + .thenReturn(tagResourceResponse); + + final UntagResourceResponse untagResourceResponse = UntagResourceResponse.builder().build(); + when(proxyClient.client().untagResource(any(UntagResourceRequest.class))) + .thenReturn(untagResourceResponse); + final ResourceModel model = ResourceModel.builder() .name(NAME) .environmentName(ENV_NAME) @@ -68,8 +87,8 @@ public void handleRequest_SimpleSuccess() { .desiredResourceState(model) .build(); - final ProgressEvent response - = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + final ProgressEvent response = handler.handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); @@ -102,12 +121,61 @@ public void handleRequest_UpdateWithoutCreate() { .id("") .build(); - final ResourceHandlerRequest requestWithEmptyStringID = ResourceHandlerRequest.builder() + final ResourceHandlerRequest requestWithEmptyStringID = ResourceHandlerRequest + .builder() .desiredResourceState(modelWithEmptyStringID) .build(); - Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, requestWithNullID, new CallbackContext(), proxyClient, logger)); + Assertions.assertThrows(CfnNotFoundException.class, + () -> handler.handleRequest(proxy, requestWithNullID, new CallbackContext(), proxyClient, logger)); + + Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, requestWithEmptyStringID, + new CallbackContext(), proxyClient, logger)); + } + + @Test + public void handleRequest_taggingError_failure() { + final GetFormResponse getResponse = GetFormResponse.builder() + .form(Form.builder() + .id(ID) + .name(NAME) + .environmentName(ENV_NAME) + .appId(APP_ID) + .build()) + .build(); + + when(proxyClient.client().getForm(any(GetFormRequest.class))) + .thenReturn(getResponse); - Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, requestWithEmptyStringID, new CallbackContext(), proxyClient, logger)); + final ResourceModel model = ResourceModel.builder() + .name(NAME) + .environmentName(ENV_NAME) + .appId(APP_ID) + .id(ID) + .cta(CTA) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().updateForm(any(UpdateFormRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new UpdateHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); } } diff --git a/aws-amplifyuibuilder-form/template.yml b/aws-amplifyuibuilder-form/template.yml index 917fb30..1dd461a 100644 --- a/aws-amplifyuibuilder-form/template.yml +++ b/aws-amplifyuibuilder-form/template.yml @@ -12,12 +12,12 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: software.amazon.amplifyuibuilder.form.HandlerWrapper::handleRequest - Runtime: java11 + Runtime: java17 CodeUri: ./target/aws-amplifyuibuilder-form-1.0.jar TestEntrypoint: Type: AWS::Serverless::Function Properties: Handler: software.amazon.amplifyuibuilder.form.HandlerWrapper::testEntrypoint - Runtime: java11 + Runtime: java17 CodeUri: ./target/aws-amplifyuibuilder-form-1.0.jar diff --git a/aws-amplifyuibuilder-theme/.rpdk-config b/aws-amplifyuibuilder-theme/.rpdk-config index d48280a..1b6914d 100644 --- a/aws-amplifyuibuilder-theme/.rpdk-config +++ b/aws-amplifyuibuilder-theme/.rpdk-config @@ -2,7 +2,7 @@ "artifact_type": "RESOURCE", "typeName": "AWS::AmplifyUIBuilder::Theme", "language": "java", - "runtime": "java11", + "runtime": "java17", "entrypoint": "software.amazon.amplifyuibuilder.theme.HandlerWrapper::handleRequest", "testEntrypoint": "software.amazon.amplifyuibuilder.theme.HandlerWrapper::testEntrypoint", "settings": { diff --git a/aws-amplifyuibuilder-theme/aws-amplifyuibuilder-theme.json b/aws-amplifyuibuilder-theme/aws-amplifyuibuilder-theme.json index 0a0a133..ec5afe1 100644 --- a/aws-amplifyuibuilder-theme/aws-amplifyuibuilder-theme.json +++ b/aws-amplifyuibuilder-theme/aws-amplifyuibuilder-theme.json @@ -151,7 +151,11 @@ "tagOnCreate": true, "tagUpdatable": true, "cloudFormationSystemTags": true, - "tagProperty": "/properties/Tags" + "tagProperty": "/properties/Tags", + "permissions": [ + "amplifyuibuilder:TagResource", + "amplifyuibuilder:UntagResource" + ] }, "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-amplifyuibuilder", "additionalProperties": false diff --git a/aws-amplifyuibuilder-theme/pom.xml b/aws-amplifyuibuilder-theme/pom.xml index 3a25495..4f83478 100644 --- a/aws-amplifyuibuilder-theme/pom.xml +++ b/aws-amplifyuibuilder-theme/pom.xml @@ -1,8 +1,6 @@ - + 4.0.0 software.amazon.amplifyuibuilder.theme @@ -11,98 +9,49 @@ 1.0-SNAPSHOT jar - - 11 - 11 - UTF-8 - UTF-8 - - - - - - - software.amazon.awssdk - bom - 2.20.81 - pom - import - - - + + software.amazon.amplifyuibuilder + aws-amplifyuibuilder-handlers + 1.0 + software.amazon.amplifyuibuilder.common aws-amplifyuibuilder-common - 1.0 compile - - software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) - org.projectlombok lombok - 1.18.4 provided - - - org.apache.logging.log4j - log4j-api - 2.17.1 - - - - org.apache.logging.log4j - log4j-core - 2.17.1 - - - - org.apache.logging.log4j - log4j-slf4j-impl - 2.17.1 - software.amazon.awssdk amplifyuibuilder - 2.20.81 - - org.assertj assertj-core - 3.12.2 test - org.junit.jupiter junit-jupiter - 5.5.0-M1 test - org.mockito mockito-core - 3.6.0 test - org.mockito mockito-junit-jupiter - 3.6.0 test @@ -112,128 +61,29 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 - - - -Xlint:all,-options,-processing - -Werror - - org.apache.maven.plugins maven-shade-plugin - 2.3 - - false - - - - package - - shade - - - org.codehaus.mojo exec-maven-plugin - 1.6.0 - - - generate - generate-sources - - exec - - - cfn - generate - ${project.basedir} - - - org.codehaus.mojo build-helper-maven-plugin - 3.0.0 - - - add-source - generate-sources - - add-source - - - - ${project.basedir}/target/generated-sources/rpdk - - - - org.apache.maven.plugins maven-resources-plugin - 2.4 maven-surefire-plugin - 3.0.0-M3 org.jacoco jacoco-maven-plugin - 0.8.4 - - - **/BaseConfiguration* - **/BaseHandler* - **/HandlerWrapper* - **/ResourceModel* - - - - - - prepare-agent - - - - report - test - - report - - - - jacoco-check - - check - - - - - PACKAGE - - - BRANCH - COVEREDRATIO - 0.8 - - - INSTRUCTION - COVEREDRATIO - 0.8 - - - - - - - diff --git a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/BaseHandlerStd.java b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/BaseHandlerStd.java index 02d4b7e..b122d6e 100644 --- a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/BaseHandlerStd.java +++ b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/BaseHandlerStd.java @@ -1,11 +1,26 @@ package software.amazon.amplifyuibuilder.theme; +import software.amazon.amplifyuibuilder.common.TaggingHelpers; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.cloudformation.proxy.*; // Placeholder for the functionality that could be shared across Create/Read/Update/Delete/List Handlers public abstract class BaseHandlerStd extends BaseHandler { + protected static ProgressEvent handleErrorInternal( + ResourceHandlerRequest awsRequest, + Exception exception, + ProxyClient client, + ResourceModel model, + CallbackContext context) { + if (TaggingHelpers.isTagBasedAccessDenied(exception)) { + return ProgressEvent.failed(model, context, HandlerErrorCode.UnauthorizedTaggingOperation, + exception.getMessage()); + } + + return ProgressEvent.failed(model, context, HandlerErrorCode.InternalFailure, exception.getMessage()); + } + @Override public final ProgressEvent handleRequest( final AmazonWebServicesClientProxy proxy, diff --git a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/CreateHandler.java b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/CreateHandler.java index 168c7bf..a6d8b05 100644 --- a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/CreateHandler.java +++ b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/CreateHandler.java @@ -40,6 +40,9 @@ protected ProgressEvent handleRequest( model.setId(response.entity().id()); return response; }) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) .progress()) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } diff --git a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/DeleteHandler.java b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/DeleteHandler.java index 0047454..2a989dd 100644 --- a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/DeleteHandler.java +++ b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/DeleteHandler.java @@ -30,6 +30,9 @@ protected ProgressEvent handleRequest( model.getId(), logger )) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) .progress() ) .then(progress -> ProgressEvent.defaultSuccessHandler(null)); diff --git a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/UpdateHandler.java b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/UpdateHandler.java index f74c7fe..8b8ffd8 100644 --- a/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/UpdateHandler.java +++ b/aws-amplifyuibuilder-theme/src/main/java/software/amazon/amplifyuibuilder/theme/UpdateHandler.java @@ -1,6 +1,7 @@ package software.amazon.amplifyuibuilder.theme; import software.amazon.amplifyuibuilder.common.ClientWrapper; +import software.amazon.amplifyuibuilder.common.TaggingHelpers; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.UpdateThemeResponse; import software.amazon.cloudformation.exceptions.CfnNotFoundException; @@ -26,13 +27,31 @@ protected ProgressEvent handleRequest( .then(progress -> proxy .initiate("AWS-AmplifyUIBuilder-Theme::Update", proxyClient, model, progress.getCallbackContext()) .translateToServiceRequest(Translator::translateToUpdateRequest) - .makeServiceCall((updateThemeRequest, proxyInvocation) -> (UpdateThemeResponse) ClientWrapper.execute( - proxy, - updateThemeRequest, - proxyInvocation.client()::updateTheme, - ResourceModel.TYPE_NAME, - model.getId(), - logger)) + .makeServiceCall((updateThemeRequest, proxyInvocation) -> { + UpdateThemeResponse response = (UpdateThemeResponse) ClientWrapper.execute( + proxy, + updateThemeRequest, + proxyInvocation.client()::updateTheme, + ResourceModel.TYPE_NAME, + model.getId(), + logger); + + final String themeArn = TaggingHelpers.generateArn( + request.getRegion(), + request.getAwsAccountId(), + updateThemeRequest.appId(), + updateThemeRequest.environmentName(), + "themes", + updateThemeRequest.id()); + + TaggingHelpers.updateTags(proxy, proxyInvocation, model.getAppId(), themeArn, ResourceModel.TYPE_NAME, + response.entity().tags(), model.getTags(), logger); + + return response; + }) + .handleError((awsRequest, exception, client, model1, context) -> { + return handleErrorInternal(request, exception, proxyClient, model1, callbackContext); + }) .progress()) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } diff --git a/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/CreateHandlerTest.java b/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/CreateHandlerTest.java index 39b4aa8..1ea1c26 100644 --- a/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/CreateHandlerTest.java +++ b/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/CreateHandlerTest.java @@ -4,6 +4,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.*; import software.amazon.cloudformation.proxy.*; @@ -189,4 +193,43 @@ public void handleRequest_NullSimpleSuccess() { assertThat(response.getResourceModel().getName()).isNull(); } + @Test + public void handleRequest_taggingError_failure() { + ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .overrides(null) + .values(null) + .tags(null) + .name(null) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + CreateThemeResponse createResponse = CreateThemeResponse.builder() + .build(); + + when(proxyClient.client().createTheme(any (CreateThemeRequest.class))) + .thenThrow(e) + .thenReturn(createResponse); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = + new CreateHandler().handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } + } diff --git a/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/DeleteHandlerTest.java b/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/DeleteHandlerTest.java index 4b5554d..d19f169 100644 --- a/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/DeleteHandlerTest.java +++ b/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/DeleteHandlerTest.java @@ -6,6 +6,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.DeleteThemeRequest; import software.amazon.awssdk.services.amplifyuibuilder.model.DeleteThemeResponse; @@ -14,6 +18,7 @@ import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -60,7 +65,8 @@ public void handleRequest_SimpleSuccess() { .desiredResourceState(model) .build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + final ProgressEvent response = handler.handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); @@ -70,4 +76,39 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } + + @Test + public void handleRequest_taggingError_failure() { + ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .overrides(null) + .values(null) + .tags(null) + .name(null) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().deleteTheme(any(DeleteThemeRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new DeleteHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/UpdateHandlerTest.java b/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/UpdateHandlerTest.java index c01a898..7b847ba 100644 --- a/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/UpdateHandlerTest.java +++ b/aws-amplifyuibuilder-theme/src/test/java/software/amazon/amplifyuibuilder/theme/UpdateHandlerTest.java @@ -6,6 +6,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import software.amazon.amplifyuibuilder.common.TaggingHelpers; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.amplifyuibuilder.AmplifyUiBuilderClient; import software.amazon.awssdk.services.amplifyuibuilder.model.*; import software.amazon.cloudformation.exceptions.CfnNotFoundException; @@ -14,121 +20,188 @@ import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static software.amazon.amplifyuibuilder.common.Transformer.transformList; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class UpdateHandlerTest extends AbstractTestBase { - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private ProxyClient proxyClient; - - @Mock - AmplifyUiBuilderClient sdkClient; - - @BeforeEach - public void setup() { - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - sdkClient = mock(AmplifyUiBuilderClient.class); - proxyClient = MOCK_PROXY(proxy, sdkClient); - } - - @Test - public void handleRequest_SimpleSuccess() { - final UpdateHandler handler = new UpdateHandler(); - - final GetThemeResponse getResponse = GetThemeResponse.builder() - .theme(Theme.builder() - .id(ID) - .name(NAME) - .tags(TAGS) - .appId(APP_ID) - .overrides(transformList(THEME_VALUES_LIST, Translator::translateThemeValuesFromCFNToSDK)) - .values(transformList(THEME_VALUES_LIST, Translator::translateThemeValuesFromCFNToSDK)) - .environmentName(ENV_NAME) - .modifiedAt(NOW) - .createdAt(NOW) - .build()) - .build(); - - when(proxyClient.client().getTheme(any(GetThemeRequest.class))) - .thenReturn(getResponse); - - final UpdateThemeResponse updateResponse = UpdateThemeResponse.builder() - .entity(Theme.builder().build()) - .build(); - - when(proxyClient.client().updateTheme(any(UpdateThemeRequest.class))) - .thenReturn(updateResponse); - - final ResourceModel model = ResourceModel.builder() - .appId(APP_ID) - .environmentName(ENV_NAME) - .id(ID) - .name(NAME) - .overrides(THEME_VALUES_LIST) - .values(THEME_VALUES_LIST) - .tags(TAGS) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); - - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); - ResourceModel actual = response.getResourceModel(); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(actual.getAppId()).isEqualTo(model.getAppId()); - assertThat(actual.getId()).isEqualTo(model.getId()); - assertThat(actual.getEnvironmentName()).isEqualTo(model.getEnvironmentName()); - assertThat(actual.getValues().size()).isEqualTo(model.getValues().size()); - assertThat(actual.getOverrides().size()).isEqualTo(model.getOverrides().size()); - assertThat(actual.getName()).isEqualTo(model.getName()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - } - - // Tests update without an ID - @Test - public void handleRequest_NullID() { - final UpdateHandler handler = new UpdateHandler(); - - final ResourceModel model = ResourceModel.builder() - .appId(APP_ID) - .environmentName(ENV_NAME) - .name(NAME) - .overrides(THEME_VALUES_LIST) - .values(THEME_VALUES_LIST) - .tags(TAGS) - .build(); - - final ResourceModel emptyStringIDModel = ResourceModel.builder() - .appId(APP_ID) - .id("") - .environmentName(ENV_NAME) - .name(NAME) - .overrides(THEME_VALUES_LIST) - .values(THEME_VALUES_LIST) - .tags(TAGS) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); - - final ResourceHandlerRequest emptyStringIDRequest = ResourceHandlerRequest.builder() - .desiredResourceState(emptyStringIDModel) - .build(); - - Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); - - Assertions.assertThrows(CfnNotFoundException.class, () -> handler.handleRequest(proxy, emptyStringIDRequest, new CallbackContext(), proxyClient, logger)); - } + @Mock + private AmazonWebServicesClientProxy proxy; + + @Mock + private ProxyClient proxyClient; + + @Mock + AmplifyUiBuilderClient sdkClient; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(AmplifyUiBuilderClient.class); + proxyClient = MOCK_PROXY(proxy, sdkClient); + } + + @Test + public void handleRequest_SimpleSuccess() { + final UpdateHandler handler = new UpdateHandler(); + + final GetThemeResponse getResponse = GetThemeResponse.builder() + .theme(Theme.builder() + .id(ID) + .name(NAME) + .tags(TAGS) + .appId(APP_ID) + .overrides(transformList(THEME_VALUES_LIST, Translator::translateThemeValuesFromCFNToSDK)) + .values(transformList(THEME_VALUES_LIST, Translator::translateThemeValuesFromCFNToSDK)) + .environmentName(ENV_NAME) + .modifiedAt(NOW) + .createdAt(NOW) + .build()) + .build(); + + when(proxyClient.client().getTheme(any(GetThemeRequest.class))) + .thenReturn(getResponse); + + final UpdateThemeResponse updateResponse = UpdateThemeResponse.builder() + .entity(Theme.builder().build()) + .build(); + + when(proxyClient.client().updateTheme(any(UpdateThemeRequest.class))) + .thenReturn(updateResponse); + + final TagResourceResponse tagResourceResponse = TagResourceResponse.builder().build(); + when(proxyClient.client().tagResource(any(TagResourceRequest.class))) + .thenReturn(tagResourceResponse); + + final UntagResourceResponse untagResourceResponse = UntagResourceResponse.builder().build(); + when(proxyClient.client().untagResource(any(UntagResourceRequest.class))) + .thenReturn(untagResourceResponse); + + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .id(ID) + .name(NAME) + .overrides(THEME_VALUES_LIST) + .values(THEME_VALUES_LIST) + .tags(TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = handler.handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + ResourceModel actual = response.getResourceModel(); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(actual.getAppId()).isEqualTo(model.getAppId()); + assertThat(actual.getId()).isEqualTo(model.getId()); + assertThat(actual.getEnvironmentName()).isEqualTo(model.getEnvironmentName()); + assertThat(actual.getValues().size()).isEqualTo(model.getValues().size()); + assertThat(actual.getOverrides().size()).isEqualTo(model.getOverrides().size()); + assertThat(actual.getName()).isEqualTo(model.getName()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + // Tests update without an ID + @Test + public void handleRequest_NullID() { + final UpdateHandler handler = new UpdateHandler(); + + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .name(NAME) + .overrides(THEME_VALUES_LIST) + .values(THEME_VALUES_LIST) + .tags(TAGS) + .build(); + + final ResourceModel emptyStringIDModel = ResourceModel.builder() + .appId(APP_ID) + .id("") + .environmentName(ENV_NAME) + .name(NAME) + .overrides(THEME_VALUES_LIST) + .values(THEME_VALUES_LIST) + .tags(TAGS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ResourceHandlerRequest emptyStringIDRequest = ResourceHandlerRequest + .builder() + .desiredResourceState(emptyStringIDModel) + .build(); + + Assertions.assertThrows(CfnNotFoundException.class, + () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger)); + + Assertions.assertThrows(CfnNotFoundException.class, + () -> handler.handleRequest(proxy, emptyStringIDRequest, new CallbackContext(), proxyClient, logger)); + } + + @Test + public void handleRequest_taggingError_failure() { + final GetThemeResponse getResponse = GetThemeResponse.builder() + .theme(Theme.builder() + .id(ID) + .name(NAME) + .tags(TAGS) + .appId(APP_ID) + .overrides(transformList(THEME_VALUES_LIST, Translator::translateThemeValuesFromCFNToSDK)) + .values(transformList(THEME_VALUES_LIST, Translator::translateThemeValuesFromCFNToSDK)) + .environmentName(ENV_NAME) + .modifiedAt(NOW) + .createdAt(NOW) + .build()) + .build(); + + when(proxyClient.client().getTheme(any(GetThemeRequest.class))) + .thenReturn(getResponse); + + final ResourceModel model = ResourceModel.builder() + .appId(APP_ID) + .environmentName(ENV_NAME) + .id(ID) + .name(NAME) + .overrides(THEME_VALUES_LIST) + .values(THEME_VALUES_LIST) + .tags(TAGS) + .build(); + + AwsServiceException e = AwsServiceException.builder() + .awsErrorDetails(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()) + .message(TaggingHelpers.SAMPLE_TAGGING_ACCESS_DENIED_MESSAGE) + .build(); + + when(proxyClient.client().updateTheme(any(UpdateThemeRequest.class))) + .thenThrow(e); + + ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + ProgressEvent response = new UpdateHandler().handleRequest(proxy, request, + new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.UnauthorizedTaggingOperation); + } } diff --git a/aws-amplifyuibuilder-theme/template.yml b/aws-amplifyuibuilder-theme/template.yml index 7ccf057..5bb047d 100644 --- a/aws-amplifyuibuilder-theme/template.yml +++ b/aws-amplifyuibuilder-theme/template.yml @@ -12,12 +12,12 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: software.amazon.amplifyuibuilder.theme.HandlerWrapper::handleRequest - Runtime: java11 + Runtime: java17 CodeUri: ./target/aws-amplifyuibuilder-theme-handler-1.0-SNAPSHOT.jar TestEntrypoint: Type: AWS::Serverless::Function Properties: Handler: software.amazon.amplifyuibuilder.theme.HandlerWrapper::testEntrypoint - Runtime: java11 + Runtime: java17 CodeUri: ./target/aws-amplifyuibuilder-theme-handler-1.0-SNAPSHOT.jar diff --git a/pom.xml b/pom.xml index 7085d6e..22c8632 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 software.amazon.amplifyuibuilder @@ -15,19 +14,206 @@ aws-amplifyuibuilder-component aws-amplifyuibuilder-form aws-amplifyuibuilder-theme - + + 17 + ${java.version} + ${java.version} + UTF-8 + UTF-8 + + + + + + + software.amazon.amplifyuibuilder.common + aws-amplifyuibuilder-common + 1.0 + compile + + + + software.amazon.awssdk + bom + 2.27.22 + pom + import + + + software.amazon.cloudformation + aws-cloudformation-rpdk-java-plugin + [2.0.0,3.0.0) + + + org.projectlombok + lombok + 1.18.26 + provided + + + org.apache.commons + commons-collections4 + 4.4 + + + org.assertj + assertj-core + 3.12.2 + test + + + org.junit.jupiter + junit-jupiter + 5.5.0-M1 + test + + + org.mockito + mockito-core + 3.6.0 + test + + + org.mockito + mockito-junit-jupiter + 3.6.0 + test + + + software.amazon.awssdk + amplifyuibuilder + 2.27.22 + + + + org.apache.maven.plugins maven-compiler-plugin + 3.8.1 + + + -Xlint:all,-options,-processing + -Werror + + + + + org.apache.maven.plugins + maven-shade-plugin + + false + + + + package + + shade + + + + + + org.codehaus.mojo + exec-maven-plugin + + + generate + generate-sources + + exec + + + cfn + generate + ${project.basedir} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/target/generated-sources/rpdk + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.0 + + + maven-surefire-plugin + 3.0.0 + + + org.jacoco + jacoco-maven-plugin + 0.8.7 - 11 - 11 + + **/BaseConfiguration* + **/BaseHandler* + **/HandlerWrapper* + **/ResourceModel* + + + + + prepare-agent + + + + report + test + + report + + + + jacoco-check + + check + + + + + PACKAGE + + + BRANCH + COVEREDRATIO + 0.8 + + + INSTRUCTION + COVEREDRATIO + 0.8 + + + + + + +