From 29664e6b7ecc3c24f41cc2a2a68f5363348081b0 Mon Sep 17 00:00:00 2001 From: Helis Freitas Date: Fri, 4 Oct 2024 21:43:45 +0000 Subject: [PATCH] feat: add Dishes Api with Test Integration --- .../registration/rest/DishResources.java | 92 ++++++++++++ .../registration/rest/ResourcePaths.java | 1 + .../registration/rest/dto/DishRequest.java | 91 ++++++++++++ .../registration/rest/dto/DishResponse.java | 135 ++++++++++++++++++ .../rest/dto/mappers/DishMapper.java | 48 +++++++ .../registration/rest/DishResourcesIT.java | 135 ++++++++++++++++++ ...uldCreateDishesFromRestaurant.approved.txt | 1 + ...dFindAllDishesFromRestaurant.approved.json | 18 +++ ...FindByIdDishesFromRestaurant.approved.json | 8 ++ ...hesFromRestaurantWithoutName.approved.json | 13 ++ .../DishResourcesIT/dishes-scenario-1.yml | 10 ++ .../dishes-scenario-2-modified.yml | 17 +++ .../DishResourcesIT/dishes-scenario-2.yml | 16 +++ 13 files changed, 585 insertions(+) create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/DishResources.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishRequest.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishResponse.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/DishMapper.java create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.java create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldCreateDishesFromRestaurant.approved.txt create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindAllDishesFromRestaurant.approved.json create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindByIdDishesFromRestaurant.approved.json create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldShowErrorOnCreateDishesFromRestaurantWithoutName.approved.json create mode 100644 my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-1.yml create mode 100644 my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2-modified.yml create mode 100644 my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2.yml diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/DishResources.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/DishResources.java new file mode 100644 index 0000000..fe3ff05 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/DishResources.java @@ -0,0 +1,92 @@ +package dev.helis.registration.rest; + +import java.net.MalformedURLException; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import dev.helis.registration.entity.Dish; +import dev.helis.registration.entity.Restaurant; +import dev.helis.registration.rest.dto.DishRequest; +import dev.helis.registration.rest.dto.DishResponse; +import dev.helis.registration.rest.dto.mappers.DishMapper; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilderException; + +@Path(ResourcePaths.DISHES) +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class DishResources { + + private static final String SELECT_FROM_DISH_WHERE_RESTAURANT_ID_AND_ID = "restaurant.id = ?1 and id = ?2"; + + @GET + public List findAll(@PathParam("restaurantId") Long restaurantId) { + return Dish.list("restaurant.id", restaurantId).stream().map( r -> (Dish)r).map(DishMapper::mapToDto).collect(Collectors.toList()); + } + + @GET + @Path("/{id}") + public DishResponse findById(@PathParam("restaurantId") Long restaurantId, @PathParam("id") Long id) { + return Dish.find(SELECT_FROM_DISH_WHERE_RESTAURANT_ID_AND_ID, restaurantId, id).singleResultOptional().map( r -> (Dish)r).map(DishMapper::mapToDto).orElseThrow(NotFoundException::new); + } + + @POST + @Transactional + public Response create(@PathParam("restaurantId") Long restaurantId, @Valid DishRequest request) throws MalformedURLException, IllegalArgumentException, UriBuilderException { + + Optional restaurant = Restaurant.findByIdOptional(restaurantId); + if (!restaurant.isPresent()) { + throw new NotFoundException(); + } + + Dish mapToEntity = DishMapper.mapToEntity(restaurant.get(),request); + mapToEntity.restaurant = restaurant.get(); + mapToEntity.persist(); + + return Response.status(Response.Status.CREATED).location(URI.create(ResourcePaths.DISHES.replace("{restaurantId}", restaurantId.toString()) + "/" + mapToEntity.id)).build(); + } + + @PUT + @Path("/{id}") + @Transactional + public Response update(@PathParam("restaurantId") Long restaurantId, @PathParam("id") Long id, @Valid DishRequest request) throws MalformedURLException, IllegalArgumentException, UriBuilderException { + Optional optional = Dish.find(SELECT_FROM_DISH_WHERE_RESTAURANT_ID_AND_ID, restaurantId, id).singleResultOptional(); + if (!optional.isPresent()) { + throw new NotFoundException(); + } + Dish dish = optional.get(); + dish.name = request.getName(); + dish.description = request.getDescription(); + dish.price = request.getPrice(); + dish.image = null; + dish.category = request.getCategory(); + dish.isAvailable = request.getIsAvailable(); + dish.persist(); + return Response.status(Response.Status.NO_CONTENT).build(); + } + + @DELETE + @Path("/{id}") + @Transactional + public void delete(@PathParam("restaurantId") Long restaurantId, @PathParam("id") Long id) { + Optional optional = Dish.find(SELECT_FROM_DISH_WHERE_RESTAURANT_ID_AND_ID, restaurantId, id).singleResultOptional(); + if (!optional.isPresent()) { + throw new NotFoundException(); + } + optional.get().delete(); + } +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/ResourcePaths.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/ResourcePaths.java index 64aa882..e95bd18 100644 --- a/my-delivery-registration/src/main/java/dev/helis/registration/rest/ResourcePaths.java +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/ResourcePaths.java @@ -3,6 +3,7 @@ public class ResourcePaths { public static final String RESTAURANTS = "/restaurants"; + public static final String DISHES = "/restaurants/{restaurantId}/dishes"; private ResourcePaths() { // Prevent instantiation diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishRequest.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishRequest.java new file mode 100644 index 0000000..5ea5bfc --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishRequest.java @@ -0,0 +1,91 @@ +package dev.helis.registration.rest.dto; + +import java.io.Serializable; +import java.math.BigDecimal; + +import dev.helis.registration.entity.Category; +import dev.helis.registration.validation.OnlyCharacterAndPunctuation; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + + +public class DishRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotBlank + @OnlyCharacterAndPunctuation + private String name; + + @OnlyCharacterAndPunctuation + private String description; + + @NotNull + @Positive + private BigDecimal price; + + private String image; + + @NotNull + private Category category; + + @NotNull + private Boolean isAvailable; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public Boolean getIsAvailable() { + return isAvailable; + } + + public void setIsAvailable(Boolean isAvailable) { + this.isAvailable = isAvailable; + } + + + @Override + public String toString() { + return "Dish [name=" + name + ", price=" + price + ", category=" + category + + "]"; + } + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishResponse.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishResponse.java new file mode 100644 index 0000000..0d75971 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/DishResponse.java @@ -0,0 +1,135 @@ +package dev.helis.registration.rest.dto; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.net.URL; + +import dev.helis.registration.entity.Category; + + +public class DishResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String name; + + private String description; + + private BigDecimal price; + + private URL image; + + private Category category; + + private Boolean isAvailable; + + private Long restaurant; + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public URL getImage() { + return image; + } + + public void setImage(URL image) { + this.image = image; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } + + public Boolean getIsAvailable() { + return isAvailable; + } + + public void setIsAvailable(Boolean isAvailable) { + this.isAvailable = isAvailable; + } + + public Long getRestaurant() { + return restaurant; + } + + public void setRestaurant(Long restaurant) { + this.restaurant = restaurant; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((category == null) ? 0 : category.hashCode()); + result = prime * result + ((restaurant == null) ? 0 : restaurant.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DishResponse other = (DishResponse) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (category != other.category) + return false; + if (restaurant == null) { + if (other.restaurant != null) + return false; + } else if (!restaurant.equals(other.restaurant)) + return false; + return true; + } + + @Override + public String toString() { + return "Dish [name=" + name + ", price=" + price + ", category=" + category + ", restaurant=" + restaurant + + "]"; + } + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/DishMapper.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/DishMapper.java new file mode 100644 index 0000000..c2e688e --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/DishMapper.java @@ -0,0 +1,48 @@ +package dev.helis.registration.rest.dto.mappers; + +import java.net.MalformedURLException; + +import dev.helis.registration.entity.Dish; +import dev.helis.registration.entity.Restaurant; +import dev.helis.registration.rest.dto.DishRequest; +import dev.helis.registration.rest.dto.DishResponse; +import jakarta.ws.rs.core.UriBuilderException; + +public class DishMapper { + + + private DishMapper() { + // Private constructor to hide the implicit public one + } + + public static DishResponse mapToDto(Dish entity) { + DishResponse dish = new DishResponse(); + + dish.setId(entity.id); + dish.setName(entity.name); + dish.setDescription(entity.description); + dish.setPrice(entity.price); + dish.setImage(entity.image); + dish.setCategory(entity.category); + dish.setIsAvailable(entity.isAvailable); + dish.setRestaurant(entity.restaurant.id); + + + return dish; + } + + public static Dish mapToEntity(Restaurant restaurant, DishRequest dto) throws MalformedURLException, IllegalArgumentException, UriBuilderException { + + Dish dish = new Dish(); + dish.name = dto.getName(); + dish.description = dto.getDescription(); + dish.price = dto.getPrice(); + dish.image = null; + dish.category = dto.getCategory(); + dish.isAvailable = dto.getIsAvailable(); + dish.restaurant = restaurant; + + return dish; + } + +} diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.java b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.java new file mode 100644 index 0000000..bb30d9b --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.java @@ -0,0 +1,135 @@ +package dev.helis.registration.rest; + + +import static io.restassured.RestAssured.given; + +import java.math.BigDecimal; + +import org.approvaltests.Approvals; +import org.approvaltests.JsonApprovals; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import com.github.database.rider.cdi.api.DBRider; +import com.github.database.rider.core.api.configuration.DBUnit; +import com.github.database.rider.core.api.configuration.Orthography; +import com.github.database.rider.core.api.dataset.DataSet; +import com.github.database.rider.core.api.dataset.ExpectedDataSet; + +import dev.helis.registration.RegistrationTestLifecycleManager; +import dev.helis.registration.entity.Category; +import dev.helis.registration.rest.dto.DishRequest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.transaction.Transactional; + +@DBRider +@DBUnit(caseInsensitiveStrategy = Orthography.LOWERCASE, alwaysCleanBefore=true) +@QuarkusTest +@QuarkusTestResource(RegistrationTestLifecycleManager.class) +@Tag("integration") +@Tag("dish-feature") +class DishResourcesIT { + + + @Test + @DataSet(value = "/DishResourcesIt/dishes-scenario-2.yml") + @Transactional + void shouldFindAllDishesFromRestaurant() { + + String response = given() + .when().get(ResourcePaths.DISHES, 1) + .then() + .statusCode(200) + .extract().asString(); + + JsonApprovals.verifyJson(response); + } + + @Test + @DataSet(value = "/DishResourcesIt/dishes-scenario-2.yml") + @Transactional + void shouldFindByIdDishesFromRestaurant() { + + String response = given() + .when().get(ResourcePaths.DISHES+"/{id}", 1, 23) + .then() + .statusCode(200) + .extract().asString(); + + JsonApprovals.verifyJson(response); + } + + @Test + @DataSet(value = "/DishResourcesIt/dishes-scenario-2.yml") + @ExpectedDataSet(value = "/DishResourcesIt/dishes-scenario-1.yml") + @Transactional + void shouldDeleteByIdDishesFromRestaurant() { + + given() + .when().delete(ResourcePaths.DISHES+"/{id}", 1, 1) + .then() + .statusCode(204); + + } + + @Test + @DataSet(value = "/DishResourcesIt/dishes-scenario-1.yml") + @ExpectedDataSet(value = "/DishResourcesIt/dishes-scenario-2.yml") + @Transactional + void shouldCreateDishesFromRestaurant() { + DishRequest request = new DishRequest(); + request.setName("X-Burguer"); + request.setPrice(new BigDecimal("9.99")); + request.setCategory(Category.MAIN_COURSE); + request.setIsAvailable(Boolean.TRUE); + + String response = given() + .when().contentType("application/json").body(request) + .post(ResourcePaths.DISHES, 1) + .then() + .statusCode(201) + .extract().header("location"); + + Approvals.verify(response); + + } + + @Test + @Transactional + void shouldShowErrorOnCreateDishesFromRestaurantWithoutName() { + + String json = "{\"category\":\"MAIN_COURSE\",\"isAvailable\":true,\"price\":9.99}"; + + + String response = given() + .when().contentType("application/json").body(json) + .post(ResourcePaths.DISHES, 1) + .then() + .statusCode(400) + .extract().response().body().asString(); + + JsonApprovals.verifyJson(response); + + } + + @Test + @DataSet(value = "/DishResourcesIt/dishes-scenario-2.yml") + @ExpectedDataSet(value = "/DishResourcesIt/dishes-scenario-2-modified.yml") + @Transactional + void shouldUpdateNameDishesFromRestaurant() { + DishRequest request = new DishRequest(); + request.setName("X-Burguer Duo"); + request.setPrice(new BigDecimal("9.99")); + request.setCategory(Category.MAIN_COURSE); + request.setIsAvailable(Boolean.TRUE); + + given() + .when().contentType("application/json").body(request) + .put(ResourcePaths.DISHES+"/{id}", 1, 1) + .then() + .statusCode(204); + + } + +} diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldCreateDishesFromRestaurant.approved.txt b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldCreateDishesFromRestaurant.approved.txt new file mode 100644 index 0000000..d9e6d50 --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldCreateDishesFromRestaurant.approved.txt @@ -0,0 +1 @@ +http://localhost:8081/restaurants/1/dishes/1 \ No newline at end of file diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindAllDishesFromRestaurant.approved.json b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindAllDishesFromRestaurant.approved.json new file mode 100644 index 0000000..c7eb237 --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindAllDishesFromRestaurant.approved.json @@ -0,0 +1,18 @@ +[ + { + "category": "MAIN_COURSE", + "id": 1, + "isAvailable": true, + "name": "X-Burguer", + "price": 9.99, + "restaurant": 1 + }, + { + "category": "MAIN_COURSE", + "id": 23, + "isAvailable": true, + "name": "Pizza Marguerita", + "price": 12.00, + "restaurant": 1 + } +] \ No newline at end of file diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindByIdDishesFromRestaurant.approved.json b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindByIdDishesFromRestaurant.approved.json new file mode 100644 index 0000000..6ccdfc4 --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldFindByIdDishesFromRestaurant.approved.json @@ -0,0 +1,8 @@ +{ + "category": "MAIN_COURSE", + "id": 23, + "isAvailable": true, + "name": "Pizza Marguerita", + "price": 12.00, + "restaurant": 1 +} \ No newline at end of file diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldShowErrorOnCreateDishesFromRestaurantWithoutName.approved.json b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldShowErrorOnCreateDishesFromRestaurantWithoutName.approved.json new file mode 100644 index 0000000..113036f --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/DishResourcesIT.shouldShowErrorOnCreateDishesFromRestaurantWithoutName.approved.json @@ -0,0 +1,13 @@ +{ + "classViolations": [], + "parameterViolations": [ + { + "constraintType": "PARAMETER", + "message": "must not be blank", + "path": "create.request.name", + "value": "" + } + ], + "propertyViolations": [], + "returnValueViolations": [] +} \ No newline at end of file diff --git a/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-1.yml b/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-1.yml new file mode 100644 index 0000000..34f161b --- /dev/null +++ b/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-1.yml @@ -0,0 +1,10 @@ +restaurant: + - id: 1 + name: New Flavor +dish: + - id: 23 + name: Pizza Marguerita + price: 12.00 + restaurant_id: 1 + category: MAIN_COURSE + isAvailable: true \ No newline at end of file diff --git a/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2-modified.yml b/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2-modified.yml new file mode 100644 index 0000000..a75ba25 --- /dev/null +++ b/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2-modified.yml @@ -0,0 +1,17 @@ +restaurant: + - id: 1 + name: New Flavor +dish: + - id: 1 + name: X-Burguer Duo + price: 9.99 + restaurant_id: 1 + category: MAIN_COURSE + isAvailable: true + - id: 23 + name: Pizza Marguerita + price: 12.00 + restaurant_id: 1 + category: MAIN_COURSE + isAvailable: true + \ No newline at end of file diff --git a/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2.yml b/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2.yml new file mode 100644 index 0000000..8883df2 --- /dev/null +++ b/my-delivery-registration/src/test/resources/datasets/DishResourcesIT/dishes-scenario-2.yml @@ -0,0 +1,16 @@ +restaurant: + - id: 1 + name: New Flavor +dish: + - id: 1 + name: X-Burguer + price: 9.99 + restaurant_id: 1 + category: MAIN_COURSE + isAvailable: true + - id: 23 + name: Pizza Marguerita + price: 12.00 + restaurant_id: 1 + category: MAIN_COURSE + isAvailable: true