diff --git a/README.md b/README.md
index 09746b3..3c4afbb 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ It is part of the HappyCoders tutorial series on Hexagonal Architecture:
* [Part 2: Hexagonal Architecture with Java - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-java/).
* [Part 3: Ports and Adapters Java Tutorial: Adding a Database Adapter](https://www.happycoders.eu/software-craftsmanship/ports-and-adapters-java-tutorial-db/).
* [Part 4: Hexagonal Architecture with Quarkus - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-quarkus/).
+* [Part 5: Hexagonal Architecture with Spring Boot - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-spring-boot/).
# Branches
@@ -33,7 +34,7 @@ In the `with-quarkus` branch, you'll find an implementation using [Quarkus](http
## `with-spring`
-There will soon be an additional branch with an implementation using [Spring](https://spring.io/) instead of Quarkus.
+In the `with-spring` branch, you'll find an implementation using [Spring](https://spring.io/) as application framework.
# Architecture Overview
@@ -47,29 +48,26 @@ The following diagram shows the hexagonal architecture of the application along

-The `model` module is not represented as a hexagon because it is not defined by the Hexagonal Architecture. Hexagonal Architecture leaves open what happens inside the application hexagon.
+The `model` module is not represented as a hexagon because it is not defined by the Hexagonal Architecture. Hexagonal Architecture leaves open what happens inside the application hexagon.
# How to Run the Application
-You can run the application in Quarkus dev mode with the following command:
+The easiest way to run the application is to start the `main` method of the `Launcher` class (you'll find it in the `boostrap` module) from your IDE.
-```shell
-mvn test-compile quarkus:dev
-```
+By default, the application will run with the in-memory persistence option.
-You can use one of the following VM options to select a persistence mechanism:
+To select the MySQL persistence option, start it with the following VM option:
-* `-Dpersistence=inmemory` to select the in-memory persistence option (default)
-* `-Dpersistence=mysql` to select the MySQL option
+`-Dspring.profiles.active=mysql`
-For example, to run the application in MySQL mode, enter:
+If you selected the MySQL option, you will need a running MySQL database. The easiest way to start one is to use the following Docker command:
```shell
-mvn test-compile quarkus:dev -Dpersistence=mysql
+docker run --name hexagon-mysql -d -p3306:3306 \
+ -e MYSQL_DATABASE=shop -e MYSQL_ROOT_PASSWORD=test mysql:8.1
```
-In dev mode, Quarkus will automatically start a MySQL database using Docker,
-and it will automatically create all database tables.
+The connection parameters for the database are hardcoded in `application-mysql.properties`. If you are using the Docker container as described above, you can leave the connection parameters as they are. Otherwise, you may need to adjust them.
# Example Curl Commands
diff --git a/adapter/pom.xml b/adapter/pom.xml
index 189ccd7..c56fdf2 100644
--- a/adapter/pom.xml
+++ b/adapter/pom.xml
@@ -23,44 +23,42 @@
- io.quarkus
- quarkus-arc
+ org.springframework.boot
+ spring-boot-starter
- io.quarkus
- quarkus-hibernate-orm
+ org.springframework.boot
+ spring-boot-starter-web
- io.quarkus
- quarkus-hibernate-orm-panache
+ org.springframework.boot
+ spring-boot-starter-data-jpa
- io.quarkus
- quarkus-jdbc-mysql
+ com.mysql
+ mysql-connector-j
+ runtime
- io.quarkus
- quarkus-resteasy
-
-
- io.quarkus
- quarkus-resteasy-jackson
+ com.h2database
+ h2
+ runtime
- io.quarkus
- quarkus-junit5
+ org.springframework.boot
+ spring-boot-starter-test
test
- io.quarkus
- quarkus-junit5-mockito
+ io.rest-assured
+ rest-assured
test
- io.rest-assured
- rest-assured
+ org.testcontainers
+ mysql
test
diff --git a/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java b/adapter/src/main/java/eu/happycoders/shop/SpringAppConfig.java
similarity index 56%
rename from adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java
rename to adapter/src/main/java/eu/happycoders/shop/SpringAppConfig.java
index 9eacf0f..017563e 100644
--- a/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java
+++ b/adapter/src/main/java/eu/happycoders/shop/SpringAppConfig.java
@@ -10,38 +10,40 @@
import eu.happycoders.shop.application.service.cart.EmptyCartService;
import eu.happycoders.shop.application.service.cart.GetCartService;
import eu.happycoders.shop.application.service.product.FindProductsService;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.enterprise.inject.Instance;
-import jakarta.enterprise.inject.Produces;
-import jakarta.inject.Inject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
-class QuarkusAppConfig {
+/**
+ * Spring application configuration, making Spring beans from services defined in application
+ * module.
+ *
+ * @author Sven Woltmann
+ */
+@SpringBootApplication
+public class SpringAppConfig {
- @Inject Instance cartRepository;
+ @Autowired CartRepository cartRepository;
- @Inject Instance productRepository;
+ @Autowired ProductRepository productRepository;
- @Produces
- @ApplicationScoped
+ @Bean
GetCartUseCase getCartUseCase() {
- return new GetCartService(cartRepository.get());
+ return new GetCartService(cartRepository);
}
- @Produces
- @ApplicationScoped
+ @Bean
EmptyCartUseCase emptyCartUseCase() {
- return new EmptyCartService(cartRepository.get());
+ return new EmptyCartService(cartRepository);
}
- @Produces
- @ApplicationScoped
+ @Bean
FindProductsUseCase findProductsUseCase() {
- return new FindProductsService(productRepository.get());
+ return new FindProductsService(productRepository);
}
- @Produces
- @ApplicationScoped
+ @Bean
AddToCartUseCase addToCartUseCase() {
- return new AddToCartService(cartRepository.get(), productRepository.get());
+ return new AddToCartService(cartRepository, productRepository);
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java
index 27bbc27..68bb441 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java
@@ -10,21 +10,20 @@
import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException;
import eu.happycoders.shop.model.customer.CustomerId;
import eu.happycoders.shop.model.product.ProductId;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
/**
* REST controller for all shopping cart use cases.
*
* @author Sven Woltmann
*/
-@Path("/carts")
-@Produces(MediaType.APPLICATION_JSON)
+@RestController
+@RequestMapping("/carts")
public class AddToCartController {
private final AddToCartUseCase addToCartUseCase;
@@ -33,12 +32,11 @@ public AddToCartController(AddToCartUseCase addToCartUseCase) {
this.addToCartUseCase = addToCartUseCase;
}
- @POST
- @Path("/{customerId}/line-items")
+ @PostMapping("/{customerId}/line-items")
public CartWebModel addLineItem(
- @PathParam("customerId") String customerIdString,
- @QueryParam("productId") String productIdString,
- @QueryParam("quantity") int quantity) {
+ @PathVariable("customerId") String customerIdString,
+ @RequestParam("productId") String productIdString,
+ @RequestParam("quantity") int quantity) {
CustomerId customerId = parseCustomerId(customerIdString);
ProductId productId = parseProductId(productIdString);
@@ -46,11 +44,10 @@ public CartWebModel addLineItem(
Cart cart = addToCartUseCase.addToCart(customerId, productId, quantity);
return CartWebModel.fromDomainModel(cart);
} catch (ProductNotFoundException e) {
- throw clientErrorException(
- Response.Status.BAD_REQUEST, "The requested product does not exist");
+ throw clientErrorException(HttpStatus.BAD_REQUEST, "The requested product does not exist");
} catch (NotEnoughItemsInStockException e) {
throw clientErrorException(
- Response.Status.BAD_REQUEST, "Only %d items in stock".formatted(e.itemsInStock()));
+ HttpStatus.BAD_REQUEST, "Only %d items in stock".formatted(e.itemsInStock()));
}
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java
index 16e089b..be98b94 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java
@@ -4,19 +4,19 @@
import eu.happycoders.shop.application.port.in.cart.EmptyCartUseCase;
import eu.happycoders.shop.model.customer.CustomerId;
-import jakarta.ws.rs.DELETE;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
/**
* REST controller for all shopping cart use cases.
*
* @author Sven Woltmann
*/
-@Path("/carts")
-@Produces(MediaType.APPLICATION_JSON)
+@RestController
+@RequestMapping("/carts")
public class EmptyCartController {
private final EmptyCartUseCase emptyCartUseCase;
@@ -25,10 +25,10 @@ public EmptyCartController(EmptyCartUseCase emptyCartUseCase) {
this.emptyCartUseCase = emptyCartUseCase;
}
- @DELETE
- @Path("/{customerId}")
- public void deleteCart(@PathParam("customerId") String customerIdString) {
+ @DeleteMapping("/{customerId}")
+ public ResponseEntity deleteCart(@PathVariable("customerId") String customerIdString) {
CustomerId customerId = parseCustomerId(customerIdString);
emptyCartUseCase.emptyCart(customerId);
+ return ResponseEntity.noContent().build();
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java
index 515cd6f..da79381 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java
@@ -5,19 +5,18 @@
import eu.happycoders.shop.application.port.in.cart.GetCartUseCase;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.customer.CustomerId;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
/**
* REST controller for all shopping cart use cases.
*
* @author Sven Woltmann
*/
-@Path("/carts")
-@Produces(MediaType.APPLICATION_JSON)
+@RestController
+@RequestMapping("/carts")
public class GetCartController {
private final GetCartUseCase getCartUseCase;
@@ -26,9 +25,8 @@ public GetCartController(GetCartUseCase getCartUseCase) {
this.getCartUseCase = getCartUseCase;
}
- @GET
- @Path("/{customerId}")
- public CartWebModel getCart(@PathParam("customerId") String customerIdString) {
+ @GetMapping("/{customerId}")
+ public CartWebModel getCart(@PathVariable("customerId") String customerIdString) {
CustomerId customerId = parseCustomerId(customerIdString);
Cart cart = getCartUseCase.getCart(customerId);
return CartWebModel.fromDomainModel(cart);
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java
new file mode 100644
index 0000000..fae718b
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java
@@ -0,0 +1,24 @@
+package eu.happycoders.shop.adapter.in.rest.common;
+
+import lombok.Getter;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * An exception to be thrown in case of a client error (e.g., invalid input).
+ *
+ * @author Sven Woltmann
+ */
+public class ClientErrorException extends RuntimeException {
+
+ @Getter private final ResponseEntity response;
+
+ public ClientErrorException(ResponseEntity response) {
+ super(getMessage(response));
+ this.response = response;
+ }
+
+ private static String getMessage(ResponseEntity response) {
+ ErrorEntity body = response.getBody();
+ return body != null ? body.errorMessage() : null;
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java
new file mode 100644
index 0000000..06e6f03
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java
@@ -0,0 +1,19 @@
+package eu.happycoders.shop.adapter.in.rest.common;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * Handles {@link ClientErrorException} by returning a JSON body containing the error details.
+ *
+ * @author Sven Woltmann
+ */
+@RestControllerAdvice
+public class ClientErrorHandler {
+
+ @ExceptionHandler(ClientErrorException.class)
+ public ResponseEntity handleProductNotFoundException(ClientErrorException ex) {
+ return ex.getResponse();
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java
index bac53c4..ea6d133 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java
@@ -1,7 +1,7 @@
package eu.happycoders.shop.adapter.in.rest.common;
-import jakarta.ws.rs.ClientErrorException;
-import jakarta.ws.rs.core.Response;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
/**
* Common functionality for all REST controllers.
@@ -12,12 +12,12 @@ public final class ControllerCommons {
private ControllerCommons() {}
- public static ClientErrorException clientErrorException(Response.Status status, String message) {
+ public static ClientErrorException clientErrorException(HttpStatus status, String message) {
return new ClientErrorException(errorResponse(status, message));
}
- public static Response errorResponse(Response.Status status, String message) {
- ErrorEntity errorEntity = new ErrorEntity(status.getStatusCode(), message);
- return Response.status(status).entity(errorEntity).build();
+ public static ResponseEntity errorResponse(HttpStatus status, String message) {
+ ErrorEntity errorEntity = new ErrorEntity(status.value(), message);
+ return ResponseEntity.status(status.value()).body(errorEntity);
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java
index 5ba9ba6..edc8483 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java
@@ -3,11 +3,11 @@
import static eu.happycoders.shop.adapter.in.rest.common.ControllerCommons.clientErrorException;
import eu.happycoders.shop.model.customer.CustomerId;
-import jakarta.ws.rs.core.Response;
+import org.springframework.http.HttpStatus;
/**
- * A parser for customer IDs, throwing a {@link jakarta.ws.rs.ClientErrorException} for invalid
- * customer IDs.
+ * A parser for customer IDs, throwing a {@link
+ * org.springframework.web.client.HttpClientErrorException} for invalid customer IDs.
*
* @author Sven Woltmann
*/
@@ -19,7 +19,7 @@ public static CustomerId parseCustomerId(String string) {
try {
return new CustomerId(Integer.parseInt(string));
} catch (IllegalArgumentException e) {
- throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'customerId'");
+ throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'customerId'");
}
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java
index 5aabeae..cd3110a 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java
@@ -3,11 +3,11 @@
import static eu.happycoders.shop.adapter.in.rest.common.ControllerCommons.clientErrorException;
import eu.happycoders.shop.model.product.ProductId;
-import jakarta.ws.rs.core.Response;
+import org.springframework.http.HttpStatus;
/**
- * A parser for product IDs, throwing a {@link jakarta.ws.rs.ClientErrorException} for invalid
- * product IDs.
+ * A parser for product IDs, throwing a {@link
+ * org.springframework.web.client.HttpClientErrorException} for invalid product IDs.
*
* @author Sven Woltmann
*/
@@ -17,13 +17,13 @@ private ProductIdParser() {}
public static ProductId parseProductId(String string) {
if (string == null) {
- throw clientErrorException(Response.Status.BAD_REQUEST, "Missing 'productId'");
+ throw clientErrorException(HttpStatus.BAD_REQUEST, "Missing 'productId'");
}
try {
return new ProductId(string);
} catch (IllegalArgumentException e) {
- throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'productId'");
+ throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'productId'");
}
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java
index 2f4f11f..1a6466b 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java
@@ -4,21 +4,20 @@
import eu.happycoders.shop.application.port.in.product.FindProductsUseCase;
import eu.happycoders.shop.model.product.Product;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.QueryParam;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
import java.util.List;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
/**
* REST controller for all product use cases.
*
* @author Sven Woltmann
*/
-@Path("/products")
-@Produces(MediaType.APPLICATION_JSON)
+@RestController
+@RequestMapping("/products")
public class FindProductsController {
private final FindProductsUseCase findProductsUseCase;
@@ -27,10 +26,11 @@ public FindProductsController(FindProductsUseCase findProductsUseCase) {
this.findProductsUseCase = findProductsUseCase;
}
- @GET
- public List findProducts(@QueryParam("query") String query) {
+ @GetMapping
+ public List findProducts(
+ @RequestParam(value = "query", required = false) String query) {
if (query == null) {
- throw clientErrorException(Response.Status.BAD_REQUEST, "Missing 'query'");
+ throw clientErrorException(HttpStatus.BAD_REQUEST, "Missing 'query'");
}
List products;
@@ -38,7 +38,7 @@ public List findProducts(@QueryParam("query") String quer
try {
products = findProductsUseCase.findByNameOrDescription(query);
} catch (IllegalArgumentException e) {
- throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'query'");
+ throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'query'");
}
return products.stream().map(ProductInListWebModel::fromDomainModel).toList();
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java
index 0cb5baa..db5a386 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java
@@ -3,19 +3,19 @@
import eu.happycoders.shop.application.port.out.persistence.CartRepository;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.customer.CustomerId;
-import io.quarkus.arc.lookup.LookupIfProperty;
-import jakarta.enterprise.context.ApplicationScoped;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Repository;
/**
* Persistence adapter: Stores carts in memory.
*
* @author Sven Woltmann
*/
-@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
-@ApplicationScoped
+@ConditionalOnProperty(name = "persistence", havingValue = "inmemory", matchIfMissing = true)
+@Repository
public class InMemoryCartRepository implements CartRepository {
private final Map carts = new ConcurrentHashMap<>();
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java
index 3179707..8691411 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java
@@ -4,21 +4,21 @@
import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
-import io.quarkus.arc.lookup.LookupIfProperty;
-import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Repository;
/**
* Persistence adapter: Stores products in memory.
*
* @author Sven Woltmann
*/
-@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
-@ApplicationScoped
+@ConditionalOnProperty(name = "persistence", havingValue = "inmemory", matchIfMissing = true)
+@Repository
public class InMemoryProductRepository implements ProductRepository {
private final Map products = new ConcurrentHashMap<>();
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java
index 91315dd..6250edb 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java
@@ -3,7 +3,6 @@
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.cart.CartLineItem;
import eu.happycoders.shop.model.customer.CustomerId;
-import java.util.Optional;
/**
* Maps model carts and line items to JPA carts and line items - and vice versa.
@@ -36,11 +35,7 @@ static CartLineItemJpaEntity toJpaEntity(CartJpaEntity cartJpaEntity, CartLineIt
return entity;
}
- static Optional toModelEntityOptional(CartJpaEntity cartJpaEntity) {
- if (cartJpaEntity == null) {
- return Optional.empty();
- }
-
+ static Cart toModelEntity(CartJpaEntity cartJpaEntity) {
CustomerId customerId = new CustomerId(cartJpaEntity.getCustomerId());
Cart cart = new Cart(customerId);
@@ -50,6 +45,6 @@ static Optional toModelEntityOptional(CartJpaEntity cartJpaEntity) {
lineItemJpaEntity.getQuantity());
}
- return Optional.of(cart);
+ return cart;
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java
deleted file mode 100644
index f565e3c..0000000
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package eu.happycoders.shop.adapter.out.persistence.jpa;
-
-import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
-import jakarta.enterprise.context.ApplicationScoped;
-
-/**
- * Panache repository for {@link CartJpaEntity}.
- *
- * @author Sven Woltmann
- */
-@ApplicationScoped
-public class JpaCartPanacheRepository implements PanacheRepositoryBase {}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java
index c5192e3..6c93b09 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java
@@ -3,42 +3,42 @@
import eu.happycoders.shop.application.port.out.persistence.CartRepository;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.customer.CustomerId;
-import io.quarkus.arc.lookup.LookupIfProperty;
-import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.Optional;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Repository;
/**
* Persistence adapter: Stores carts via JPA in a database.
*
* @author Sven Woltmann
*/
-@LookupIfProperty(name = "persistence", stringValue = "mysql")
-@ApplicationScoped
+@ConditionalOnProperty(name = "persistence", havingValue = "mysql")
+@Repository
public class JpaCartRepository implements CartRepository {
- private final JpaCartPanacheRepository panacheRepository;
+ private final JpaCartSpringDataRepository springDataRepository;
- public JpaCartRepository(JpaCartPanacheRepository panacheRepository) {
- this.panacheRepository = panacheRepository;
+ public JpaCartRepository(JpaCartSpringDataRepository springDataRepository) {
+ this.springDataRepository = springDataRepository;
}
@Override
@Transactional
public void save(Cart cart) {
- panacheRepository.getEntityManager().merge(CartMapper.toJpaEntity(cart));
+ springDataRepository.save(CartMapper.toJpaEntity(cart));
}
@Override
@Transactional
public Optional findByCustomerId(CustomerId customerId) {
- CartJpaEntity cartJpaEntity = panacheRepository.findById(customerId.value());
- return CartMapper.toModelEntityOptional(cartJpaEntity);
+ Optional cartJpaEntity = springDataRepository.findById(customerId.value());
+ return cartJpaEntity.map(CartMapper::toModelEntity);
}
@Override
@Transactional
public void deleteByCustomerId(CustomerId customerId) {
- panacheRepository.deleteById(customerId.value());
+ springDataRepository.deleteById(customerId.value());
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java
new file mode 100644
index 0000000..78f1456
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java
@@ -0,0 +1,12 @@
+package eu.happycoders.shop.adapter.out.persistence.jpa;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Spring Data repository for {@link CartJpaEntity}.
+ *
+ * @author Sven Woltmann
+ */
+@Repository
+public interface JpaCartSpringDataRepository extends JpaRepository {}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java
deleted file mode 100644
index 96d855c..0000000
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package eu.happycoders.shop.adapter.out.persistence.jpa;
-
-import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
-import jakarta.enterprise.context.ApplicationScoped;
-
-/**
- * Panache repository for {@link ProductJpaEntity}.
- *
- * @author Sven Woltmann
- */
-@ApplicationScoped
-public class JpaProductPanacheRepository
- implements PanacheRepositoryBase {}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java
index 2d7eb0a..a385645 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java
@@ -4,26 +4,26 @@
import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
-import io.quarkus.arc.lookup.LookupIfProperty;
import jakarta.annotation.PostConstruct;
-import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Repository;
/**
* Persistence adapter: Stores products via JPA in a database.
*
* @author Sven Woltmann
*/
-@LookupIfProperty(name = "persistence", stringValue = "mysql")
-@ApplicationScoped
+@ConditionalOnProperty(name = "persistence", havingValue = "mysql")
+@Repository
public class JpaProductRepository implements ProductRepository {
- private final JpaProductPanacheRepository panacheRepository;
+ private final JpaProductSpringDataRepository springDataRepository;
- public JpaProductRepository(JpaProductPanacheRepository panacheRepository) {
- this.panacheRepository = panacheRepository;
+ public JpaProductRepository(JpaProductSpringDataRepository springDataRepository) {
+ this.springDataRepository = springDataRepository;
}
@PostConstruct
@@ -34,23 +34,21 @@ void createDemoProducts() {
@Override
@Transactional
public void save(Product product) {
- panacheRepository.getEntityManager().merge(ProductMapper.toJpaEntity(product));
+ springDataRepository.save(ProductMapper.toJpaEntity(product));
}
@Override
@Transactional
public Optional findById(ProductId productId) {
- ProductJpaEntity jpaEntity = panacheRepository.findById(productId.value());
- return ProductMapper.toModelEntityOptional(jpaEntity);
+ Optional jpaEntity = springDataRepository.findById(productId.value());
+ return jpaEntity.map(ProductMapper::toModelEntity);
}
@Override
@Transactional
public List findByNameOrDescription(String queryString) {
List entities =
- panacheRepository
- .find("name like ?1 or description like ?1", "%" + queryString + "%")
- .list();
+ springDataRepository.findByNameOrDescriptionLike("%" + queryString + "%");
return ProductMapper.toModelEntities(entities);
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java
new file mode 100644
index 0000000..3dc3649
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java
@@ -0,0 +1,18 @@
+package eu.happycoders.shop.adapter.out.persistence.jpa;
+
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Spring Data repository for {@link ProductJpaEntity}.
+ *
+ * @author Sven Woltmann
+ */
+@Repository
+public interface JpaProductSpringDataRepository extends JpaRepository {
+
+ @Query("SELECT p FROM ProductJpaEntity p WHERE p.name like ?1 or p.description like ?1")
+ List findByNameOrDescriptionLike(String pattern);
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java
index 50fb7be..35d2e92 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java
@@ -5,7 +5,6 @@
import eu.happycoders.shop.model.product.ProductId;
import java.util.Currency;
import java.util.List;
-import java.util.Optional;
/**
* Maps a model product to a JPA product and vice versa.
@@ -29,10 +28,6 @@ static ProductJpaEntity toJpaEntity(Product product) {
return jpaEntity;
}
- static Optional toModelEntityOptional(ProductJpaEntity jpaEntity) {
- return Optional.ofNullable(jpaEntity).map(ProductMapper::toModelEntity);
- }
-
static Product toModelEntity(ProductJpaEntity jpaEntity) {
return new Product(
new ProductId(jpaEntity.getId()),
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java b/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java
deleted file mode 100644
index c097e95..0000000
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package eu.happycoders.shop.adapter;
-
-import io.quarkus.test.junit.QuarkusTestProfile;
-import java.util.Map;
-
-public class TestProfileWithMySQL implements QuarkusTestProfile {
-
- @Override
- public Map getConfigOverrides() {
- return Map.of("persistence", "mysql");
- }
-}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java
index 12a181d..2662ed4 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java
@@ -4,20 +4,19 @@
import io.restassured.path.json.JsonPath;
import io.restassured.response.Response;
+import org.springframework.http.HttpStatus;
public final class HttpTestCommons {
private HttpTestCommons() {}
public static void assertThatResponseIsError(
- Response response,
- jakarta.ws.rs.core.Response.Status expectedStatus,
- String expectedErrorMessage) {
- assertThat(response.getStatusCode()).isEqualTo(expectedStatus.getStatusCode());
+ Response response, HttpStatus expectedStatus, String expectedErrorMessage) {
+ assertThat(response.getStatusCode()).isEqualTo(expectedStatus.value());
JsonPath json = response.jsonPath();
- assertThat(json.getInt("httpStatus")).isEqualTo(expectedStatus.getStatusCode());
+ assertThat(json.getInt("httpStatus")).isEqualTo(expectedStatus.value());
assertThat(json.getString("errorMessage")).isEqualTo(expectedErrorMessage);
}
}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java
index 65c62b7..db9d17e 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java
@@ -1,7 +1,7 @@
package eu.happycoders.shop.adapter.in.rest.cart;
-import static jakarta.ws.rs.core.Response.Status.OK;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.http.HttpStatus.OK;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.cart.CartLineItem;
@@ -13,7 +13,7 @@ public final class CartsControllerAssertions {
private CartsControllerAssertions() {}
public static void assertThatResponseIsCart(Response response, Cart cart) {
- assertThat(response.statusCode()).isEqualTo(OK.getStatusCode());
+ assertThat(response.statusCode()).isEqualTo(OK.value());
JsonPath json = response.jsonPath();
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java
index 2e2b48a..cd88f81 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java
@@ -5,10 +5,10 @@
import static eu.happycoders.shop.model.money.TestMoneyFactory.euros;
import static eu.happycoders.shop.model.product.TestProductFactory.createTestProduct;
import static io.restassured.RestAssured.given;
-import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
-import static jakarta.ws.rs.core.Response.Status.NO_CONTENT;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.NO_CONTENT;
import eu.happycoders.shop.application.port.in.cart.AddToCartUseCase;
import eu.happycoders.shop.application.port.in.cart.EmptyCartUseCase;
@@ -19,27 +19,33 @@
import eu.happycoders.shop.model.customer.CustomerId;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
import io.restassured.response.Response;
import org.junit.jupiter.api.Test;
-
-@QuarkusTest
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
+
+@ActiveProfiles("test")
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class CartsControllerTest {
private static final CustomerId TEST_CUSTOMER_ID = new CustomerId(61157);
private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99));
private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99));
- @InjectMock AddToCartUseCase addToCartUseCase;
- @InjectMock GetCartUseCase getCartUseCase;
- @InjectMock EmptyCartUseCase emptyCartUseCase;
+ @LocalServerPort private Integer port;
+
+ @MockBean AddToCartUseCase addToCartUseCase;
+ @MockBean GetCartUseCase getCartUseCase;
+ @MockBean EmptyCartUseCase emptyCartUseCase;
@Test
void givenASyntacticallyInvalidCustomerId_getCart_returnsAnError() {
String customerId = "foo";
- Response response = given().get("/carts/" + customerId).then().extract().response();
+ Response response = given().port(port).get("/carts/" + customerId).then().extract().response();
assertThatResponseIsError(response, BAD_REQUEST, "Invalid 'customerId'");
}
@@ -55,7 +61,8 @@ void givenAValidCustomerIdAndACart_getCart_requestsCartFromUseCaseAndReturnsIt()
when(getCartUseCase.getCart(customerId)).thenReturn(cart);
- Response response = given().get("/carts/" + customerId.value()).then().extract().response();
+ Response response =
+ given().port(port).get("/carts/" + customerId.value()).then().extract().response();
assertThatResponseIsCart(response, cart);
}
@@ -74,6 +81,7 @@ void givenSomeTestData_addLineItem_invokesAddToCartUseCaseAndReturnsUpdatedCart(
Response response =
given()
+ .port(port)
.queryParam("productId", productId.value())
.queryParam("quantity", quantity)
.post("/carts/" + customerId.value() + "/line-items")
@@ -92,6 +100,7 @@ void givenAnInvalidProductId_addLineItem_returnsAnError() {
Response response =
given()
+ .port(port)
.queryParam("productId", productId)
.queryParam("quantity", quantity)
.post("/carts/" + customerId.value() + "/line-items")
@@ -114,6 +123,7 @@ void givenProductNotFound_addLineItem_returnsAnError()
Response response =
given()
+ .port(port)
.queryParam("productId", productId.value())
.queryParam("quantity", quantity)
.post("/carts/" + customerId.value() + "/line-items")
@@ -136,6 +146,7 @@ void givenNotEnoughItemsInStock_addLineItem_returnsAnError()
Response response =
given()
+ .port(port)
.queryParam("productId", productId.value())
.queryParam("quantity", quantity)
.post("/carts/" + customerId.value() + "/line-items")
@@ -150,7 +161,7 @@ void givenNotEnoughItemsInStock_addLineItem_returnsAnError()
void givenACustomerId_deleteCart_invokesDeleteCartUseCaseAndReturnsUpdatedCart() {
CustomerId customerId = TEST_CUSTOMER_ID;
- given().delete("/carts/" + customerId.value()).then().statusCode(NO_CONTENT.getStatusCode());
+ given().port(port).delete("/carts/" + customerId.value()).then().statusCode(NO_CONTENT.value());
verify(emptyCartUseCase).emptyCart(customerId);
}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java
index e335f1d..6e89d64 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java
@@ -1,7 +1,7 @@
package eu.happycoders.shop.adapter.in.rest.product;
-import static jakarta.ws.rs.core.Response.Status.OK;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.http.HttpStatus.OK;
import eu.happycoders.shop.model.product.Product;
import io.restassured.path.json.JsonPath;
@@ -13,7 +13,7 @@ public final class ProductsControllerAssertions {
private ProductsControllerAssertions() {}
public static void assertThatResponseIsProduct(Response response, Product product) {
- assertThat(response.statusCode()).isEqualTo(OK.getStatusCode());
+ assertThat(response.statusCode()).isEqualTo(OK.value());
JsonPath json = response.jsonPath();
@@ -21,7 +21,7 @@ public static void assertThatResponseIsProduct(Response response, Product produc
}
public static void assertThatResponseIsProductList(Response response, List products) {
- assertThat(response.statusCode()).isEqualTo(OK.getStatusCode());
+ assertThat(response.statusCode()).isEqualTo(OK.value());
JsonPath json = response.jsonPath();
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java
index fad9dab..a0cd187 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java
@@ -5,24 +5,30 @@
import static eu.happycoders.shop.model.money.TestMoneyFactory.euros;
import static eu.happycoders.shop.model.product.TestProductFactory.createTestProduct;
import static io.restassured.RestAssured.given;
-import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
import static org.mockito.Mockito.when;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
import eu.happycoders.shop.application.port.in.product.FindProductsUseCase;
import eu.happycoders.shop.model.product.Product;
-import io.quarkus.test.InjectMock;
-import io.quarkus.test.junit.QuarkusTest;
import io.restassured.response.Response;
import java.util.List;
import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
+@ActiveProfiles("test")
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ProductsControllerTest {
private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99));
private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99));
- @InjectMock FindProductsUseCase findProductsUseCase;
+ @LocalServerPort private Integer port;
+
+ @MockBean FindProductsUseCase findProductsUseCase;
@Test
void givenAQueryAndAListOfProducts_findProducts_requestsProductsViaQueryAndReturnsThem() {
@@ -32,14 +38,14 @@ void givenAQueryAndAListOfProducts_findProducts_requestsProductsViaQueryAndRetur
when(findProductsUseCase.findByNameOrDescription(query)).thenReturn(productList);
Response response =
- given().queryParam("query", query).get("/products").then().extract().response();
+ given().port(port).queryParam("query", query).get("/products").then().extract().response();
assertThatResponseIsProductList(response, productList);
}
@Test
void givenANullQuery_findProducts_returnsError() {
- Response response = given().get("/products").then().extract().response();
+ Response response = given().port(port).get("/products").then().extract().response();
assertThatResponseIsError(response, BAD_REQUEST, "Missing 'query'");
}
@@ -51,7 +57,7 @@ void givenATooShortQuery_findProducts_returnsError() {
.thenThrow(IllegalArgumentException.class);
Response response =
- given().queryParam("query", query).get("/products").then().extract().response();
+ given().port(port).queryParam("query", query).get("/products").then().extract().response();
assertThatResponseIsError(response, BAD_REQUEST, "Invalid 'query'");
}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java
index 7a8a0d8..e75d4ef 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java
@@ -11,12 +11,11 @@
import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException;
import eu.happycoders.shop.model.customer.CustomerId;
import eu.happycoders.shop.model.product.Product;
-import jakarta.enterprise.inject.Instance;
-import jakarta.inject.Inject;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
public abstract class AbstractCartRepositoryTest {
@@ -25,20 +24,16 @@ public abstract class AbstractCartRepositoryTest {
private static final AtomicInteger CUSTOMER_ID_SEQUENCE_GENERATOR = new AtomicInteger();
- @Inject Instance cartRepositoryInstance;
+ @Autowired CartRepository cartRepository;
- @Inject Instance productRepositoryInstance;
-
- private CartRepository cartRepository;
+ @Autowired ProductRepository productRepository;
@BeforeEach
void initRepositories() {
- cartRepository = cartRepositoryInstance.get();
persistTestProducts();
}
private void persistTestProducts() {
- ProductRepository productRepository = productRepositoryInstance.get();
productRepository.save(TEST_PRODUCT_1);
productRepository.save(TEST_PRODUCT_2);
}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java
index c8c7c39..ef6e5c7 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java
@@ -5,23 +5,14 @@
import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
-import jakarta.enterprise.inject.Instance;
-import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
public abstract class AbstractProductRepositoryTest {
- @Inject Instance productRepositoryInstance;
-
- private ProductRepository productRepository;
-
- @BeforeEach
- void initRepository() {
- productRepository = productRepositoryInstance.get();
- }
+ @Autowired ProductRepository productRepository;
@Test
void givenTestProductsAndATestProductId_findById_returnsATestProduct() {
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java
index 7df6d04..5dfd4de 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java
@@ -1,7 +1,9 @@
package eu.happycoders.shop.adapter.out.persistence.inmemory;
import eu.happycoders.shop.adapter.out.persistence.AbstractCartRepositoryTest;
-import io.quarkus.test.junit.QuarkusTest;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
+@SpringBootTest
+@ActiveProfiles("test")
class InMemoryCartRepositoryTest extends AbstractCartRepositoryTest {}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java
index 6d42eb8..c99e569 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java
@@ -1,7 +1,9 @@
package eu.happycoders.shop.adapter.out.persistence.inmemory;
import eu.happycoders.shop.adapter.out.persistence.AbstractProductRepositoryTest;
-import io.quarkus.test.junit.QuarkusTest;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
+@SpringBootTest
+@ActiveProfiles("test")
class InMemoryProductRepositoryTest extends AbstractProductRepositoryTest {}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java
index 4b68780..93f29e0 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java
@@ -1,10 +1,9 @@
package eu.happycoders.shop.adapter.out.persistence.jpa;
-import eu.happycoders.shop.adapter.TestProfileWithMySQL;
import eu.happycoders.shop.adapter.out.persistence.AbstractCartRepositoryTest;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.TestProfile;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
-@TestProfile(TestProfileWithMySQL.class)
+@SpringBootTest
+@ActiveProfiles("test-with-mysql")
class JpaCartRepositoryTest extends AbstractCartRepositoryTest {}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java
index bd20969..ed633e8 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java
@@ -1,10 +1,9 @@
package eu.happycoders.shop.adapter.out.persistence.jpa;
-import eu.happycoders.shop.adapter.TestProfileWithMySQL;
import eu.happycoders.shop.adapter.out.persistence.AbstractProductRepositoryTest;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.TestProfile;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
-@TestProfile(TestProfileWithMySQL.class)
+@SpringBootTest
+@ActiveProfiles("test-with-mysql")
class JpaProductRepositoryTest extends AbstractProductRepositoryTest {}
diff --git a/adapter/src/test/resources/application-test-with-mysql.properties b/adapter/src/test/resources/application-test-with-mysql.properties
new file mode 100644
index 0000000..cdb8696
--- /dev/null
+++ b/adapter/src/test/resources/application-test-with-mysql.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:tc:mysql:8.0:///shop
+
+spring.jpa.hibernate.ddl-auto=update
+
+persistence=mysql
diff --git a/adapter/src/test/resources/application-test.properties b/adapter/src/test/resources/application-test.properties
new file mode 100644
index 0000000..71c3bde
--- /dev/null
+++ b/adapter/src/test/resources/application-test.properties
@@ -0,0 +1 @@
+spring.datasource.url=jdbc:h2:mem:testdb
diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml
index 1328237..5e3a376 100644
--- a/bootstrap/pom.xml
+++ b/bootstrap/pom.xml
@@ -34,13 +34,8 @@
- io.quarkus
- quarkus-junit5
- test
-
-
- io.quarkus
- quarkus-junit5-mockito
+ org.springframework.boot
+ spring-boot-starter-test
test
@@ -53,6 +48,11 @@
rest-assured
test
+
+ org.testcontainers
+ mysql
+ test
+
@@ -68,18 +68,8 @@
- io.quarkus.platform
- quarkus-maven-plugin
- true
-
-
-
- build
- generate-code
- generate-code-tests
-
-
-
+ org.springframework.boot
+ spring-boot-maven-plugin
diff --git a/bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java b/bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java
new file mode 100644
index 0000000..35932f7
--- /dev/null
+++ b/bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java
@@ -0,0 +1,16 @@
+package eu.happycoders.shop.bootstrap;
+
+import eu.happycoders.shop.SpringAppConfig;
+import org.springframework.boot.SpringApplication;
+
+/**
+ * Launcher for the application: starts the Spring application.
+ *
+ * @author Sven Woltmann
+ */
+public class Launcher {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringAppConfig.class, args);
+ }
+}
diff --git a/bootstrap/src/main/resources/application-mysql.properties b/bootstrap/src/main/resources/application-mysql.properties
new file mode 100644
index 0000000..da26d08
--- /dev/null
+++ b/bootstrap/src/main/resources/application-mysql.properties
@@ -0,0 +1,7 @@
+spring.datasource.url=jdbc:mysql://localhost:3306/shop
+spring.datasource.username=root
+spring.datasource.password=test
+
+spring.jpa.hibernate.ddl-auto=update
+
+persistence=mysql
diff --git a/bootstrap/src/main/resources/application.properties b/bootstrap/src/main/resources/application.properties
index bdb9f17..88c8262 100644
--- a/bootstrap/src/main/resources/application.properties
+++ b/bootstrap/src/main/resources/application.properties
@@ -1,11 +1,2 @@
-# Without this line, Quarkus would abort with the following error message:
-# "Model classes are defined for the default persistence unit but configured datasource not found:
-# the default EntityManagerFactory will not be created."
-%prod.quarkus.datasource.jdbc.url=dummy
-%prod.persistence=inmemory
-
-%mysql.quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/shop
-%mysql.quarkus.datasource.username=root
-%mysql.quarkus.datasource.password=test
-%mysql.quarkus.hibernate-orm.database.generation=update
-%mysql.persistence=mysql
+spring.datasource.url=jdbc:h2:mem:testdb
+persistence=inmemory
diff --git a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java
index ba7657f..111b362 100644
--- a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java
+++ b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java
@@ -4,34 +4,38 @@
import static eu.happycoders.shop.adapter.out.persistence.DemoProducts.LED_LIGHTS;
import static eu.happycoders.shop.adapter.out.persistence.DemoProducts.MONITOR_DESK_MOUNT;
import static io.restassured.RestAssured.given;
-import static jakarta.ws.rs.core.Response.Status.NO_CONTENT;
+import static org.springframework.http.HttpStatus.NO_CONTENT;
-import eu.happycoders.shop.adapter.TestProfileWithMySQL;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException;
import eu.happycoders.shop.model.customer.CustomerId;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.TestProfile;
import io.restassured.response.Response;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
-@TestProfile(TestProfileWithMySQL.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("test-with-mysql")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CartTest {
private static final CustomerId TEST_CUSTOMER_ID = new CustomerId(61157);
private static final String CARTS_PATH = "/carts/" + TEST_CUSTOMER_ID.value();
+ @LocalServerPort private Integer port;
+
@Test
@Order(1)
void givenAnEmptyCart_addLineItem_addsTheLineItemAndReturnsTheCartWithTheAddedItem()
throws NotEnoughItemsInStockException {
Response response =
given()
+ .port(port)
.queryParam("productId", LED_LIGHTS.id().value())
.queryParam("quantity", 3)
.post(CARTS_PATH + "/line-items")
@@ -51,6 +55,7 @@ void givenACartWithOneLineItem_addLineItem_addsTheLineItemAndReturnsACartWithTwo
throws NotEnoughItemsInStockException {
Response response =
given()
+ .port(port)
.queryParam("productId", MONITOR_DESK_MOUNT.id().value())
.queryParam("quantity", 1)
.post(CARTS_PATH + "/line-items")
@@ -68,7 +73,7 @@ void givenACartWithOneLineItem_addLineItem_addsTheLineItemAndReturnsACartWithTwo
@Test
@Order(3)
void givenACartWithTwoLineItems_getCart_returnsTheCart() throws NotEnoughItemsInStockException {
- Response response = given().get(CARTS_PATH).then().extract().response();
+ Response response = given().port(port).get(CARTS_PATH).then().extract().response();
Cart expectedCart = new Cart(TEST_CUSTOMER_ID);
expectedCart.addProduct(LED_LIGHTS, 3);
@@ -80,13 +85,13 @@ void givenACartWithTwoLineItems_getCart_returnsTheCart() throws NotEnoughItemsIn
@Test
@Order(4)
void givenACartWithTwoLineItems_delete_returnsStatusCodeNoContent() {
- given().delete(CARTS_PATH).then().statusCode(NO_CONTENT.getStatusCode());
+ given().port(port).delete(CARTS_PATH).then().statusCode(NO_CONTENT.value());
}
@Test
@Order(5)
void givenAnEmptiedCart_getCart_returnsAnEmptyCart() {
- Response response = given().get(CARTS_PATH).then().extract().response();
+ Response response = given().port(port).get(CARTS_PATH).then().extract().response();
assertThatResponseIsCart(response, new Cart(TEST_CUSTOMER_ID));
}
diff --git a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java
index 2410dea..3a152b7 100644
--- a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java
+++ b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java
@@ -5,23 +5,26 @@
import static eu.happycoders.shop.adapter.out.persistence.DemoProducts.MONITOR_DESK_MOUNT;
import static io.restassured.RestAssured.given;
-import eu.happycoders.shop.adapter.TestProfileWithMySQL;
-import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.TestProfile;
import io.restassured.response.Response;
import java.util.List;
import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
-@QuarkusTest
-@TestProfile(TestProfileWithMySQL.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("test-with-mysql")
class FindProductsTest {
+ @LocalServerPort private Integer port;
+
@Test
void givenTestProductsAndAQuery_findProducts_returnsMatchingProducts() {
String query = "monitor";
Response response =
- given().queryParam("query", query).get("/products").then().extract().response();
+ given().port(port).queryParam("query", query).get("/products").then().extract().response();
assertThatResponseIsProductList(response, List.of(COMPUTER_MONITOR, MONITOR_DESK_MOUNT));
}
diff --git a/doc/sample-requests.http b/doc/sample-requests.http
index 5208fea..2c1a5e8 100644
--- a/doc/sample-requests.http
+++ b/doc/sample-requests.http
@@ -1,11 +1,11 @@
### Search for products containing "plastic"
-GET http://localhost:8080/products/?query=plastic
+GET http://localhost:8080/products?query=plastic
### Search for products containing "monitor"
-GET http://localhost:8080/products/?query=monitor
+GET http://localhost:8080/products?query=monitor
### Invalid search (search query too short)
-GET http://localhost:8080/products/?query=x
+GET http://localhost:8080/products?query=x
### Get cart
GET http://localhost:8080/carts/61157
diff --git a/pom.xml b/pom.xml
index 57c951a..a50cc6d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,6 +4,12 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.5
+
+
eu.happycoders.shop
parent
1.0-SNAPSHOT
@@ -22,7 +28,6 @@
20
UTF-8
- 3.4.3
6.55.0
@@ -50,34 +55,25 @@
org.junit.jupiter
junit-jupiter
-
+
test
org.assertj
assertj-core
- 3.24.2
+
test
org.mockito
mockito-core
-
+
test
-
-
- io.quarkus.platform
- quarkus-bom
- ${quarkus.platform.version}
- pom
- import
-
-
com.tngtech.archunit
archunit-junit5
@@ -88,12 +84,6 @@
-
- io.quarkus.platform
- quarkus-maven-plugin
- ${quarkus.platform.version}
-
-