From 916ce9258336165b96295771978199848623bc27 Mon Sep 17 00:00:00 2001 From: Helis Freitas Date: Fri, 4 Oct 2024 21:05:53 +0000 Subject: [PATCH 1/4] feat: add Restaurant Api --- .../registration/rest/ResourcePaths.java | 10 ++ .../rest/RestaurantResources.java | 77 +++++++++++++++ .../helis/registration/rest/dto/Location.java | 79 ++++++++++++++++ .../rest/dto/RestaurantRequest.java | 61 ++++++++++++ .../rest/dto/RestaurantResponse.java | 93 +++++++++++++++++++ .../rest/dto/mappers/RestaurantMapper.java | 21 +++++ 6 files changed, 341 insertions(+) create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/ResourcePaths.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/RestaurantResources.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/Location.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantResponse.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/RestaurantMapper.java 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 new file mode 100644 index 0000000..64aa882 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/ResourcePaths.java @@ -0,0 +1,10 @@ +package dev.helis.registration.rest; + +public class ResourcePaths { + + public static final String RESTAURANTS = "/restaurants"; + + private ResourcePaths() { + // Prevent instantiation + } +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/RestaurantResources.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/RestaurantResources.java new file mode 100644 index 0000000..0b09de3 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/RestaurantResources.java @@ -0,0 +1,77 @@ +package dev.helis.registration.rest; + +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import dev.helis.registration.entity.Restaurant; +import dev.helis.registration.rest.dto.RestaurantRequest; +import dev.helis.registration.rest.dto.RestaurantResponse; +import dev.helis.registration.rest.dto.mappers.RestaurantMapper; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.servlet.http.HttpServletResponse; +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.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path(ResourcePaths.RESTAURANTS) +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class RestaurantResources { + + @GET + public List findAll() { + return Restaurant.findAll().stream().map( r -> (Restaurant)r).map(RestaurantMapper::mapToDto).collect(Collectors.toList()); + } + + @GET + @Path("/{id}") + public RestaurantResponse findById(Long id) { + return Restaurant.findByIdOptional(id).map( r -> (Restaurant)r).map(RestaurantMapper::mapToDto).orElseThrow(NotFoundException::new); + } + + @POST + @Transactional + public Response create(@Valid RestaurantRequest restaurant) { + Restaurant mapToEntity = RestaurantMapper.mapToEntity(restaurant); + Restaurant.persist(mapToEntity); + return Response.status(HttpServletResponse.SC_CREATED).location(URI.create(ResourcePaths.RESTAURANTS + "/" + mapToEntity.id)).build(); + } + + + @PUT + @Path("/{id}") + @Transactional + public void update(Long id, @Valid RestaurantRequest updatedRestaurant) { + Optional optional = Restaurant.findByIdOptional(id); + if (!optional.isPresent()) { + throw new NotFoundException(); + } + Restaurant restaurant = optional.get(); + restaurant.name = updatedRestaurant.getName(); + restaurant.location.setLatitude(updatedRestaurant.getLocation().getLatitude()); + restaurant.location.setLongitude(updatedRestaurant.getLocation().getLongitude()); + + restaurant.persist(); + } + + @DELETE + @Path("/{id}") + public void delete(Long id) { + + Optional optional = Restaurant.findByIdOptional(id); + optional.ifPresentOrElse(PanacheEntityBase::delete, () -> {throw new NotFoundException();}); + } + + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/Location.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/Location.java new file mode 100644 index 0000000..d990d73 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/Location.java @@ -0,0 +1,79 @@ +package dev.helis.registration.rest.dto; + +import java.io.Serializable; + +import jakarta.validation.constraints.Size; + +public class Location implements Serializable { + + private static final long serialVersionUID = 1L; + + @Size(min = 11, max = 12) + private String longitude; + + @Size(min = 11, max = 12) + private String latitude; + + public Location() { + // Default constructor + } + + public Location(String longitude, String latitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + public String getLongitude() { + return longitude; + } + + public void setLongitude(String longitude) { + this.longitude = longitude; + } + + public String getLatitude() { + return latitude; + } + + public void setLatitude(String latitude) { + this.latitude = latitude; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((longitude == null) ? 0 : longitude.hashCode()); + result = prime * result + ((latitude == null) ? 0 : latitude.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; + Location other = (Location) obj; + if (longitude == null) { + if (other.longitude != null) + return false; + } else if (!longitude.equals(other.longitude)) + return false; + if (latitude == null) { + if (other.latitude != null) + return false; + } else if (!latitude.equals(other.latitude)) + return false; + return true; + } + + @Override + public String toString() { + return "Location [longitude=" + longitude + ", latitude=" + latitude + "]"; + } + + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java new file mode 100644 index 0000000..dd12273 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java @@ -0,0 +1,61 @@ +package dev.helis.registration.rest.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class RestaurantRequest implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + @NotBlank + @Size(min = 5, max = 100) + private String owner; + + @NotBlank + @Size(min = 5, max = 100) + private String name; + + @NotNull + private Location location; + + public RestaurantRequest() { + // Default constructor + } + + public RestaurantRequest(@NotBlank String owner, @NotBlank String name, @NotNull Location location) { + this.owner = owner; + this.name = name; + this.location = location; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + @Override + public String toString() { + return "RestaurantRequest [owner=" + owner + ", name=" + name + ", location=" + location + "]"; + } + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantResponse.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantResponse.java new file mode 100644 index 0000000..3d8147c --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantResponse.java @@ -0,0 +1,93 @@ +package dev.helis.registration.rest.dto; + +import java.util.Objects; + +import jakarta.validation.constraints.NotBlank; + +public class RestaurantResponse implements java.io.Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String owner; + + private String name; + + private Location location; + + public RestaurantResponse(Long id, @NotBlank String owner, @NotBlank String name, + dev.helis.registration.entity.Location location) { + this.id = id; + this.owner = owner; + this.name = name; + if(Objects.nonNull(location)) { + this.location = new Location(location.convertLongitudeToDMS(), location.convertLatitudeToDMS()); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((owner == null) ? 0 : owner.hashCode()); + result = prime * result + ((name == null) ? 0 : name.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; + RestaurantResponse other = (RestaurantResponse) obj; + if (owner == null) { + if (other.owner != null) + return false; + } else if (!owner.equals(other.owner)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/RestaurantMapper.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/RestaurantMapper.java new file mode 100644 index 0000000..a8bf6d6 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/mappers/RestaurantMapper.java @@ -0,0 +1,21 @@ +package dev.helis.registration.rest.dto.mappers; + +import dev.helis.registration.entity.Location; +import dev.helis.registration.entity.Restaurant; +import dev.helis.registration.rest.dto.RestaurantRequest; +import dev.helis.registration.rest.dto.RestaurantResponse; + +public class RestaurantMapper { + + private RestaurantMapper() { + // Private constructor to hide the implicit public one + } + + public static RestaurantResponse mapToDto(Restaurant restaurant) { + return new RestaurantResponse(restaurant.id, restaurant.owner, restaurant.name, restaurant.location); + } + + public static Restaurant mapToEntity(RestaurantRequest restaurant) { + return new Restaurant(restaurant.getName(), restaurant.getOwner(), new Location(restaurant.getLocation().getLongitude(), restaurant.getLocation().getLatitude())); + } +} \ No newline at end of file From 007e84c1c32d09a74a838fbf2da0b4b4e795c61c Mon Sep 17 00:00:00 2001 From: Helis Freitas Date: Fri, 4 Oct 2024 21:10:34 +0000 Subject: [PATCH 2/4] feat: add custom validation contraint --- .../dev/helis/registration/entity/Dish.java | 2 ++ .../helis/registration/entity/Restaurant.java | 3 ++ .../rest/dto/RestaurantRequest.java | 3 ++ .../OnlyCharacterAndPunctuation.java | 33 +++++++++++++++++++ ...haracterAndPunctuationForCharSequence.java | 18 ++++++++++ 5 files changed, 59 insertions(+) create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuation.java create mode 100644 my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuationForCharSequence.java diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/entity/Dish.java b/my-delivery-registration/src/main/java/dev/helis/registration/entity/Dish.java index 42904b5..01077d2 100644 --- a/my-delivery-registration/src/main/java/dev/helis/registration/entity/Dish.java +++ b/my-delivery-registration/src/main/java/dev/helis/registration/entity/Dish.java @@ -25,8 +25,10 @@ public class Dish extends PanacheEntityBase { public Long id; @NotBlank + @OnlyCharacterAndPunctuation public String name; + @OnlyCharacterAndPunctuation public String description; @NotNull diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/entity/Restaurant.java b/my-delivery-registration/src/main/java/dev/helis/registration/entity/Restaurant.java index 1711c82..9a90523 100644 --- a/my-delivery-registration/src/main/java/dev/helis/registration/entity/Restaurant.java +++ b/my-delivery-registration/src/main/java/dev/helis/registration/entity/Restaurant.java @@ -2,6 +2,8 @@ import java.time.LocalDate; +import dev.helis.registration.validation.OnlyCharacterAndPunctuation; + import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; @@ -23,6 +25,7 @@ public class Restaurant extends PanacheEntityBase { public String owner; @NotBlank + @OnlyCharacterAndPunctuation public String name; @ManyToOne(cascade = CascadeType.ALL) diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java index dd12273..dbf11c3 100644 --- a/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java +++ b/my-delivery-registration/src/main/java/dev/helis/registration/rest/dto/RestaurantRequest.java @@ -1,5 +1,7 @@ package dev.helis.registration.rest.dto; +import dev.helis.registration.validation.OnlyCharacterAndPunctuation; + import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -14,6 +16,7 @@ public class RestaurantRequest implements java.io.Serializable { @NotBlank @Size(min = 5, max = 100) + @OnlyCharacterAndPunctuation private String name; @NotNull diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuation.java b/my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuation.java new file mode 100644 index 0000000..5d1ebc6 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuation.java @@ -0,0 +1,33 @@ +package dev.helis.registration.validation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.hibernate.validator.constraints.Normalized; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = { OnlyCharacterAndPunctuationForCharSequence.class }) +@Normalized +public @interface OnlyCharacterAndPunctuation { + + String message() default "{dev.helis.registration.validation.OnlyCharacterAndPunctuation.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuationForCharSequence.java b/my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuationForCharSequence.java new file mode 100644 index 0000000..613b2d3 --- /dev/null +++ b/my-delivery-registration/src/main/java/dev/helis/registration/validation/OnlyCharacterAndPunctuationForCharSequence.java @@ -0,0 +1,18 @@ +package dev.helis.registration.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class OnlyCharacterAndPunctuationForCharSequence implements ConstraintValidator { + + private static final String VALID_PATTERN = "^[\\p{L}.!?\\-_:;'\\s]+$"; + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + if (value == null || value.length() == 0) { + return true; + } + return value.toString().matches(VALID_PATTERN); + } + +} From 27f8355b30845b08ef643169327ac4dec3944e9d Mon Sep 17 00:00:00 2001 From: Helis Freitas Date: Fri, 4 Oct 2024 21:26:20 +0000 Subject: [PATCH 3/4] feat: add Integration Test for Restaurant Api using DBunit with DBRider CDI and Approval Test for json asserts Note: It was necessary remove stax dependency from DBRider Cdi maven dependency to correct work --- my-delivery-registration/pom.xml | 83 +++++++++++++++---- .../RegistrationTestLifecycleManager.java | 34 ++++++++ .../rest/RestaurantResourcesIT.java | 41 +++++++++ ...sIT.shouldFindAllRestaurants.approved.json | 6 ++ .../restaurant-scenario-1.yml | 3 + 5 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/RegistrationTestLifecycleManager.java create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.java create mode 100644 my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.shouldFindAllRestaurants.approved.json create mode 100644 my-delivery-registration/src/test/resources/datasets/RestaurantResourcesIT/restaurant-scenario-1.yml diff --git a/my-delivery-registration/pom.xml b/my-delivery-registration/pom.xml index 41f0931..16b5d0f 100644 --- a/my-delivery-registration/pom.xml +++ b/my-delivery-registration/pom.xml @@ -1,5 +1,7 @@ - - + + 4.0.0 dev.helis @@ -11,25 +13,25 @@ 0.0.1 Registration Api - + 3.13.0 21 + 21 + 21 UTF-8 UTF-8 - quarkus-bom - io.quarkus.platform - 3.15.1 + 3.15.1 true - 3.3.1 + 3.5.0 + 1.20.2 - - ${quarkus.platform.group-id} - ${quarkus.platform.artifact-id} - ${quarkus.platform.version} + io.quarkus + quarkus-bom + ${quarkus.version} pom import @@ -54,27 +56,68 @@ quarkus-resteasy-jsonb - io.quarkus - quarkus-hibernate-validator + io.quarkus + quarkus-hibernate-validator io.quarkus quarkus-smallrye-openapi - + io.quarkus quarkus-junit5 test - + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + com.github.database-rider + rider-cdi + 1.44.0 + test + jakarta + + + stax-api + stax + + + + + io.rest-assured + rest-assured + test + + + com.approvaltests + approvaltests + 24.7.0 + test + + + com.google.code.gson + gson + 2.11.0 + test + - ${quarkus.platform.group-id} + io.quarkus quarkus-maven-plugin - ${quarkus.platform.version} + ${quarkus.version} true @@ -88,6 +131,7 @@ + org.apache.maven.plugins maven-compiler-plugin ${compiler-plugin.version} @@ -95,9 +139,14 @@ + org.apache.maven.plugins maven-surefire-plugin ${surefire-plugin.version} + + **/*IT.java + **/*Test.java + org.jboss.logmanager.LogManager ${maven.home} diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/RegistrationTestLifecycleManager.java b/my-delivery-registration/src/test/java/dev/helis/registration/RegistrationTestLifecycleManager.java new file mode 100644 index 0000000..af029b1 --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/RegistrationTestLifecycleManager.java @@ -0,0 +1,34 @@ +package dev.helis.registration; + +import java.util.HashMap; +import java.util.Map; + +import org.testcontainers.containers.PostgreSQLContainer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class RegistrationTestLifecycleManager implements QuarkusTestResourceLifecycleManager { + + public static final PostgreSQLContainer POSTGRES = new PostgreSQLContainer<>("postgres:12.0"); + + @Override + public Map start() { + POSTGRES.start(); + + Map proprieties = new HashMap(); + + proprieties.put("quarkus.datasource.jdbc.url", POSTGRES.getJdbcUrl()); + proprieties.put("quarkus.datasource.username", POSTGRES.getUsername()); + proprieties.put("quarkus.datasource.password", POSTGRES.getPassword()); + + return proprieties; + } + + @Override + public void stop() { + if(POSTGRES != null && POSTGRES.isRunning()) { + POSTGRES.stop(); + } + } + +} \ No newline at end of file diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.java b/my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.java new file mode 100644 index 0000000..e63a6af --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.java @@ -0,0 +1,41 @@ +package dev.helis.registration.rest; + +import static io.restassured.RestAssured.given; + +import org.approvaltests.JsonApprovals; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +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.cdi.api.DBRider; + +import dev.helis.registration.RegistrationTestLifecycleManager; +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("restaurant-feature") +class RestaurantResourcesIT { + + @Test + @DataSet(value = "/RestaurantResourcesIT/restaurant-scenario-1.yml") + @Transactional + void shouldFindAllRestaurants() { + + String response = given() + .when().get(ResourcePaths.RESTAURANTS) + .then() + .statusCode(200) + .extract().asString(); + + JsonApprovals.verifyJson(response); + } + +} diff --git a/my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.shouldFindAllRestaurants.approved.json b/my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.shouldFindAllRestaurants.approved.json new file mode 100644 index 0000000..c546ce9 --- /dev/null +++ b/my-delivery-registration/src/test/java/dev/helis/registration/rest/RestaurantResourcesIT.shouldFindAllRestaurants.approved.json @@ -0,0 +1,6 @@ +[ + { + "id": 1, + "name": "New Flavor" + } +] \ No newline at end of file diff --git a/my-delivery-registration/src/test/resources/datasets/RestaurantResourcesIT/restaurant-scenario-1.yml b/my-delivery-registration/src/test/resources/datasets/RestaurantResourcesIT/restaurant-scenario-1.yml new file mode 100644 index 0000000..976d73f --- /dev/null +++ b/my-delivery-registration/src/test/resources/datasets/RestaurantResourcesIT/restaurant-scenario-1.yml @@ -0,0 +1,3 @@ +restaurant: + - id: 1 + name: New Flavor From 17e443e5c51c114e6c34dfc324f7c90121fafa1a Mon Sep 17 00:00:00 2001 From: Helis Freitas Date: Fri, 4 Oct 2024 21:43:45 +0000 Subject: [PATCH 4/4] 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