From fbee4532ddea9414c4b01b93610bd397bed28dd3 Mon Sep 17 00:00:00 2001 From: Jose Carranza Date: Wed, 15 Nov 2023 19:08:06 +0100 Subject: [PATCH] implementing vertx-web-validation module tests --- README.md | 7 + http/vertx-web-validation/pom.xml | 27 ++++ .../ts/vertx/web/validation/ShopResource.java | 33 ++++ .../ts/vertx/web/validation/ShoppingList.java | 60 +++++++ .../validation/ValidationHandlerOnRoutes.java | 153 ++++++++++++++++++ .../OpenShiftVertxWebValidationIT.java | 7 + .../web/validation/VertxWebValidationIT.java | 124 ++++++++++++++ pom.xml | 1 + 8 files changed, 412 insertions(+) create mode 100644 http/vertx-web-validation/pom.xml create mode 100644 http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java create mode 100644 http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java create mode 100644 http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java create mode 100644 http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java create mode 100644 http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java diff --git a/README.md b/README.md index a78e1773e..f02978071 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,13 @@ Vert.x Mutiny webClient exploratory test. Also see http/vertx-web-client/README.md +### `http/vertx-web-validation` +Ensure that you can deploy a simple Quarkus application with predefined routes using schema parser from vertx-json-schema and Router Vert.x approach, + incorporating web validation configuration through ValidationHandlerBuilder. One of the goal of vertx-web-validation functionality is to validate parameters and bodies of incoming requests. + +It also verifies multiple deployment strategies like: +- Using Quarkus OpenShift extension + ### `http/graphql` This module covers some basic scenarios around GraphQL. diff --git a/http/vertx-web-validation/pom.xml b/http/vertx-web-validation/pom.xml new file mode 100644 index 000000000..a3859d72f --- /dev/null +++ b/http/vertx-web-validation/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../.. + + vertx-web-validation + jar + Quarkus QE TS: HTTP: Vert.x-Web-Validation + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.vertx + vertx-web-validation + + + io.vertx + vertx-json-schema + + + diff --git a/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java new file mode 100644 index 000000000..73ee3bca3 --- /dev/null +++ b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java @@ -0,0 +1,33 @@ +package io.quarkus.ts.vertx.web.validation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/shoppinglist") +@Produces(MediaType.APPLICATION_JSON) +public class ShopResource { + + private static List shoppingList = createSampleProductList(); + + private static List createSampleProductList() { + shoppingList = new ArrayList<>(); + shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName1", 25, + new ArrayList<>(Arrays.asList("Carrots", "Water", "Cheese", "Beer")))); + shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName2", 80, + new ArrayList<>(Arrays.asList("Meat", "Wine", "Almonds", "Potatoes", "Cake")))); + return shoppingList; + } + + @GET + public List get() { + return shoppingList; + } + +} diff --git a/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java new file mode 100644 index 000000000..45ca3d5d1 --- /dev/null +++ b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java @@ -0,0 +1,60 @@ +package io.quarkus.ts.vertx.web.validation; + +import java.util.ArrayList; +import java.util.UUID; + +public class ShoppingList { + + public UUID id; + + public String name; + + public ArrayList products; + + public double price; + + public ShoppingList(UUID id, String name, double price, ArrayList products) { + this.id = id; + this.name = name; + this.price = price; + this.products = products; + } + + public ArrayList getProducts() { + return products; + } + + public void setProducts(ArrayList products) { + this.products = products; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + @Override + public String toString() { + return String.format( + "Shopping list{id=%s, name=%s, products=%s, price=%s}", + getId(), + getName(), + getProducts().toString(), + getPrice()); + } +} diff --git a/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java new file mode 100644 index 000000000..784badab2 --- /dev/null +++ b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java @@ -0,0 +1,153 @@ +package io.quarkus.ts.vertx.web.validation; + +import static io.vertx.ext.web.validation.builder.Parameters.param; +import static io.vertx.json.schema.common.dsl.Schemas.arraySchema; +import static io.vertx.json.schema.common.dsl.Schemas.numberSchema; +import static io.vertx.json.schema.common.dsl.Schemas.objectSchema; +import static io.vertx.json.schema.common.dsl.Schemas.stringSchema; +import static io.vertx.json.schema.draft7.dsl.Keywords.maximum; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Vertx; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.validation.BadRequestException; +import io.vertx.ext.web.validation.BodyProcessorException; +import io.vertx.ext.web.validation.ParameterProcessorException; +import io.vertx.ext.web.validation.RequestParameters; +import io.vertx.ext.web.validation.RequestPredicate; +import io.vertx.ext.web.validation.RequestPredicateException; +import io.vertx.ext.web.validation.ValidationHandler; +import io.vertx.ext.web.validation.builder.Bodies; +import io.vertx.ext.web.validation.builder.Parameters; +import io.vertx.ext.web.validation.builder.ValidationHandlerBuilder; +import io.vertx.json.schema.SchemaParser; +import io.vertx.json.schema.SchemaRouter; +import io.vertx.json.schema.SchemaRouterOptions; +import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder; + +public class ValidationHandlerOnRoutes { + //TODO when Quarkus use vert.x version 4.4.6 we can use SchemaRepository instead of SchemaParser with SchemaRouter + //private SchemaRepository schemaRepository =SchemaRepository.create(new JsonSchemaOptions().setDraft(Draft.DRAFT7).setBaseUri(BASEURI)); + private SchemaParser schemaParser; + private SchemaRouter schemaRouter; + + @Inject + Vertx vertx; + + private static ShopResource shopResource = new ShopResource(); + + private static final String ERROR_MESSAGE = "{\"error\": \"%s\"}"; + private static final String SHOPPINGLIST_NOT_FOUND = "Shopping list not found in the list or does not exist with that name or price"; + + @PostConstruct + void initialize() { + schemaParser = createSchema(); + } + + private SchemaParser createSchema() { + schemaRouter = SchemaRouter.create(vertx, new SchemaRouterOptions()); + schemaParser = SchemaParser.createDraft7SchemaParser(schemaRouter); + return schemaParser; + } + + public void validateHandlerShoppingList(@Observes Router router) { + AtomicReference queryAnswer = new AtomicReference<>(); + router.get("/filterList") + .handler(ValidationHandlerBuilder + .create(schemaParser) + .queryParameter(param("shoppingListName", stringSchema())) + .queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100)))).build()) + .handler(routingContext -> { + RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY); + String shoppingListName = parameters.queryParameter("shoppingListName").getString(); + Double totalPrice = parameters.queryParameter("shoppingListPrice").getDouble(); + + // Logic to list shoppingList based on shoppingListName and totalPrice + String shoppingListFound = fetchProductDetailsFromQuery(shoppingListName, totalPrice); + queryAnswer.set(shoppingListFound); + + if (queryAnswer.get().equalsIgnoreCase(SHOPPINGLIST_NOT_FOUND)) { + routingContext.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()); + } + + routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN).end(queryAnswer.get()); + }).failureHandler(routingContext -> { + // Error handling: + if (routingContext.failure() instanceof BadRequestException || + routingContext.failure() instanceof ParameterProcessorException || + routingContext.failure() instanceof BodyProcessorException || + routingContext.failure() instanceof RequestPredicateException) { + + String errorMessage = routingContext.failure().toString(); + routingContext.response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .end(String.format(ERROR_MESSAGE, errorMessage)); + } else { + routingContext.next(); + } + + }); + // Create a ValidationHandlerBuilder with explodedParam and arraySchema to filter by array items + ObjectSchemaBuilder bodySchemaBuilder = objectSchema() + .property("shoppingListName", stringSchema()); + ValidationHandlerBuilder + .create(schemaParser) + .body(Bodies.json(bodySchemaBuilder)); + router.get("/filterByArrayItem") + .handler( + ValidationHandlerBuilder + .create(schemaParser) + .queryParameter(Parameters.explodedParam("shoppingArray", arraySchema().items(stringSchema()))) + .body(Bodies.json(bodySchemaBuilder)) + .build()) + .handler(routingContext -> { + RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY); + JsonArray myArray = parameters.queryParameter("shoppingArray").getJsonArray(); + // Retrieve the list of all shoppingLists + List shoppingLists = fetchProductDetailsFromArrayQuery(myArray); + + routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .end(Json.encodeToBuffer(shoppingLists)); + }); + // Let's allow to create a new item + router.post("/createShoppingList").handler( + ValidationHandlerBuilder + .create(schemaParser) + .predicate(RequestPredicate.BODY_REQUIRED) + .queryParameter(param("shoppingListName", stringSchema())) + .queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100)))) + .build()) + .handler(routingContext -> { + routingContext.response().setStatusCode(HttpResponseStatus.OK.code()).end("Shopping list created"); + }); + + } + + public List fetchProductDetailsFromArrayQuery(JsonArray myArray) { + return shopResource.get().stream() + .filter(shoppingList -> myArray.contains(shoppingList.getName())) + .collect(Collectors.toList()); + } + + public String fetchProductDetailsFromQuery(String name, Double price) { + return shopResource.get().stream() + .filter(product -> name.equalsIgnoreCase(product.getName()) && price.equals(product.getPrice())) + .map(ShoppingList::toString) + .findFirst() + .orElse(SHOPPINGLIST_NOT_FOUND); + } + +} diff --git a/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java new file mode 100644 index 000000000..49497491f --- /dev/null +++ b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.vertx.web.validation; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftVertxWebValidationIT extends VertxWebValidationIT { +} diff --git a/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java new file mode 100644 index 000000000..5117c6cd3 --- /dev/null +++ b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java @@ -0,0 +1,124 @@ +package io.quarkus.ts.vertx.web.validation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.http.ContentType; +import io.restassured.response.Response; + +@QuarkusScenario +public class VertxWebValidationIT { + @QuarkusApplication + static RestService restserviceapp = new RestService(); + + private static final String SHOPPING_LIST_URL = "/shoppinglist"; + private static final String LIST_NAME1 = "name=ListName1, products=[Carrots, Water, Cheese, Beer], price=25.0"; + private static final String LIST_NAME2 = "name=ListName2, products=[Meat, Wine, Almonds, Potatoes, Cake], price=80.0"; + private static final String FILTER_BY_NAME_PRICE_URL_1 = "/filterList?shoppingListName=ListName1&shoppingListPrice=25"; + private static final String FILTER_BY_NAME_PRICE_URL_2 = "/filterList?shoppingListName=ListName2&shoppingListPrice=80"; + private static final String FILTER_BY_WRONG_NAME_PRICE_URL = "/filterList?shoppingListName=ListName35&shoppingListPrice=25"; + private static final String ONLY_FILTER_BY_NAME = "/filterList?shoppingListName=ListName1"; + private static final String PRICE_OUT_OF_RANGE = "/filterList?shoppingListName=ListName1&shoppingListPrice=125"; + private static final String FILTER_BY_LIST_NAMES = "/filterByArrayItem?shoppingArray=ListName1&shoppingArray=ListName2"; + + private static final String ERROR_PARAMETER_MISSING = "ParameterProcessorException"; + + @Test + void checkShoppingListUrl() { + Response response = restserviceapp.given() + .get(SHOPPING_LIST_URL) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .response(); + + assertThat(response.getBody().jsonPath().getString("name"), containsString("[ListName1, ListName2]")); + } + + @Test + void checkNamePriceParams() { + Response response = restserviceapp + .given() + .get(FILTER_BY_NAME_PRICE_URL_1) + .then() + .statusCode(HttpStatus.SC_OK).extract().response(); + assertThat(response.asString(), containsString(LIST_NAME1)); + Response response2 = restserviceapp + .given() + .get(FILTER_BY_NAME_PRICE_URL_2) + .then() + .statusCode(HttpStatus.SC_OK).extract().response(); + assertThat(response2.asString(), containsString(LIST_NAME2)); + } + + @Test + void checkWrongNamePriceParams() { + Response response = restserviceapp + .given() + .get(FILTER_BY_WRONG_NAME_PRICE_URL) + .then() + .statusCode(HttpStatus.SC_NOT_FOUND).extract().response(); + assertThat(response.asString(), + containsString("Shopping list not found in the list or does not exist with that name or price")); + } + + @Test + void checkParameterMissingError() { + Response response = restserviceapp + .given() + .get(ONLY_FILTER_BY_NAME) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST) + .extract() + .response(); + assertThat(response.asString(), containsString(ERROR_PARAMETER_MISSING)); + assertThat(response.asString(), containsString("Missing parameter shoppingListPrice in QUERY")); + } + + @Test + void checkPriceOutOfRangeError() { + Response response = restserviceapp + .given() + .get(PRICE_OUT_OF_RANGE) + .then() + .statusCode(HttpStatus.SC_BAD_REQUEST) + .extract() + .response(); + assertThat(response.asString(), containsString(ERROR_PARAMETER_MISSING)); + assertThat(response.asString(), containsString("value should be <= 100.0")); + } + + @Test + void checkFilterByArrayListName() { + Response response = restserviceapp + .given() + .get(FILTER_BY_LIST_NAMES) + .then() + .statusCode(HttpStatus.SC_OK).extract().response(); + assertThat(response.getBody().jsonPath().getString("name"), containsString("[ListName1, ListName2]")); + assertThat(response.getBody().jsonPath().getString("price"), equalTo("[25.0, 80.0]")); + } + + @Test + void createShoppingList() { + restserviceapp.given() + .contentType(ContentType.JSON) + .body("{}") + .queryParam("shoppingListName", "MyList3") + .queryParam("shoppingListPrice", 50) + .when() + .post("/createShoppingList") + .then() + .statusCode(200) + .body(equalTo("Shopping list created")); + } + +} diff --git a/pom.xml b/pom.xml index a5593ea9c..1bfc16ac3 100644 --- a/pom.xml +++ b/pom.xml @@ -466,6 +466,7 @@ http/rest-client-reactive http/servlet-undertow http/vertx-web-client + http/vertx-web-validation http/hibernate-validator http/graphql http/graphql-telemetry