Skip to content

feat(v2): Validation failures return 400s #1489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/utilities/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ public void shouldReturnOkStatusWhenInputIsValid() {
}

@Test
public void shouldThrowExceptionWhenRequestInInvalid() {
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());
}
}
6 changes: 6 additions & 0 deletions powertools-e2e-tests/handlers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<module>metrics</module>
<module>idempotency</module>
<module>parameters</module>
<module>validation</module>
</modules>

<dependencyManagement>
Expand Down Expand Up @@ -79,6 +80,11 @@
<artifactId>powertools-batch</artifactId>
<version>${lambda.powertools.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-validation</artifactId>
<version>${lambda.powertools.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
Expand Down
60 changes: 60 additions & 0 deletions powertools-e2e-tests/handlers/validation/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>software.amazon.lambda</groupId>
<artifactId>e2e-test-handlers-parent</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>e2e-test-handler-validation</artifactId>
<packaging>jar</packaging>
<name>A Lambda function using Powertools for AWS Lambda (Java) validation</name>

<dependencies>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-validation</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>dev.aspectj</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<complianceLevel>${maven.compiler.target}</complianceLevel>
<aspectLibraries>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-validation</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
@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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="JsonAppender" target="SYSTEM_OUT">
<JsonTemplateLayout eventTemplateUri="classpath:LambdaJsonLayout.json" />
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="JsonAppender"/>
</Root>
<Logger name="JsonLogger" level="INFO" additivity="false">
<AppenderRef ref="JsonAppender"/>
</Logger>
</Loggers>
</Configuration>
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions powertools-e2e-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@
<artifactId>powertools-serialization</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-tests</artifactId>
<version>1.1.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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");
}
}
Loading