Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 4 additions & 1 deletion docs/utilities/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +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 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.
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.

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 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();
}
}

@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, 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_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, invalidEvent);

// 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");
}

@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, 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");
}
}
Loading