diff --git a/src/main/java/de/unistuttgart/t2/modulith/uibackend/UIBackendService.java b/src/main/java/de/unistuttgart/t2/modulith/uibackend/UIBackendService.java index c97b631..1e81605 100644 --- a/src/main/java/de/unistuttgart/t2/modulith/uibackend/UIBackendService.java +++ b/src/main/java/de/unistuttgart/t2/modulith/uibackend/UIBackendService.java @@ -39,9 +39,9 @@ public class UIBackendService { @Autowired public UIBackendService(CartService cartService, - InventoryService inventoryService, - OrderService orderService, - @Value("${t2.computation-simulator.enabled}") boolean enableComputeIntensiveSimulation) { + InventoryService inventoryService, + OrderService orderService, + @Value("${t2.computation-simulator.enabled}") boolean enableComputeIntensiveSimulation) { this.cartService = cartService; this.inventoryService = inventoryService; this.orderService = orderService; @@ -74,16 +74,27 @@ public List getAllProducts() { return inventoryService.getAllProducts(); } + /** + * Gets a product by its ID. + * + * @return product + */ + public Optional getProduct(String productId) { + return inventoryService.getSingleProduct(productId); + } + /** * Add the given number units of product to a users cart. *

- * If the product is already in the cart, the units of that product will be updated. + * If the product is already in the cart, the units of that product will be + * updated. * * @param sessionId identifies the cart to add to * @param productId id of product to be added * @param units number of units to be added (must not be negative) * @return successfully added item - * @throws ReservationFailedException if there is an error making reservations for the product + * @throws ReservationFailedException if there is an error making reservations + * for the product */ public Product addItemToCart(String sessionId, String productId, int units) throws ReservationFailedException { // contact inventory first, cause i'd rather have a dangling reservation than a @@ -94,8 +105,8 @@ public Product addItemToCart(String sessionId, String productId, int units) thro addedProduct.setUnits(units); } catch (InsufficientUnitsAvailableException e) { throw new ReservationFailedException(String.format( - "Adding item %s with %s units to cart of session %s failed. Reason: %s", - productId, units, sessionId, e.getMessage())); + "Adding item %s with %s units to cart of session %s failed. Reason: %s", + productId, units, sessionId, e.getMessage())); } cartService.addItemToCart(sessionId, productId, units); return addedProduct; @@ -104,7 +115,8 @@ public Product addItemToCart(String sessionId, String productId, int units) thro /** * Delete the given number units of product from a users cart. *

- * If the number of units in the cart decrease to zero or less, the product is remove from the cart. If the no such + * If the number of units in the cart decrease to zero or less, the product is + * remove from the cart. If the no such * product is in cart, do nothing. * * @param sessionId identifies the cart to delete from @@ -149,8 +161,10 @@ public List getProductsInCart(String sessionId) { } /** - * Posts a request to start a transaction to the orchestrator. Attempts to delete the cart of the given sessionId - * once the orchestrator accepted the request. Nothing happens if the deletion of a cart fails, as the cart service + * Posts a request to start a transaction to the orchestrator. Attempts to + * delete the cart of the given sessionId + * once the orchestrator accepted the request. Nothing happens if the deletion + * of a cart fails, as the cart service * supposed to periodically remove out dated cart entries anyway. * * @param sessionId identifies the session @@ -158,7 +172,8 @@ public List getProductsInCart(String sessionId) { * @param cardOwner part of payment details * @param checksum part of payment details */ - public void confirmOrder(String sessionId, String cardNumber, String cardOwner, String checksum) throws OrderNotPlacedException { + public void confirmOrder(String sessionId, String cardNumber, String cardOwner, String checksum) + throws OrderNotPlacedException { try { orderService.confirmOrder(sessionId, cardNumber, cardOwner, checksum); } catch (Exception e) { @@ -178,6 +193,7 @@ private void simulateComputeIntensiveTask(String sessionId) { LOG.info("Start simulation of an intensive computation task ... Session: {}", sessionId); // Returns the duration in milliseconds that the calculation took Double duration = computationSimulatorService.doCompute(); - LOG.info("Finished simulation of an intensive computation task. Duration: {} ms, Session: {}", duration, sessionId); + LOG.info("Finished simulation of an intensive computation task. Duration: {} ms, Session: {}", duration, + sessionId); } } diff --git a/src/main/java/de/unistuttgart/t2/modulith/uibackend/web/UIBackendController.java b/src/main/java/de/unistuttgart/t2/modulith/uibackend/web/UIBackendController.java index 3e177ce..98f8ad3 100644 --- a/src/main/java/de/unistuttgart/t2/modulith/uibackend/web/UIBackendController.java +++ b/src/main/java/de/unistuttgart/t2/modulith/uibackend/web/UIBackendController.java @@ -5,8 +5,10 @@ import de.unistuttgart.t2.modulith.uibackend.exceptions.OrderNotPlacedException; import de.unistuttgart.t2.modulith.uibackend.exceptions.ReservationFailedException; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.slf4j.Logger; @@ -15,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import java.util.ArrayList; import java.util.List; @@ -22,7 +25,8 @@ /** * Defines the http endpoints of the UIBackend. - * These endpoints are not used by the UI, but can be used e.g. for load testing the application. + * These endpoints are not used by the UI, but can be used e.g. for load testing + * the application. * * @author maumau * @author davidkopp @@ -39,33 +43,61 @@ public UIBackendController(@Autowired UIBackendService service) { } /** - * @return a list of all products in the inventory + * Get all existing products in the inventory + * + * @return list of products */ - @Operation(summary = "List all available products") + @Operation(summary = "List all available products", description = "Retrieve a list of all available products.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "List of products retrieved successfully", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Product.class)))), + }) @GetMapping("/products") public List getAllProducts() { return service.getAllProducts(); } + /** + * Get a specific product by its ID + * + * @param productId ID of the product + * @return product if ID exists + */ + @Operation(summary = "Get product by ID", description = "Retrieve a product by its unique identifier.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product retrieved successfully", content = @Content(schema = @Schema(implementation = Product.class))), + @ApiResponse(responseCode = "404", description = "Product not found", content = @Content(schema = @Schema(example = "{\"message\": \"Product with ID '123' not found\"}"))) + }) + @GetMapping("/products/{productId}") + public Product getProduct(@PathVariable String productId) { + return service.getProduct(productId) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.NOT_FOUND, "Product with ID '" + productId + "' not found")); + } + /** * Update units of the given products to the cart. *

- * Add something to the cart, if the number of units is positive or delete from the cart when it is negative. Only - * add the products to the cart if the requested number of unit is available. To achieve this, at first a - * reservations are placed in the inventory and only after the reservations are succeeded be are the products added + * Add something to the cart, if the number of units is positive or delete from + * the cart when it is negative. Only + * add the products to the cart if the requested number of unit is available. To + * achieve this, at first a + * reservations are placed in the inventory and only after the reservations are + * succeeded be are the products added * to the cart. * * @param sessionId sessionId to identify the user's cart - * @param updateCartRequest request that contains the id of the products to be updated and the number of units to be + * @param updateCartRequest request that contains the id of the products to be + * updated and the number of units to be * added or deleted * @return list of successfully added items */ @Operation(summary = "Update items in cart") @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(examples = @ExampleObject(value = "{\n\"content\": {\n \"product-id\": 3\n }\n}"))) - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Cart updated"), - @ApiResponse(responseCode = "500", description = "Cart could not be updated")}) + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Cart updated"), + @ApiResponse(responseCode = "500", description = "Cart could not be updated") }) @PostMapping("/cart/{sessionId}") - public List updateCart(@PathVariable String sessionId, @RequestBody UpdateCartRequest updateCartRequest) throws ReservationFailedException { + public List updateCart(@PathVariable String sessionId, @RequestBody UpdateCartRequest updateCartRequest) + throws ReservationFailedException { List successfullyAddedProducts = new ArrayList<>(); for (Map.Entry product : updateCartRequest.getContent().entrySet()) { @@ -96,29 +128,31 @@ public List getCart(@PathVariable String sessionId) { /** * Place an order, i.e. start a transaction.
- * Upon successfully placing the order, the cart is cleared and the session gets invalidated.
+ * Upon successfully placing the order, the cart is cleared and the session gets + * invalidated.
* If the user wants to place another order he needs a new http session. * * @param request request to place an Order * @throws OrderNotPlacedException if the order could not be placed. */ @Operation(summary = "Order all items in the cart", description = "Order all items in the cart") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Order for items is placed"), - @ApiResponse(responseCode = "500", description = "Order could not be placed")}) + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Order for items is placed"), + @ApiResponse(responseCode = "500", description = "Order could not be placed") }) @PostMapping("/confirm") public void confirmOrder(@RequestBody OrderRequest request) - throws OrderNotPlacedException { + throws OrderNotPlacedException { service.confirmOrder(request.getSessionId(), request.getCardNumber(), request.getCardOwner(), - request.getChecksum()); + request.getChecksum()); } /** - * Creates the response entity if a request could not be served because a custom exception was thrown. + * Creates the response entity if a request could not be served because a custom + * exception was thrown. * * @param exception the exception that was thrown * @return a response entity with an exceptional message */ - @ExceptionHandler({OrderNotPlacedException.class, ReservationFailedException.class}) + @ExceptionHandler({ OrderNotPlacedException.class, ReservationFailedException.class }) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseEntity handleCustomException(Exception exception) { LOG.error("Internal server error. Caused by: {}", exception.getMessage()); diff --git a/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendControllerTests.java b/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendControllerTests.java index fedc0a4..dcabb3c 100644 --- a/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendControllerTests.java +++ b/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendControllerTests.java @@ -16,13 +16,18 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.server.ResponseStatusException; import java.util.List; import java.util.Map; +import java.util.Optional; import static de.unistuttgart.t2.modulith.TestData.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @@ -54,6 +59,36 @@ public void setUp() { controller = new UIBackendController(service); } + @Test + public void getAllProducts() { + when(service.getAllProducts()).thenReturn(inventoryResponseAllProducts()); + + List products = controller.getAllProducts(); + + verify(service, times(1)).getAllProducts(); + assertEquals(2, products.size()); + } + + @Test + public void getProduct() { + when(service.getProduct(productId)).thenReturn(inventoryResponse()); + + Product product = controller.getProduct(productId); + + verify(service, times(1)).getProduct(productId); + assertEquals(productId, product.getId()); + } + + @Test + public void getProductNotFoundThrowsException() { + when(service.getProduct(productId)).thenReturn(Optional.empty()); + + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> controller.getProduct(productId)); + + assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode()); + } + @Test public void dontChangeCartIfUnitsAreZero() throws ReservationFailedException { @@ -72,7 +107,8 @@ public void addItemToCart() throws ReservationFailedException { UpdateCartRequest request = new UpdateCartRequest(Map.of(productId, units)); controller.updateCart(sessionId, request); - verify(service, times(1)).addItemToCart(sessionIdCaptor.capture(), productIdCaptor.capture(), unitsCaptor.capture()); + verify(service, times(1)).addItemToCart(sessionIdCaptor.capture(), productIdCaptor.capture(), + unitsCaptor.capture()); assertEquals(sessionId, sessionIdCaptor.getValue()); assertEquals(productId, productIdCaptor.getValue()); assertEquals(units, unitsCaptor.getValue()); @@ -95,7 +131,8 @@ public void removeItemFromCart() throws ReservationFailedException { UpdateCartRequest request = new UpdateCartRequest(Map.of(productId, -units)); List addedProducts = controller.updateCart(sessionId, request); - verify(service, times(1)).deleteItemFromCart(sessionIdCaptor.capture(), productIdCaptor.capture(), unitsCaptor.capture()); + verify(service, times(1)).deleteItemFromCart(sessionIdCaptor.capture(), productIdCaptor.capture(), + unitsCaptor.capture()); assertEquals(sessionId, sessionIdCaptor.getValue()); assertEquals(productId, productIdCaptor.getValue()); assertEquals(units, unitsCaptor.getValue()); @@ -117,11 +154,11 @@ public final void updateCartRequestSerializingAndDeserializing() throws JsonProc ObjectMapper mapper = new ObjectMapper(); UpdateCartRequest original = new UpdateCartRequest( - Map.of("c1e359ff-4cd7-4ede-93fb-378aced160e5", 1)); + Map.of("c1e359ff-4cd7-4ede-93fb-378aced160e5", 1)); String serialized = mapper.writeValueAsString(original); UpdateCartRequest deserialized = mapper.reader() - .forType(UpdateCartRequest.class) - .readValue(serialized); + .forType(UpdateCartRequest.class) + .readValue(serialized); assertEquals(original.getContent(), deserialized.getContent()); } diff --git a/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendServiceTests.java b/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendServiceTests.java index 433ec57..d7d96fe 100644 --- a/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendServiceTests.java +++ b/src/test/java/de/unistuttgart/t2/modulith/uibackend/UIBackendServiceTests.java @@ -16,9 +16,11 @@ import org.springframework.test.context.ActiveProfiles; import java.util.List; +import java.util.Optional; import static de.unistuttgart.t2.modulith.TestData.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -85,6 +87,19 @@ public void getAllProducts() { assertEquals(2, result.size()); } + @Test + public void getSingleProduct() { + + // setup + when(inventoryService.getSingleProduct(productId)).thenReturn(inventoryResponse()); + + // execute + Optional result = service.getProduct(productId); + + // assert + assertTrue(result.isPresent()); + } + @Test public void addItemToCart() throws InsufficientUnitsAvailableException, ReservationFailedException {