Skip to content

Commit

Permalink
Add API endpoint to get single product
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkopp committed Nov 26, 2024
1 parent 06776d3 commit 5a6e5e9
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,16 +74,27 @@ public List<Product> getAllProducts() {
return inventoryService.getAllProducts();
}

/**
* Gets a product by its ID.
*
* @return product
*/
public Optional<Product> getProduct(String productId) {
return inventoryService.getSingleProduct(productId);
}

/**
* Add the given number units of product to a users cart.
* <p>
* 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
Expand All @@ -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;
Expand All @@ -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.
* <p>
* 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
Expand Down Expand Up @@ -149,16 +161,19 @@ public List<Product> 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
* @param cardNumber part of payment details
* @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) {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,14 +17,16 @@
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;
import java.util.Map;

/**
* 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
Expand All @@ -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<Product> 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.
* <p>
* 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<Product> updateCart(@PathVariable String sessionId, @RequestBody UpdateCartRequest updateCartRequest) throws ReservationFailedException {
public List<Product> updateCart(@PathVariable String sessionId, @RequestBody UpdateCartRequest updateCartRequest)
throws ReservationFailedException {
List<Product> successfullyAddedProducts = new ArrayList<>();

for (Map.Entry<String, Integer> product : updateCartRequest.getContent().entrySet()) {
Expand Down Expand Up @@ -96,29 +128,31 @@ public List<Product> getCart(@PathVariable String sessionId) {

/**
* Place an order, i.e. start a transaction.<br>
* Upon successfully placing the order, the cart is cleared and the session gets invalidated.<br>
* Upon successfully placing the order, the cart is cleared and the session gets
* invalidated.<br>
* 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<String> handleCustomException(Exception exception) {
LOG.error("Internal server error. Caused by: {}", exception.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand Down Expand Up @@ -54,6 +59,36 @@ public void setUp() {
controller = new UIBackendController(service);
}

@Test
public void getAllProducts() {
when(service.getAllProducts()).thenReturn(inventoryResponseAllProducts());

List<Product> 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 {

Expand All @@ -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());
Expand All @@ -95,7 +131,8 @@ public void removeItemFromCart() throws ReservationFailedException {
UpdateCartRequest request = new UpdateCartRequest(Map.of(productId, -units));
List<Product> 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());
Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,6 +87,19 @@ public void getAllProducts() {
assertEquals(2, result.size());
}

@Test
public void getSingleProduct() {

// setup
when(inventoryService.getSingleProduct(productId)).thenReturn(inventoryResponse());

// execute
Optional<Product> result = service.getProduct(productId);

// assert
assertTrue(result.isPresent());
}

@Test
public void addItemToCart() throws InsufficientUnitsAvailableException, ReservationFailedException {

Expand Down

0 comments on commit 5a6e5e9

Please sign in to comment.