From f311722565d0cef85c63c689212b3ff28ffe00e9 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Tue, 24 Oct 2023 12:50:18 +0200 Subject: [PATCH 01/19] #1298 first implem - api gateway events validation will be catched and returned as a 400 error --- .../validation/internal/ValidationAspect.java | 115 ++++++++++++++---- ...aV7APIGatewayProxyRequestEventHandler.java | 34 ++++++ ...java => GenericSchemaV7StringHandler.java} | 5 +- ...nInboundAPIGatewayV2HTTPEventHandler.java} | 11 +- ...andledResponseEventsArgumentsProvider.java | 48 ++++++++ .../ResponseEventsArgumentsProvider.java | 7 +- .../internal/ValidationAspectTest.java | 102 ++++++++++++---- 7 files changed, 258 insertions(+), 64 deletions(-) create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java rename powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/{GenericSchemaV7Handler.java => GenericSchemaV7StringHandler.java} (92%) rename powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/{ValidationInboundStringHandler.java => ValidationInboundAPIGatewayV2HTTPEventHandler.java} (87%) create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 0d71104f3..46ee8d741 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -51,6 +51,7 @@ import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; +import software.amazon.lambda.powertools.validation.ValidationException; /** * Aspect for {@link Validation} annotation @@ -73,6 +74,11 @@ public Object around(ProceedingJoinPoint pjp, if (validation.schemaVersion() != V201909) { ValidationConfig.get().setSchemaVersion(validation.schemaVersion()); } + + // we need this result object to be null at this point as validation of API events, if + // it fails, will catch the ValidationException and generate a 400 API response. This response + // will be stored in the result object to prevent executing the lambda + Object result = null; if (placedOnRequestHandler(pjp)) { validationNeeded = true; @@ -85,10 +91,10 @@ public Object around(ProceedingJoinPoint pjp, validate(obj, inboundJsonSchema, validation.envelope()); } else if (obj instanceof APIGatewayProxyRequestEvent) { APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; - validate(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema); } else if (obj instanceof APIGatewayV2HTTPEvent) { APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; - validate(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema); } else if (obj instanceof SNSEvent) { SNSEvent event = (SNSEvent) obj; event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema)); @@ -140,33 +146,88 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); } } - Object result = pjp.proceed(proceedArgs); - - if (validationNeeded && !validation.outboundSchema().isEmpty()) { - JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); - - if (result instanceof APIGatewayProxyResponseEvent) { - APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof APIGatewayV2HTTPResponse) { - APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof APIGatewayV2WebSocketResponse) { - APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof ApplicationLoadBalancerResponseEvent) { - ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { - KinesisAnalyticsInputPreprocessingResponse response = - (KinesisAnalyticsInputPreprocessingResponse) result; - response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); - } else { - LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", - result.getClass().getName()); - } + // don't execute the lambda if result was set by previous validation step + // in that case result should already hold a response with validation information + if (result != null) { + LOG.error("Incoming API event's body failed inbound schema validation."); } + else { + result = pjp.proceed(proceedArgs); + + if (validationNeeded && !validation.outboundSchema().isEmpty()) { + JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); + + Object overridenResponse = null; + if (result instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; + overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema); + } else if (result instanceof APIGatewayV2HTTPResponse) { + APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; + overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema); + } else if (result instanceof APIGatewayV2WebSocketResponse) { + APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof ApplicationLoadBalancerResponseEvent) { + ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { + KinesisAnalyticsInputPreprocessingResponse response = + (KinesisAnalyticsInputPreprocessingResponse) result; + response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); + } else { + LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", + result.getClass().getName()); + } + + if (overridenResponse != null) { + result = overridenResponse; + LOG.error("API response failed outbound schema validation."); + } + } + } return result; } + + /** + * Validates the given body against the provided JsonSchema. If validation fails the ValidationException + * will be catched and transformed to a 400, bad request, API response + * @param body body of the event to validate + * @param inboundJsonSchema validation schema + * @return null if validation passed, or a 400 response object otherwise + */ + private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema) { + APIGatewayProxyResponseEvent result = null; + try { + validate(body, jsonSchema); + } catch (ValidationException e) { + LOG.error("There were validation errors: {}", e.getMessage()); + result = new APIGatewayProxyResponseEvent(); + result.setBody(e.getMessage()); + result.setStatusCode(400); + result.setIsBase64Encoded(false); + } + return result; + } + + /** + * Validates the given body against the provided JsonSchema. If validation fails the ValidationException + * will be catched and transformed to a 400, bad request, API response + * @param body body of the event to validate + * @param inboundJsonSchema validation schema + * @return null if validation passed, or a 400 response object otherwise + */ + private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema) { + APIGatewayV2HTTPResponse result = null; + try { + validate(body, jsonSchema); + } catch (ValidationException e) { + LOG.error("There were validation errors: {}", e.getMessage()); + result = new APIGatewayV2HTTPResponse(); + result.setBody(e.getMessage()); + result.setStatusCode(400); + result.setIsBase64Encoded(false); + } + return result; + } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java new file mode 100644 index 000000000..74e8605a5 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +import software.amazon.lambda.powertools.validation.Validation; + +public class GenericSchemaV7APIGatewayProxyRequestEventHandler implements RequestHandler { + + @Validation(inboundSchema = "classpath:/schema_v7.json") + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); + response.setBody("valid-test"); + response.setStatusCode(200); + return response; + } +} \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java similarity index 92% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java index 5b8343d1b..ab0645f29 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java @@ -16,13 +16,14 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.validation.Validation; -public class GenericSchemaV7Handler implements RequestHandler { +public class GenericSchemaV7StringHandler implements RequestHandler { @Validation(inboundSchema = "classpath:/schema_v7.json") @Override public String handleRequest(T input, Context context) { return "OK"; } -} +} \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java similarity index 87% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java index fd5692884..b8c67b1eb 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java @@ -17,10 +17,12 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; + import software.amazon.lambda.powertools.validation.Validation; -public class ValidationInboundStringHandler implements RequestHandler { +public class ValidationInboundAPIGatewayV2HTTPEventHandler implements RequestHandler { private static final String schema = "{\n" + " \"$schema\": \"http://json-schema.org/draft-07/schema\",\n" + @@ -80,7 +82,10 @@ public class ValidationInboundStringHandler implements RequestHandler provideArguments(ExtensionContext context) { + + String body = "{id"; + + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); + + APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGWV2HTTPResponse.setBody(body); + + APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); + apiGWV2WebSocketResponse.setBody(body); + + ApplicationLoadBalancerResponseEvent albResponseEvent = new ApplicationLoadBalancerResponseEvent(); + albResponseEvent.setBody(body); + + return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java index b634d6f8c..74803a05a 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java @@ -35,11 +35,6 @@ public Stream provideArguments(ExtensionContext context) { String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); - - APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); - apiGWV2HTTPResponse.setBody(body); - APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); apiGWV2WebSocketResponse.setBody(body); @@ -53,7 +48,7 @@ public Stream provideArguments(ExtensionContext context) { KinesisAnalyticsInputPreprocessingResponse.Result.Ok, buffer)); kaipResponse.setRecords(records); - return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse, apiGWV2WebSocketResponse, albResponseEvent, + return Stream.of(apiGWV2WebSocketResponse, albResponseEvent, kaipResponse).map(Arguments::of); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index c8d5e7ade..858d2e933 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -17,11 +17,28 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; +import java.io.IOException; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; @@ -40,25 +57,15 @@ import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import com.networknt.schema.SpecVersion; -import java.io.IOException; -import java.util.stream.Stream; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.Signature; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; + import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; import software.amazon.lambda.powertools.validation.ValidationException; -import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7Handler; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7APIGatewayProxyRequestEventHandler; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7StringHandler; import software.amazon.lambda.powertools.validation.handlers.SQSWithCustomEnvelopeHandler; import software.amazon.lambda.powertools.validation.handlers.SQSWithWrongEnvelopeHandler; -import software.amazon.lambda.powertools.validation.handlers.ValidationInboundStringHandler; +import software.amazon.lambda.powertools.validation.handlers.ValidationInboundAPIGatewayV2HTTPEventHandler; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; @@ -99,7 +106,7 @@ void setUp() { @ParameterizedTest @ArgumentsSource(ResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchema(Object object) throws Throwable { + public void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -114,6 +121,37 @@ public void testValidateOutboundJsonSchema(Object object) throws Throwable { validationAspect.around(pjp, validation); }); } + + @ParameterizedTest + @ArgumentsSource(HandledResponseEventsArgumentsProvider.class) + public void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { + when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); + when(pjp.getSignature()).thenReturn(signature); + when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); + Object[] args = {new Object(), context}; + when(pjp.getArgs()).thenReturn(args); + when(pjp.proceed(args)).thenReturn(object); + when(validation.inboundSchema()).thenReturn(""); + when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json"); + + Object response = validationAspect.around(pjp, validation); + assertThat(response).isInstanceOfAny(APIGatewayProxyResponseEvent.class, APIGatewayV2HTTPResponse.class); + if (response instanceof APIGatewayProxyResponseEvent) { + assertThat(response).isInstanceOfSatisfying(APIGatewayProxyResponseEvent.class, t -> { + assertThat(t.getStatusCode()).isEqualTo(400); + assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getIsBase64Encoded()).isFalse(); + }); + } else if (response instanceof APIGatewayV2HTTPResponse) { + assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { + assertThat(t.getStatusCode()).isEqualTo(400); + assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getIsBase64Encoded()).isFalse(); + }); + } else { + fail(); + } + } @Test public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { @@ -137,19 +175,22 @@ public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { @Test public void validate_inputOK_schemaInClasspath_shouldValidate() { - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": 42" + "}"); - assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } @Test public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + @@ -157,30 +198,39 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() " \"price\": -2" + "}"); // price is negative - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getStatusCode()).isEqualTo(400); } @Test public void validate_inputOK_schemaInString_shouldValidate() { - ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": 42" + "}"); - assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } + @Test public void validate_inputKO_schemaInString_shouldThrowValidationException() { - ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"" + "}"); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getStatusCode()).isEqualTo(400); } @Test @@ -189,7 +239,7 @@ public void validate_SQS() { LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs.json")); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } @@ -219,7 +269,7 @@ public void validate_Kinesis() { LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); KinesisEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/kinesis.json")); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } @@ -229,7 +279,7 @@ public void validateEEvent(String jsonResource, Class eventClass) throws IOExcep Object event = ValidationConfig.get().getObjectMapper() .readValue(this.getClass().getResourceAsStream(jsonResource), eventClass); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } } From ddbb2a18e6a25c1d21fcb46af8d3c979a0c9684d Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Tue, 24 Oct 2023 14:42:02 +0200 Subject: [PATCH 02/19] #1298 updated tests in pt-examples-validation + sonar qaulity fixes --- .../org/demo/validation/InboundValidationTest.java | 6 ++++-- .../validation/internal/ValidationAspectTest.java | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java b/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java index d5e6de313..f56051b73 100644 --- a/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java +++ b/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java @@ -53,13 +53,15 @@ public void shouldReturnOkStatusWhenInputIsValid() { } @Test - public void shouldThrowExceptionWhenRequestInInvalid() { + public void shouldReturnBadRequestWhenRequestInInvalid() { String bodyWithMissedId = "{\n" + " \"name\": \"FooBar XY\",\n" + " \"price\": 258\n" + " }"; APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent().withBody(bodyWithMissedId); - assertThrows(ValidationException.class, () -> inboundValidation.handleRequest(request, context)); + APIGatewayProxyResponseEvent response = inboundValidation.handleRequest(request, context); + + assertEquals(400, response.getStatusCode()); } } \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 858d2e933..431d8718b 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -106,7 +106,7 @@ void setUp() { @ParameterizedTest @ArgumentsSource(ResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { + void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -124,7 +124,7 @@ public void testValidateOutboundJsonSchemaWithExceptions(Object object) throws T @ParameterizedTest @ArgumentsSource(HandledResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { + void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -139,13 +139,13 @@ public void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) t if (response instanceof APIGatewayProxyResponseEvent) { assertThat(response).isInstanceOfSatisfying(APIGatewayProxyResponseEvent.class, t -> { assertThat(t.getStatusCode()).isEqualTo(400); - assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); }); } else if (response instanceof APIGatewayV2HTTPResponse) { assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { assertThat(t.getStatusCode()).isEqualTo(400); - assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); }); } else { @@ -199,7 +199,7 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() "}"); // price is negative APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); - assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); } @@ -229,7 +229,7 @@ public void validate_inputKO_schemaInString_shouldThrowValidationException() { "}"); APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); - assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); } From eda478da1ab06fb9c231cc38d42686bdf34eb870 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Tue, 24 Oct 2023 14:44:36 +0200 Subject: [PATCH 03/19] #1298 code smell --- .../test/java/org/demo/validation/InboundValidationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java b/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java index f56051b73..cce7b99a9 100644 --- a/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java +++ b/examples/powertools-examples-validation/src/test/java/org/demo/validation/InboundValidationTest.java @@ -53,7 +53,7 @@ public void shouldReturnOkStatusWhenInputIsValid() { } @Test - public void shouldReturnBadRequestWhenRequestInInvalid() { + void shouldReturnBadRequestWhenRequestInInvalid() { String bodyWithMissedId = "{\n" + " \"name\": \"FooBar XY\",\n" + " \"price\": 258\n" + From 4808b8f739379bf305eb9dc61cb98206d8de5bc5 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Thu, 26 Oct 2023 14:16:12 +0200 Subject: [PATCH 04/19] 1298 introduced E2E tests for validation --- powertools-e2e-tests/handlers/pom.xml | 6 ++ .../handlers/validation/pom.xml | 60 +++++++++++ .../lambda/powertools/e2e/Function.java | 33 +++++++ .../validation/src/main/resources/log4j2.xml | 16 +++ .../resources/validation/inbound_schema.json | 32 ++++++ .../resources/validation/outbound_schema.json | 32 ++++++ powertools-e2e-tests/pom.xml | 7 ++ .../lambda/powertools/ValidationE2ET.java | 99 +++++++++++++++++++ .../validation/invalid_api_gw_in_event.json | 62 ++++++++++++ .../validation/invalid_api_gw_out_event.json | 62 ++++++++++++ .../validation/valid_api_gw_in_out_event.json | 62 ++++++++++++ 11 files changed, 471 insertions(+) create mode 100644 powertools-e2e-tests/handlers/validation/pom.xml create mode 100644 powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java create mode 100644 powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json create mode 100644 powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index fbe2e6d8b..e341b8410 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -32,6 +32,7 @@ metrics idempotency parameters + validation @@ -79,6 +80,11 @@ powertools-batch ${lambda.powertools.version} + + software.amazon.lambda + powertools-validation + ${lambda.powertools.version} + com.amazonaws aws-lambda-java-core diff --git a/powertools-e2e-tests/handlers/validation/pom.xml b/powertools-e2e-tests/handlers/validation/pom.xml new file mode 100644 index 000000000..2a7d1baf9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 1.0.0 + + + e2e-test-handler-validation + jar + A Lambda function using Powertools for AWS Lambda (Java) validation + + + + software.amazon.lambda + powertools-validation + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-validation + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..5ac8951d8 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +import software.amazon.lambda.powertools.validation.Validation; + +public class Function implements RequestHandler { + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json new file mode 100644 index 000000000..3665879eb --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json new file mode 100644 index 000000000..b1f14d025 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMaximum": 1000, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index b150a179e..6a70c3af1 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -172,6 +172,13 @@ powertools-serialization test + + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + test + diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java new file mode 100644 index 000000000..2842ae592 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +class ValidationE2ET { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationE2ET.class.getSimpleName()) + .pathToFunction("validation").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @ParameterizedTest + @Event(value = "/validation/valid_api_gw_in_out_event.json", type = APIGatewayProxyRequestEvent.class) + void test_validInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @ParameterizedTest + @Event(value = "/validation/invalid_api_gw_in_event.json", type = APIGatewayProxyRequestEvent.class) + void test_invalidInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + + // THEN + // invocation should fail inbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains("$.price: is missing but it is required"); + } + + @ParameterizedTest + @Event(value = "/validation/invalid_api_gw_out_event.json", type = APIGatewayProxyRequestEvent.class) + void test_invalidOutboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains("$.price: must have an exclusive maximum value of 1000"); + } +} diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json new file mode 100644 index 000000000..014ef9f05 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"Lambda rocks\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json new file mode 100644 index 000000000..b7ef1780c --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"price\": 50000}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json new file mode 100644 index 000000000..8cb8ea27a --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"price\": 150}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file From ecf056e8945ed186dbf24ac51b8333c8fbe91991 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Thu, 26 Oct 2023 15:05:08 +0200 Subject: [PATCH 05/19] #1298 updated documentation to match new behavior of @Validation --- docs/utilities/validation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index 928ffb6c8..be8a3ac8e 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -156,7 +156,9 @@ We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson librar `@Validation` annotation is used to validate either inbound events or functions' response. -It will fail fast with `ValidationException` if an event or response doesn't conform with given JSON Schema. +It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. But for +specific `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent`, instead, the `@Validation` annotation will build and return a custom +400 (BAD_REQUEST) response. Its body will contain the validation error(s) message(s). While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema. From 3f64d55b9a7a64b37503ec4822b933adaa2feb1d Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Fri, 27 Oct 2023 14:32:10 +0200 Subject: [PATCH 06/19] #1298 implemented suggested changes --- docs/utilities/validation.md | 7 +-- powertools-e2e-tests/pom.xml | 7 --- .../lambda/powertools/ValidationE2ET.java | 44 +++++++++++-------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index be8a3ac8e..dfd97e0d4 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -156,9 +156,10 @@ We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson librar `@Validation` annotation is used to validate either inbound events or functions' response. -It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. But for -specific `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent`, instead, the `@Validation` annotation will build and return a custom -400 (BAD_REQUEST) response. Its body will contain the validation error(s) message(s). +It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. +For API gateway events associated with REST APIs and HTTP APIs - `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent` - the `@Validation` +annotation will build and return a custom 400 / "Bad Request" response, with a body containing the validation errors. This saves you from having +to catch the validation exception and map it back to a meaningful user error yourself. While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema. diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 6a70c3af1..b150a179e 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -172,13 +172,6 @@ powertools-serialization test - - - com.amazonaws - aws-lambda-java-tests - 1.1.1 - test - diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java index 2842ae592..bd2c42a82 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java @@ -19,16 +19,17 @@ import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.tests.annotations.Event; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -58,11 +59,13 @@ public static void tearDown() { } } - @ParameterizedTest - @Event(value = "/validation/valid_api_gw_in_out_event.json", type = APIGatewayProxyRequestEvent.class) - void test_validInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + @Test + void test_validInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + // WHEN - InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + InvocationResult invocationResult = invokeFunction(functionName, validEvent); // THEN // invocation should pass validation and return 200 @@ -70,12 +73,14 @@ void test_validInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); } - - @ParameterizedTest - @Event(value = "/validation/invalid_api_gw_in_event.json", type = APIGatewayProxyRequestEvent.class) - void test_invalidInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + + @Test + void test_invalidInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + // WHEN - InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); // THEN // invocation should fail inbound validation and return 400 @@ -83,17 +88,20 @@ void test_invalidInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throw assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); assertThat(validJsonNode.get("body").asText()).contains("$.price: is missing but it is required"); } - - @ParameterizedTest - @Event(value = "/validation/invalid_api_gw_out_event.json", type = APIGatewayProxyRequestEvent.class) - void test_invalidOutboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + + @Test + void test_invalidOutboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + // WHEN - InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); // THEN // invocation should fail outbound validation and return 400 JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()).contains("$.price: must have an exclusive maximum value of 1000"); + assertThat(validJsonNode.get("body").asText()) + .contains("$.price: must have an exclusive maximum value of 1000"); } } From 46f7f4cd127580135b3b566f7dfe79f6f8cb57be Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Mon, 30 Oct 2023 15:41:13 +0100 Subject: [PATCH 07/19] #1298 added new E2E tests for ALB event, which should trigger a validation exception --- .../pom.xml | 2 +- .../lambda/powertools/e2e/Function.java | 40 +++++++ .../src/main/resources/log4j2.xml | 0 .../resources/validation/inbound_schema.json | 0 .../resources/validation/outbound_schema.json | 0 .../handlers/validation-apigw-event/pom.xml | 60 ++++++++++ .../lambda/powertools/e2e/Function.java | 0 .../src/main/resources/log4j2.xml | 16 +++ .../resources/validation/inbound_schema.json | 32 ++++++ .../resources/validation/outbound_schema.json | 32 ++++++ .../lambda/powertools/ValidationALBE2ET.java | 104 ++++++++++++++++++ ...tionE2ET.java => ValidationApiGWE2ET.java} | 6 +- .../validation/invalid_alb_in_event.json | 28 +++++ .../validation/invalid_alb_out_event.json | 28 +++++ .../validation/valid_alb_in_out_event.json | 28 +++++ 15 files changed, 372 insertions(+), 4 deletions(-) rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/pom.xml (97%) create mode 100644 powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/src/main/resources/log4j2.xml (100%) rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/src/main/resources/validation/inbound_schema.json (100%) rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/src/main/resources/validation/outbound_schema.json (100%) create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/pom.xml rename powertools-e2e-tests/handlers/{validation => validation-apigw-event}/src/main/java/software/amazon/lambda/powertools/e2e/Function.java (100%) create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java rename powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/{ValidationE2ET.java => ValidationApiGWE2ET.java} (95%) create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json diff --git a/powertools-e2e-tests/handlers/validation/pom.xml b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml similarity index 97% rename from powertools-e2e-tests/handlers/validation/pom.xml rename to powertools-e2e-tests/handlers/validation-alb-event/pom.xml index 2a7d1baf9..31570fe4e 100644 --- a/powertools-e2e-tests/handlers/validation/pom.xml +++ b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml @@ -8,7 +8,7 @@ 1.0.0 - e2e-test-handler-validation + e2e-test-handler-validation-alb-event jar A Lambda function using Powertools for AWS Lambda (Java) validation diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..70a9bb3f3 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; + +import software.amazon.lambda.powertools.validation.Validation; +public class Function implements RequestHandler { +// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") +// public String handleRequest(SQSEvent input, Context context) { +// return "OK"; +// } + + + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") +// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json") + public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, Context context) { + ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml new file mode 100644 index 000000000..9129abc7d --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 1.0.0 + + + e2e-test-handler-validation-apigw-event + jar + A Lambda function using Powertools for AWS Lambda (Java) validation + + + + software.amazon.lambda + powertools-validation + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-validation + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java rename to powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json new file mode 100644 index 000000000..3665879eb --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json new file mode 100644 index 000000000..b1f14d025 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMaximum": 1000, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java new file mode 100644 index 000000000..324c77a34 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +class ValidationALBE2ET { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) + .pathToFunction("validation-alb-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @Test + void test_invalidInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return an error message + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains("$.price: is missing but it is required"); + } + + @Test + void test_invalidOutboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains("$.price: must have an exclusive maximum value of 1000"); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java similarity index 95% rename from powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java rename to powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java index bd2c42a82..af7c7d87c 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java @@ -36,7 +36,7 @@ import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -class ValidationE2ET { +class ValidationApiGWE2ET { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -46,8 +46,8 @@ class ValidationE2ET { @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) public static void setup() { - infrastructure = Infrastructure.builder().testName(ValidationE2ET.class.getSimpleName()) - .pathToFunction("validation").build(); + infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) + .pathToFunction("validation-apigw-event").build(); Map outputs = infrastructure.deploy(); functionName = outputs.get(FUNCTION_NAME_OUTPUT); } diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json new file mode 100644 index 000000000..ebad834d8 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"message\": \"Lambda rocks\"}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json new file mode 100644 index 000000000..1b1961063 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"price\": 50000}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json new file mode 100644 index 000000000..35560f109 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"price\": 150}", + "isBase64Encoded": true +} \ No newline at end of file From 3a486565f05822d6c6af4f5fb302fd8a01b6e6e0 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Mon, 30 Oct 2023 16:37:20 +0100 Subject: [PATCH 08/19] #1298 original headers are returned as part of a 400 validation failed response --- .../validation/internal/ValidationAspect.java | 28 +++++++++++++++---- ...andledResponseEventsArgumentsProvider.java | 9 +++++- .../internal/ValidationAspectTest.java | 17 ++++++++++- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 46ee8d741..978be16de 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -22,6 +22,10 @@ import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; import static software.amazon.lambda.powertools.validation.ValidationUtils.validate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; @@ -91,10 +95,10 @@ public Object around(ProceedingJoinPoint pjp, validate(obj, inboundJsonSchema, validation.envelope()); } else if (obj instanceof APIGatewayProxyRequestEvent) { APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; - result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema, null, null); } else if (obj instanceof APIGatewayV2HTTPEvent) { APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; - result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema, null, null); } else if (obj instanceof SNSEvent) { SNSEvent event = (SNSEvent) obj; event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema)); @@ -158,12 +162,18 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); Object overridenResponse = null; + // The normal behavior of @Validation is to throw an exception if response's validation fails. + // but in the case of APIGatewayProxyResponseEvent and APIGatewayV2HTTPResponse we want to return + // a 400 response with the validation errors instead of throwing an exception. if (result instanceof APIGatewayProxyResponseEvent) { APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; - overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema); + overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema, response.getHeaders(), + response.getMultiValueHeaders()); } else if (result instanceof APIGatewayV2HTTPResponse) { APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; - overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema); + overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema, response.getHeaders(), + response.getMultiValueHeaders()); + // all type of below responses will throw an exception if validation fails } else if (result instanceof APIGatewayV2WebSocketResponse) { APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; validate(response.getBody(), outboundJsonSchema); @@ -196,7 +206,8 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); * @param inboundJsonSchema validation schema * @return null if validation passed, or a 400 response object otherwise */ - private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema) { + private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema, + final Map headers, Map> multivalueHeaders) { APIGatewayProxyResponseEvent result = null; try { validate(body, jsonSchema); @@ -204,6 +215,8 @@ private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String bo LOG.error("There were validation errors: {}", e.getMessage()); result = new APIGatewayProxyResponseEvent(); result.setBody(e.getMessage()); + result.setHeaders(headers == null ? Collections.emptyMap() : headers); + result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders); result.setStatusCode(400); result.setIsBase64Encoded(false); } @@ -217,7 +230,8 @@ private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String bo * @param inboundJsonSchema validation schema * @return null if validation passed, or a 400 response object otherwise */ - private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema) { + private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema, + final Map headers, Map> multivalueHeaders) { APIGatewayV2HTTPResponse result = null; try { validate(body, jsonSchema); @@ -225,6 +239,8 @@ private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, LOG.error("There were validation errors: {}", e.getMessage()); result = new APIGatewayV2HTTPResponse(); result.setBody(e.getMessage()); + result.setHeaders(headers == null ? Collections.emptyMap() : headers); + result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders); result.setStatusCode(400); result.setIsBase64Encoded(false); } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java index 1c48fc0ad..c675c8479 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java @@ -14,6 +14,8 @@ package software.amazon.lambda.powertools.validation.internal; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; @@ -32,10 +34,15 @@ public Stream provideArguments(ExtensionContext context) { String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() + .withBody(body) + .withHeaders(Map.of("header1", "value1,value2,value3")) + .withMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); apiGWV2HTTPResponse.setBody(body); + apiGWV2HTTPResponse.setHeaders(Map.of("header1", "value1,value2,value3")); + apiGWV2HTTPResponse.setMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); apiGWV2WebSocketResponse.setBody(body); diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 431d8718b..d05453f12 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -21,9 +21,10 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.junit.jupiter.api.BeforeEach; @@ -141,12 +142,16 @@ void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws T assertThat(t.getStatusCode()).isEqualTo(400); assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); + assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3"); + assertThat(t.getMultiValueHeaders()).containsEntry("header1", List.of("value1", "value2", "value3")); }); } else if (response instanceof APIGatewayV2HTTPResponse) { assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { assertThat(t.getStatusCode()).isEqualTo(400); assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); + assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3"); + assertThat(t.getMultiValueHeaders()).containsEntry("header1", List.of("value1", "value2", "value3")); }); } else { fail(); @@ -183,9 +188,11 @@ public void validate_inputOK_schemaInClasspath_shouldValidate() { " \"price\": 42" + "}"); + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); assertThat(response.getBody()).isEqualTo("valid-test"); assertThat(response.getStatusCode()).isEqualTo(200); + } @Test @@ -197,10 +204,15 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() " \"name\": \"Lampshade\"," + " \"price\": -2" + "}"); + event.setHeaders(Map.of("header1", "value1")); + event.setMultiValueHeaders(Map.of("header1", List.of("value1"))); + // price is negative APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test @@ -227,10 +239,13 @@ public void validate_inputKO_schemaInString_shouldThrowValidationException() { " \"id\": 1," + " \"name\": \"Lampshade\"" + "}"); + event.setHeaders(Map.of("header1", "value1")); APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test From 80ceb05306de68b3e7d85efefc6306f935921d9e Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Fri, 24 Nov 2023 09:48:35 +0100 Subject: [PATCH 09/19] #1298 javadoc --- .../lambda/powertools/e2e/Function.java | 28 ++++++------- ...andledResponseEventsArgumentsProvider.java | 40 +++++++++---------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 70a9bb3f3..d221ee153 100644 --- a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -21,20 +21,16 @@ import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import software.amazon.lambda.powertools.validation.Validation; -public class Function implements RequestHandler { -// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") -// public String handleRequest(SQSEvent input, Context context) { -// return "OK"; -// } - - - @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") -// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json") - public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, Context context) { - ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); - response.setBody(input.getBody()); - response.setStatusCode(200); - response.setIsBase64Encoded(false); - return response; - } + +public class Function + implements RequestHandler { + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") + public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, + Context context) { + ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } } \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java index c675c8479..3a3120f0d 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java @@ -24,32 +24,30 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; -import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketResponse; -import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; +/** + * Provides test arguments that are used in unit tests. + * It creates API Gateway response arguments that can be used to confirm + * that @Validation validates responses and returns a response's headers even + * when validation fails + */ public class HandledResponseEventsArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - - String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() - .withBody(body) - .withHeaders(Map.of("header1", "value1,value2,value3")) - .withMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); + @Override + public Stream provideArguments(ExtensionContext context) { - APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); - apiGWV2HTTPResponse.setBody(body); - apiGWV2HTTPResponse.setHeaders(Map.of("header1", "value1,value2,value3")); - apiGWV2HTTPResponse.setMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); + String body = "{id"; - APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); - apiGWV2WebSocketResponse.setBody(body); + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() + .withBody(body) + .withHeaders(Map.of("header1", "value1,value2,value3")) + .withMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); - ApplicationLoadBalancerResponseEvent albResponseEvent = new ApplicationLoadBalancerResponseEvent(); - albResponseEvent.setBody(body); + APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGWV2HTTPResponse.setBody(body); + apiGWV2HTTPResponse.setHeaders(Map.of("header1", "value1,value2,value3")); + apiGWV2HTTPResponse.setMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); - return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); - } + return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); + } } From 5387e4986a1fa394936764b3177c434646a97443 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Tue, 24 Oct 2023 12:50:18 +0200 Subject: [PATCH 10/19] #1298 first implem - api gateway events validation will be catched and returned as a 400 error --- .../validation/internal/ValidationAspect.java | 115 ++++++++++++++---- ...aV7APIGatewayProxyRequestEventHandler.java | 34 ++++++ ...java => GenericSchemaV7StringHandler.java} | 5 +- ...nInboundAPIGatewayV2HTTPEventHandler.java} | 11 +- ...andledResponseEventsArgumentsProvider.java | 48 ++++++++ .../ResponseEventsArgumentsProvider.java | 7 +- .../internal/ValidationAspectTest.java | 102 ++++++++++++---- 7 files changed, 258 insertions(+), 64 deletions(-) create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java rename powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/{GenericSchemaV7Handler.java => GenericSchemaV7StringHandler.java} (92%) rename powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/{ValidationInboundStringHandler.java => ValidationInboundAPIGatewayV2HTTPEventHandler.java} (87%) create mode 100644 powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 0d71104f3..46ee8d741 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -51,6 +51,7 @@ import org.slf4j.LoggerFactory; import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; +import software.amazon.lambda.powertools.validation.ValidationException; /** * Aspect for {@link Validation} annotation @@ -73,6 +74,11 @@ public Object around(ProceedingJoinPoint pjp, if (validation.schemaVersion() != V201909) { ValidationConfig.get().setSchemaVersion(validation.schemaVersion()); } + + // we need this result object to be null at this point as validation of API events, if + // it fails, will catch the ValidationException and generate a 400 API response. This response + // will be stored in the result object to prevent executing the lambda + Object result = null; if (placedOnRequestHandler(pjp)) { validationNeeded = true; @@ -85,10 +91,10 @@ public Object around(ProceedingJoinPoint pjp, validate(obj, inboundJsonSchema, validation.envelope()); } else if (obj instanceof APIGatewayProxyRequestEvent) { APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; - validate(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema); } else if (obj instanceof APIGatewayV2HTTPEvent) { APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; - validate(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema); } else if (obj instanceof SNSEvent) { SNSEvent event = (SNSEvent) obj; event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema)); @@ -140,33 +146,88 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); } } - Object result = pjp.proceed(proceedArgs); - - if (validationNeeded && !validation.outboundSchema().isEmpty()) { - JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); - - if (result instanceof APIGatewayProxyResponseEvent) { - APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof APIGatewayV2HTTPResponse) { - APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof APIGatewayV2WebSocketResponse) { - APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof ApplicationLoadBalancerResponseEvent) { - ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; - validate(response.getBody(), outboundJsonSchema); - } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { - KinesisAnalyticsInputPreprocessingResponse response = - (KinesisAnalyticsInputPreprocessingResponse) result; - response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); - } else { - LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", - result.getClass().getName()); - } + // don't execute the lambda if result was set by previous validation step + // in that case result should already hold a response with validation information + if (result != null) { + LOG.error("Incoming API event's body failed inbound schema validation."); } + else { + result = pjp.proceed(proceedArgs); + + if (validationNeeded && !validation.outboundSchema().isEmpty()) { + JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); + + Object overridenResponse = null; + if (result instanceof APIGatewayProxyResponseEvent) { + APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; + overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema); + } else if (result instanceof APIGatewayV2HTTPResponse) { + APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; + overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema); + } else if (result instanceof APIGatewayV2WebSocketResponse) { + APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof ApplicationLoadBalancerResponseEvent) { + ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result; + validate(response.getBody(), outboundJsonSchema); + } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) { + KinesisAnalyticsInputPreprocessingResponse response = + (KinesisAnalyticsInputPreprocessingResponse) result; + response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema)); + } else { + LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate", + result.getClass().getName()); + } + + if (overridenResponse != null) { + result = overridenResponse; + LOG.error("API response failed outbound schema validation."); + } + } + } return result; } + + /** + * Validates the given body against the provided JsonSchema. If validation fails the ValidationException + * will be catched and transformed to a 400, bad request, API response + * @param body body of the event to validate + * @param inboundJsonSchema validation schema + * @return null if validation passed, or a 400 response object otherwise + */ + private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema) { + APIGatewayProxyResponseEvent result = null; + try { + validate(body, jsonSchema); + } catch (ValidationException e) { + LOG.error("There were validation errors: {}", e.getMessage()); + result = new APIGatewayProxyResponseEvent(); + result.setBody(e.getMessage()); + result.setStatusCode(400); + result.setIsBase64Encoded(false); + } + return result; + } + + /** + * Validates the given body against the provided JsonSchema. If validation fails the ValidationException + * will be catched and transformed to a 400, bad request, API response + * @param body body of the event to validate + * @param inboundJsonSchema validation schema + * @return null if validation passed, or a 400 response object otherwise + */ + private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema) { + APIGatewayV2HTTPResponse result = null; + try { + validate(body, jsonSchema); + } catch (ValidationException e) { + LOG.error("There were validation errors: {}", e.getMessage()); + result = new APIGatewayV2HTTPResponse(); + result.setBody(e.getMessage()); + result.setStatusCode(400); + result.setIsBase64Encoded(false); + } + return result; + } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java new file mode 100644 index 000000000..74e8605a5 --- /dev/null +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.validation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +import software.amazon.lambda.powertools.validation.Validation; + +public class GenericSchemaV7APIGatewayProxyRequestEventHandler implements RequestHandler { + + @Validation(inboundSchema = "classpath:/schema_v7.json") + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); + response.setBody("valid-test"); + response.setStatusCode(200); + return response; + } +} \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java similarity index 92% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java index 5b8343d1b..ab0645f29 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java @@ -16,13 +16,14 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; + import software.amazon.lambda.powertools.validation.Validation; -public class GenericSchemaV7Handler implements RequestHandler { +public class GenericSchemaV7StringHandler implements RequestHandler { @Validation(inboundSchema = "classpath:/schema_v7.json") @Override public String handleRequest(T input, Context context) { return "OK"; } -} +} \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java similarity index 87% rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java index fd5692884..b8c67b1eb 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java @@ -17,10 +17,12 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; + import software.amazon.lambda.powertools.validation.Validation; -public class ValidationInboundStringHandler implements RequestHandler { +public class ValidationInboundAPIGatewayV2HTTPEventHandler implements RequestHandler { private static final String schema = "{\n" + " \"$schema\": \"http://json-schema.org/draft-07/schema\",\n" + @@ -80,7 +82,10 @@ public class ValidationInboundStringHandler implements RequestHandler provideArguments(ExtensionContext context) { + + String body = "{id"; + + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); + + APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGWV2HTTPResponse.setBody(body); + + APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); + apiGWV2WebSocketResponse.setBody(body); + + ApplicationLoadBalancerResponseEvent albResponseEvent = new ApplicationLoadBalancerResponseEvent(); + albResponseEvent.setBody(body); + + return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); + } +} diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java index b634d6f8c..74803a05a 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java @@ -35,11 +35,6 @@ public Stream provideArguments(ExtensionContext context) { String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); - - APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); - apiGWV2HTTPResponse.setBody(body); - APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); apiGWV2WebSocketResponse.setBody(body); @@ -53,7 +48,7 @@ public Stream provideArguments(ExtensionContext context) { KinesisAnalyticsInputPreprocessingResponse.Result.Ok, buffer)); kaipResponse.setRecords(records); - return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse, apiGWV2WebSocketResponse, albResponseEvent, + return Stream.of(apiGWV2WebSocketResponse, albResponseEvent, kaipResponse).map(Arguments::of); } } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index c8d5e7ade..858d2e933 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -17,11 +17,28 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; +import java.io.IOException; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.Signature; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; @@ -40,25 +57,15 @@ import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import com.networknt.schema.SpecVersion; -import java.io.IOException; -import java.util.stream.Stream; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.Signature; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; + import software.amazon.lambda.powertools.validation.Validation; import software.amazon.lambda.powertools.validation.ValidationConfig; import software.amazon.lambda.powertools.validation.ValidationException; -import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7Handler; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7APIGatewayProxyRequestEventHandler; +import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7StringHandler; import software.amazon.lambda.powertools.validation.handlers.SQSWithCustomEnvelopeHandler; import software.amazon.lambda.powertools.validation.handlers.SQSWithWrongEnvelopeHandler; -import software.amazon.lambda.powertools.validation.handlers.ValidationInboundStringHandler; +import software.amazon.lambda.powertools.validation.handlers.ValidationInboundAPIGatewayV2HTTPEventHandler; import software.amazon.lambda.powertools.validation.model.MyCustomEvent; @@ -99,7 +106,7 @@ void setUp() { @ParameterizedTest @ArgumentsSource(ResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchema(Object object) throws Throwable { + public void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -114,6 +121,37 @@ public void testValidateOutboundJsonSchema(Object object) throws Throwable { validationAspect.around(pjp, validation); }); } + + @ParameterizedTest + @ArgumentsSource(HandledResponseEventsArgumentsProvider.class) + public void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { + when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); + when(pjp.getSignature()).thenReturn(signature); + when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); + Object[] args = {new Object(), context}; + when(pjp.getArgs()).thenReturn(args); + when(pjp.proceed(args)).thenReturn(object); + when(validation.inboundSchema()).thenReturn(""); + when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json"); + + Object response = validationAspect.around(pjp, validation); + assertThat(response).isInstanceOfAny(APIGatewayProxyResponseEvent.class, APIGatewayV2HTTPResponse.class); + if (response instanceof APIGatewayProxyResponseEvent) { + assertThat(response).isInstanceOfSatisfying(APIGatewayProxyResponseEvent.class, t -> { + assertThat(t.getStatusCode()).isEqualTo(400); + assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getIsBase64Encoded()).isFalse(); + }); + } else if (response instanceof APIGatewayV2HTTPResponse) { + assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { + assertThat(t.getStatusCode()).isEqualTo(400); + assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getIsBase64Encoded()).isFalse(); + }); + } else { + fail(); + } + } @Test public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { @@ -137,19 +175,22 @@ public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable { @Test public void validate_inputOK_schemaInClasspath_shouldValidate() { - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": 42" + "}"); - assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } @Test public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); event.setBody("{" + " \"id\": 1," + @@ -157,30 +198,39 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() " \"price\": -2" + "}"); // price is negative - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getStatusCode()).isEqualTo(400); } @Test public void validate_inputOK_schemaInString_shouldValidate() { - ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": 42" + "}"); - assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } + @Test public void validate_inputKO_schemaInString_shouldThrowValidationException() { - ValidationInboundStringHandler handler = new ValidationInboundStringHandler(); + ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"" + "}"); - assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context)); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getStatusCode()).isEqualTo(400); } @Test @@ -189,7 +239,7 @@ public void validate_SQS() { LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader()); SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs.json")); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } @@ -219,7 +269,7 @@ public void validate_Kinesis() { LambdaEventSerializers.serializerFor(KinesisEvent.class, ClassLoader.getSystemClassLoader()); KinesisEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/kinesis.json")); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } @@ -229,7 +279,7 @@ public void validateEEvent(String jsonResource, Class eventClass) throws IOExcep Object event = ValidationConfig.get().getObjectMapper() .readValue(this.getClass().getResourceAsStream(jsonResource), eventClass); - GenericSchemaV7Handler handler = new GenericSchemaV7Handler(); + GenericSchemaV7StringHandler handler = new GenericSchemaV7StringHandler<>(); assertThat(handler.handleRequest(event, context)).isEqualTo("OK"); } } From d72ee64fbf8f24130f0564d074b706b7fab4fd8c Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Tue, 24 Oct 2023 14:42:02 +0200 Subject: [PATCH 11/19] Rebased upstream --- .../validation/internal/ValidationAspectTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 858d2e933..431d8718b 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -106,7 +106,7 @@ void setUp() { @ParameterizedTest @ArgumentsSource(ResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { + void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -124,7 +124,7 @@ public void testValidateOutboundJsonSchemaWithExceptions(Object object) throws T @ParameterizedTest @ArgumentsSource(HandledResponseEventsArgumentsProvider.class) - public void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { + void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable { when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7); when(pjp.getSignature()).thenReturn(signature); when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class); @@ -139,13 +139,13 @@ public void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) t if (response instanceof APIGatewayProxyResponseEvent) { assertThat(response).isInstanceOfSatisfying(APIGatewayProxyResponseEvent.class, t -> { assertThat(t.getStatusCode()).isEqualTo(400); - assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); }); } else if (response instanceof APIGatewayV2HTTPResponse) { assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { assertThat(t.getStatusCode()).isEqualTo(400); - assertThat(t.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); }); } else { @@ -199,7 +199,7 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() "}"); // price is negative APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); - assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); } @@ -229,7 +229,7 @@ public void validate_inputKO_schemaInString_shouldThrowValidationException() { "}"); APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); - assertThat(response.getBody()).satisfies(x -> StringUtils.isNotBlank(x)); + assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); } From 4ef6abf33e4d1b4b7902d3b8243c33a16d4c4fe9 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Thu, 26 Oct 2023 14:16:12 +0200 Subject: [PATCH 12/19] 1298 introduced E2E tests for validation --- powertools-e2e-tests/handlers/pom.xml | 6 ++ .../handlers/validation/pom.xml | 60 +++++++++++ .../lambda/powertools/e2e/Function.java | 33 +++++++ .../validation/src/main/resources/log4j2.xml | 16 +++ .../resources/validation/inbound_schema.json | 32 ++++++ .../resources/validation/outbound_schema.json | 32 ++++++ powertools-e2e-tests/pom.xml | 7 ++ .../lambda/powertools/ValidationE2ET.java | 99 +++++++++++++++++++ .../validation/invalid_api_gw_in_event.json | 62 ++++++++++++ .../validation/invalid_api_gw_out_event.json | 62 ++++++++++++ .../validation/valid_api_gw_in_out_event.json | 62 ++++++++++++ 11 files changed, 471 insertions(+) create mode 100644 powertools-e2e-tests/handlers/validation/pom.xml create mode 100644 powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java create mode 100644 powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json create mode 100644 powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index 1bda3caa5..7c1208470 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -32,6 +32,7 @@ metrics idempotency parameters + validation @@ -83,6 +84,11 @@ powertools-batch ${lambda.powertools.version} + + software.amazon.lambda + powertools-validation + ${lambda.powertools.version} + com.amazonaws aws-lambda-java-core diff --git a/powertools-e2e-tests/handlers/validation/pom.xml b/powertools-e2e-tests/handlers/validation/pom.xml new file mode 100644 index 000000000..2a7d1baf9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 1.0.0 + + + e2e-test-handler-validation + jar + A Lambda function using Powertools for AWS Lambda (Java) validation + + + + software.amazon.lambda + powertools-validation + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-validation + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..5ac8951d8 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +import software.amazon.lambda.powertools.validation.Validation; + +public class Function implements RequestHandler { + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) { + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json new file mode 100644 index 000000000..3665879eb --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json new file mode 100644 index 000000000..b1f14d025 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMaximum": 1000, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 1f5bd6347..7e817496f 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -172,6 +172,13 @@ powertools-serialization test + + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + test + diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java new file mode 100644 index 000000000..2842ae592 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +class ValidationE2ET { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationE2ET.class.getSimpleName()) + .pathToFunction("validation").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @ParameterizedTest + @Event(value = "/validation/valid_api_gw_in_out_event.json", type = APIGatewayProxyRequestEvent.class) + void test_validInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @ParameterizedTest + @Event(value = "/validation/invalid_api_gw_in_event.json", type = APIGatewayProxyRequestEvent.class) + void test_invalidInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + + // THEN + // invocation should fail inbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains("$.price: is missing but it is required"); + } + + @ParameterizedTest + @Event(value = "/validation/invalid_api_gw_out_event.json", type = APIGatewayProxyRequestEvent.class) + void test_invalidOutboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains("$.price: must have an exclusive maximum value of 1000"); + } +} diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json new file mode 100644 index 000000000..014ef9f05 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"Lambda rocks\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json new file mode 100644 index 000000000..b7ef1780c --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"price\": 50000}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json new file mode 100644 index 000000000..8cb8ea27a --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"price\": 150}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file From 200cbd1bee1a2973079a4c6cde4a8d86aaef8a1f Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Thu, 26 Oct 2023 15:05:08 +0200 Subject: [PATCH 13/19] #1298 updated documentation to match new behavior of @Validation --- docs/utilities/validation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index 928ffb6c8..be8a3ac8e 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -156,7 +156,9 @@ We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson librar `@Validation` annotation is used to validate either inbound events or functions' response. -It will fail fast with `ValidationException` if an event or response doesn't conform with given JSON Schema. +It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. But for +specific `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent`, instead, the `@Validation` annotation will build and return a custom +400 (BAD_REQUEST) response. Its body will contain the validation error(s) message(s). While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema. From 22e069a3ed8903d47dc586668221686a701c9e25 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Fri, 27 Oct 2023 14:32:10 +0200 Subject: [PATCH 14/19] #1298 implemented suggested changes --- docs/utilities/validation.md | 7 +-- powertools-e2e-tests/pom.xml | 7 --- .../lambda/powertools/ValidationE2ET.java | 44 +++++++++++-------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index be8a3ac8e..dfd97e0d4 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -156,9 +156,10 @@ We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson librar `@Validation` annotation is used to validate either inbound events or functions' response. -It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. But for -specific `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent`, instead, the `@Validation` annotation will build and return a custom -400 (BAD_REQUEST) response. Its body will contain the validation error(s) message(s). +It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown. +For API gateway events associated with REST APIs and HTTP APIs - `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent` - the `@Validation` +annotation will build and return a custom 400 / "Bad Request" response, with a body containing the validation errors. This saves you from having +to catch the validation exception and map it back to a meaningful user error yourself. While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema. diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 7e817496f..1f5bd6347 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -172,13 +172,6 @@ powertools-serialization test - - - com.amazonaws - aws-lambda-java-tests - 1.1.1 - test - diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java index 2842ae592..bd2c42a82 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java @@ -19,16 +19,17 @@ import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.tests.annotations.Event; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -58,11 +59,13 @@ public static void tearDown() { } } - @ParameterizedTest - @Event(value = "/validation/valid_api_gw_in_out_event.json", type = APIGatewayProxyRequestEvent.class) - void test_validInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + @Test + void test_validInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + // WHEN - InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + InvocationResult invocationResult = invokeFunction(functionName, validEvent); // THEN // invocation should pass validation and return 200 @@ -70,12 +73,14 @@ void test_validInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); } - - @ParameterizedTest - @Event(value = "/validation/invalid_api_gw_in_event.json", type = APIGatewayProxyRequestEvent.class) - void test_invalidInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + + @Test + void test_invalidInboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + // WHEN - InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); // THEN // invocation should fail inbound validation and return 400 @@ -83,17 +88,20 @@ void test_invalidInboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throw assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); assertThat(validJsonNode.get("body").asText()).contains("$.price: is missing but it is required"); } - - @ParameterizedTest - @Event(value = "/validation/invalid_api_gw_out_event.json", type = APIGatewayProxyRequestEvent.class) - void test_invalidOutboundApiGWEvent(APIGatewayProxyRequestEvent validEvent) throws IOException { + + @Test + void test_invalidOutboundApiGWEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + // WHEN - InvocationResult invocationResult = invokeFunction(functionName, objectMapper.writeValueAsString(validEvent)); + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); // THEN // invocation should fail outbound validation and return 400 JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()).contains("$.price: must have an exclusive maximum value of 1000"); + assertThat(validJsonNode.get("body").asText()) + .contains("$.price: must have an exclusive maximum value of 1000"); } } From a7d02c4eeeeade3b53617dd296229e736db09ee1 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Mon, 30 Oct 2023 15:41:13 +0100 Subject: [PATCH 15/19] #1298 added new E2E tests for ALB event, which should trigger a validation exception --- .../pom.xml | 2 +- .../lambda/powertools/e2e/Function.java | 40 +++++++ .../src/main/resources/log4j2.xml | 0 .../resources/validation/inbound_schema.json | 0 .../resources/validation/outbound_schema.json | 0 .../handlers/validation-apigw-event/pom.xml | 60 ++++++++++ .../lambda/powertools/e2e/Function.java | 0 .../src/main/resources/log4j2.xml | 16 +++ .../resources/validation/inbound_schema.json | 32 ++++++ .../resources/validation/outbound_schema.json | 32 ++++++ .../lambda/powertools/ValidationALBE2ET.java | 104 ++++++++++++++++++ ...tionE2ET.java => ValidationApiGWE2ET.java} | 6 +- .../validation/invalid_alb_in_event.json | 28 +++++ .../validation/invalid_alb_out_event.json | 28 +++++ .../validation/valid_alb_in_out_event.json | 28 +++++ 15 files changed, 372 insertions(+), 4 deletions(-) rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/pom.xml (97%) create mode 100644 powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/src/main/resources/log4j2.xml (100%) rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/src/main/resources/validation/inbound_schema.json (100%) rename powertools-e2e-tests/handlers/{validation => validation-alb-event}/src/main/resources/validation/outbound_schema.json (100%) create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/pom.xml rename powertools-e2e-tests/handlers/{validation => validation-apigw-event}/src/main/java/software/amazon/lambda/powertools/e2e/Function.java (100%) create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json create mode 100644 powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json create mode 100644 powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java rename powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/{ValidationE2ET.java => ValidationApiGWE2ET.java} (95%) create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json create mode 100644 powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json diff --git a/powertools-e2e-tests/handlers/validation/pom.xml b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml similarity index 97% rename from powertools-e2e-tests/handlers/validation/pom.xml rename to powertools-e2e-tests/handlers/validation-alb-event/pom.xml index 2a7d1baf9..31570fe4e 100644 --- a/powertools-e2e-tests/handlers/validation/pom.xml +++ b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml @@ -8,7 +8,7 @@ 1.0.0 - e2e-test-handler-validation + e2e-test-handler-validation-alb-event jar A Lambda function using Powertools for AWS Lambda (Java) validation diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..70a9bb3f3 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; +import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; + +import software.amazon.lambda.powertools.validation.Validation; +public class Function implements RequestHandler { +// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") +// public String handleRequest(SQSEvent input, Context context) { +// return "OK"; +// } + + + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") +// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json") + public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, Context context) { + ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/resources/log4j2.xml rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/resources/validation/inbound_schema.json rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json diff --git a/powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/resources/validation/outbound_schema.json rename to powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml new file mode 100644 index 000000000..9129abc7d --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 1.0.0 + + + e2e-test-handler-validation-apigw-event + jar + A Lambda function using Powertools for AWS Lambda (Java) validation + + + + software.amazon.lambda + powertools-validation + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-validation + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java similarity index 100% rename from powertools-e2e-tests/handlers/validation/src/main/java/software/amazon/lambda/powertools/e2e/Function.java rename to powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json new file mode 100644 index 000000000..3665879eb --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMinimum": 0, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json new file mode 100644 index 000000000..b1f14d025 --- /dev/null +++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/product.json", + "type": "object", + "title": "Product schema", + "description": "JSON schema to validate Products", + "default": {}, + "examples": [ + { + "id": 43242, + "name": "FooBar XY", + "price": 258 + } + ], + "required": [ + "price" + ], + "properties": { + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "Price of the product", + "description": "Positive price of the product", + "default": 0, + "exclusiveMaximum": 1000, + "examples": [ + 258.99 + ] + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java new file mode 100644 index 000000000..324c77a34 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +class ValidationALBE2ET { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) + .pathToFunction("validation-alb-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); + String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + + @Test + void test_invalidInboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return an error message + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains("$.price: is missing but it is required"); + } + + @Test + void test_invalidOutboundSQSEvent() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); + String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains("$.price: must have an exclusive maximum value of 1000"); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java similarity index 95% rename from powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java rename to powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java index bd2c42a82..af7c7d87c 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java @@ -36,7 +36,7 @@ import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -class ValidationE2ET { +class ValidationApiGWE2ET { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -46,8 +46,8 @@ class ValidationE2ET { @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) public static void setup() { - infrastructure = Infrastructure.builder().testName(ValidationE2ET.class.getSimpleName()) - .pathToFunction("validation").build(); + infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) + .pathToFunction("validation-apigw-event").build(); Map outputs = infrastructure.deploy(); functionName = outputs.get(FUNCTION_NAME_OUTPUT); } diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json new file mode 100644 index 000000000..ebad834d8 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"message\": \"Lambda rocks\"}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json new file mode 100644 index 000000000..1b1961063 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"price\": 50000}", + "isBase64Encoded": true +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json new file mode 100644 index 000000000..35560f109 --- /dev/null +++ b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json @@ -0,0 +1,28 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "POST", + "path": "/path/to/resource", + "queryStringParameters": { + "query": "1234ABCD" + }, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + "x-imforwards": "20" + }, + "body": "{\"price\": 150}", + "isBase64Encoded": true +} \ No newline at end of file From dd13481b3797969aed902a0ef4832f34a6f2ae97 Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Mon, 30 Oct 2023 16:37:20 +0100 Subject: [PATCH 16/19] #1298 original headers are returned as part of a 400 validation failed response --- .../validation/internal/ValidationAspect.java | 28 +++++++++++++++---- ...andledResponseEventsArgumentsProvider.java | 9 +++++- .../internal/ValidationAspectTest.java | 17 ++++++++++- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java index 46ee8d741..978be16de 100644 --- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java +++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java @@ -22,6 +22,10 @@ import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema; import static software.amazon.lambda.powertools.validation.ValidationUtils.validate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; @@ -91,10 +95,10 @@ public Object around(ProceedingJoinPoint pjp, validate(obj, inboundJsonSchema, validation.envelope()); } else if (obj instanceof APIGatewayProxyRequestEvent) { APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj; - result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema, null, null); } else if (obj instanceof APIGatewayV2HTTPEvent) { APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj; - result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema); + result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema, null, null); } else if (obj instanceof SNSEvent) { SNSEvent event = (SNSEvent) obj; event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema)); @@ -158,12 +162,18 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true); Object overridenResponse = null; + // The normal behavior of @Validation is to throw an exception if response's validation fails. + // but in the case of APIGatewayProxyResponseEvent and APIGatewayV2HTTPResponse we want to return + // a 400 response with the validation errors instead of throwing an exception. if (result instanceof APIGatewayProxyResponseEvent) { APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result; - overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema); + overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema, response.getHeaders(), + response.getMultiValueHeaders()); } else if (result instanceof APIGatewayV2HTTPResponse) { APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result; - overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema); + overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema, response.getHeaders(), + response.getMultiValueHeaders()); + // all type of below responses will throw an exception if validation fails } else if (result instanceof APIGatewayV2WebSocketResponse) { APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result; validate(response.getBody(), outboundJsonSchema); @@ -196,7 +206,8 @@ record -> validate(decode(record.getData()), inboundJsonSchema))); * @param inboundJsonSchema validation schema * @return null if validation passed, or a 400 response object otherwise */ - private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema) { + private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema, + final Map headers, Map> multivalueHeaders) { APIGatewayProxyResponseEvent result = null; try { validate(body, jsonSchema); @@ -204,6 +215,8 @@ private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String bo LOG.error("There were validation errors: {}", e.getMessage()); result = new APIGatewayProxyResponseEvent(); result.setBody(e.getMessage()); + result.setHeaders(headers == null ? Collections.emptyMap() : headers); + result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders); result.setStatusCode(400); result.setIsBase64Encoded(false); } @@ -217,7 +230,8 @@ private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String bo * @param inboundJsonSchema validation schema * @return null if validation passed, or a 400 response object otherwise */ - private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema) { + private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema, + final Map headers, Map> multivalueHeaders) { APIGatewayV2HTTPResponse result = null; try { validate(body, jsonSchema); @@ -225,6 +239,8 @@ private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, LOG.error("There were validation errors: {}", e.getMessage()); result = new APIGatewayV2HTTPResponse(); result.setBody(e.getMessage()); + result.setHeaders(headers == null ? Collections.emptyMap() : headers); + result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders); result.setStatusCode(400); result.setIsBase64Encoded(false); } diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java index 1c48fc0ad..c675c8479 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java @@ -14,6 +14,8 @@ package software.amazon.lambda.powertools.validation.internal; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; @@ -32,10 +34,15 @@ public Stream provideArguments(ExtensionContext context) { String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body); + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() + .withBody(body) + .withHeaders(Map.of("header1", "value1,value2,value3")) + .withMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); apiGWV2HTTPResponse.setBody(body); + apiGWV2HTTPResponse.setHeaders(Map.of("header1", "value1,value2,value3")); + apiGWV2HTTPResponse.setMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); apiGWV2WebSocketResponse.setBody(body); diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 431d8718b..d05453f12 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -21,9 +21,10 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.junit.jupiter.api.BeforeEach; @@ -141,12 +142,16 @@ void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws T assertThat(t.getStatusCode()).isEqualTo(400); assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); + assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3"); + assertThat(t.getMultiValueHeaders()).containsEntry("header1", List.of("value1", "value2", "value3")); }); } else if (response instanceof APIGatewayV2HTTPResponse) { assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> { assertThat(t.getStatusCode()).isEqualTo(400); assertThat(t.getBody()).isNotBlank(); assertThat(t.getIsBase64Encoded()).isFalse(); + assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3"); + assertThat(t.getMultiValueHeaders()).containsEntry("header1", List.of("value1", "value2", "value3")); }); } else { fail(); @@ -183,9 +188,11 @@ public void validate_inputOK_schemaInClasspath_shouldValidate() { " \"price\": 42" + "}"); + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); assertThat(response.getBody()).isEqualTo("valid-test"); assertThat(response.getStatusCode()).isEqualTo(200); + } @Test @@ -197,10 +204,15 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() " \"name\": \"Lampshade\"," + " \"price\": -2" + "}"); + event.setHeaders(Map.of("header1", "value1")); + event.setMultiValueHeaders(Map.of("header1", List.of("value1"))); + // price is negative APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test @@ -227,10 +239,13 @@ public void validate_inputKO_schemaInString_shouldThrowValidationException() { " \"id\": 1," + " \"name\": \"Lampshade\"" + "}"); + event.setHeaders(Map.of("header1", "value1")); APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); assertThat(response.getBody()).isNotBlank(); assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test From a414353fd1e0f7e140f94df11dfbe545a115432d Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Fri, 24 Nov 2023 09:48:35 +0100 Subject: [PATCH 17/19] #1298 javadoc --- .../lambda/powertools/e2e/Function.java | 28 ++++++------- ...andledResponseEventsArgumentsProvider.java | 40 +++++++++---------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 70a9bb3f3..d221ee153 100644 --- a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -21,20 +21,16 @@ import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import software.amazon.lambda.powertools.validation.Validation; -public class Function implements RequestHandler { -// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") -// public String handleRequest(SQSEvent input, Context context) { -// return "OK"; -// } - - - @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") -// @Validation(inboundSchema = "classpath:/validation/inbound_schema.json") - public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, Context context) { - ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); - response.setBody(input.getBody()); - response.setStatusCode(200); - response.setIsBase64Encoded(false); - return response; - } + +public class Function + implements RequestHandler { + @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json") + public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input, + Context context) { + ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); + response.setBody(input.getBody()); + response.setStatusCode(200); + response.setIsBase64Encoded(false); + return response; + } } \ No newline at end of file diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java index c675c8479..3a3120f0d 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/HandledResponseEventsArgumentsProvider.java @@ -24,32 +24,30 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; -import com.amazonaws.services.lambda.runtime.events.APIGatewayV2WebSocketResponse; -import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; +/** + * Provides test arguments that are used in unit tests. + * It creates API Gateway response arguments that can be used to confirm + * that @Validation validates responses and returns a response's headers even + * when validation fails + */ public class HandledResponseEventsArgumentsProvider implements ArgumentsProvider { - - @Override - public Stream provideArguments(ExtensionContext context) { - - String body = "{id"; - final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() - .withBody(body) - .withHeaders(Map.of("header1", "value1,value2,value3")) - .withMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); + @Override + public Stream provideArguments(ExtensionContext context) { - APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); - apiGWV2HTTPResponse.setBody(body); - apiGWV2HTTPResponse.setHeaders(Map.of("header1", "value1,value2,value3")); - apiGWV2HTTPResponse.setMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); + String body = "{id"; - APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse(); - apiGWV2WebSocketResponse.setBody(body); + final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent() + .withBody(body) + .withHeaders(Map.of("header1", "value1,value2,value3")) + .withMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); - ApplicationLoadBalancerResponseEvent albResponseEvent = new ApplicationLoadBalancerResponseEvent(); - albResponseEvent.setBody(body); + APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse(); + apiGWV2HTTPResponse.setBody(body); + apiGWV2HTTPResponse.setHeaders(Map.of("header1", "value1,value2,value3")); + apiGWV2HTTPResponse.setMultiValueHeaders(Map.of("header1", List.of("value1", "value2", "value3"))); - return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); - } + return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of); + } } From 36318b26f3ed091ca9a25ef6d56705aff369382e Mon Sep 17 00:00:00 2001 From: Pascal Romanens Date: Fri, 1 Dec 2023 10:58:09 +0100 Subject: [PATCH 18/19] fixed java8 compatibility --- .../internal/ValidationAspectTest.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index e45a62e7a..3888177f4 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -234,28 +234,32 @@ public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() @Test public void validate_inputOK_schemaInString_shouldValidate() { ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); - APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); - event.setBody("{" + - " \"id\": 1," + - " \"name\": \"Lampshade\"," + - " \"price\": 42" + - "}"); - - APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); - assertThat(response.getBody()).isEqualTo("valid-test"); - assertThat(response.getStatusCode()).isEqualTo(200); + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); + event.setBody("{" + + " \"id\": 1," + + " \"name\": \"Lampshade\"," + + " \"price\": 42" + + "}"); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } @Test public void validate_inputKO_schemaInString_shouldThrowValidationException() { ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); + + Map headers = new HashMap<>(); + headers.put("header1", "value1"); + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"" + "}"); - event.setHeaders(Map.of("header1", "value1")); + event.setHeaders(headers); APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); assertThat(response.getBody()).isNotBlank(); From 111d79ae3db9f792fa57c076e1fd5f71fd5567cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:42:34 +0100 Subject: [PATCH 19/19] indentation --- .../internal/ValidationAspectTest.java | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java index 3888177f4..1708ebeeb 100644 --- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java +++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java @@ -207,43 +207,43 @@ public void validate_inputOK_schemaInClasspath_shouldValidate() { public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() { GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler(); - Map headers = new HashMap<>(); - headers.put("header1", "value1"); - Map> headersList = new HashMap<>(); - List headerValues = new ArrayList<>(); - headerValues.add("value1"); - headersList.put("header1", headerValues); - - APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); - event.setBody("{" + + Map headers = new HashMap<>(); + headers.put("header1", "value1"); + Map> headersList = new HashMap<>(); + List headerValues = new ArrayList<>(); + headerValues.add("value1"); + headersList.put("header1", headerValues); + + APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent(); + event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": -2" + "}"); - event.setHeaders(headers); - event.setMultiValueHeaders(headersList); - - // price is negative - APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); - assertThat(response.getBody()).isNotBlank(); - assertThat(response.getStatusCode()).isEqualTo(400); - assertThat(response.getHeaders()).isEmpty(); - assertThat(response.getMultiValueHeaders()).isEmpty(); + event.setHeaders(headers); + event.setMultiValueHeaders(headersList); + + // price is negative + APIGatewayProxyResponseEvent response = handler.handleRequest(event, context); + assertThat(response.getBody()).isNotBlank(); + assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test public void validate_inputOK_schemaInString_shouldValidate() { ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); - APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); - event.setBody("{" + + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); + event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"," + " \"price\": 42" + "}"); - APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); - assertThat(response.getBody()).isEqualTo("valid-test"); - assertThat(response.getStatusCode()).isEqualTo(200); + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isEqualTo("valid-test"); + assertThat(response.getStatusCode()).isEqualTo(200); } @@ -251,21 +251,21 @@ public void validate_inputOK_schemaInString_shouldValidate() { public void validate_inputKO_schemaInString_shouldThrowValidationException() { ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler(); - Map headers = new HashMap<>(); - headers.put("header1", "value1"); + Map headers = new HashMap<>(); + headers.put("header1", "value1"); - APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); - event.setBody("{" + + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); + event.setBody("{" + " \"id\": 1," + " \"name\": \"Lampshade\"" + "}"); - event.setHeaders(headers); - - APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); - assertThat(response.getBody()).isNotBlank(); - assertThat(response.getStatusCode()).isEqualTo(400); - assertThat(response.getHeaders()).isEmpty(); - assertThat(response.getMultiValueHeaders()).isEmpty(); + event.setHeaders(headers); + + APIGatewayV2HTTPResponse response = handler.handleRequest(event, context); + assertThat(response.getBody()).isNotBlank(); + assertThat(response.getStatusCode()).isEqualTo(400); + assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getMultiValueHeaders()).isEmpty(); } @Test