diff --git a/src/main/java/com/leungcheng/spring_simple_backend/auth/JwtService.java b/src/main/java/com/leungcheng/spring_simple_backend/auth/JwtService.java index d384c17..0f2ea49 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/auth/JwtService.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/auth/JwtService.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.time.Instant; import java.util.Date; +import java.util.UUID; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.springframework.beans.factory.annotation.Value; @@ -40,7 +41,7 @@ private Date getExpirationDate() { public String generateAccessToken(User user) { return Jwts.builder() - .subject(user.getId()) + .subject(user.getId().toString()) .expiration(getExpirationDate()) .signWith(this.secretKey, Jwts.SIG.HS256) .compact(); @@ -59,7 +60,7 @@ public UserAuthenticatedInfo parseAccessToken(String token) { .parseSignedClaims(token) .getPayload() .getSubject(); - return new UserAuthenticatedInfo(userId); + return new UserAuthenticatedInfo(UUID.fromString(userId)); } catch (Exception e) { if (e instanceof ExpiredJwtException) { throw new InvalidTokenException("Expired token"); diff --git a/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfo.java b/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfo.java index 897ade5..b09503c 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfo.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfo.java @@ -1,3 +1,5 @@ package com.leungcheng.spring_simple_backend.auth; -public record UserAuthenticatedInfo(String userId) {} +import java.util.UUID; + +public record UserAuthenticatedInfo(UUID userId) {} diff --git a/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfoToken.java b/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfoToken.java index c6af7bd..a1c5699 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfoToken.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/auth/UserAuthenticatedInfoToken.java @@ -17,7 +17,7 @@ public Object getCredentials() { } @Override - public String getPrincipal() { - return userAuthenticatedInfo.userId(); + public UserAuthenticatedInfo getPrincipal() { + return userAuthenticatedInfo; } } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/controller/MeController.java b/src/main/java/com/leungcheng/spring_simple_backend/controller/MeController.java index 99f7fa1..4d6de4e 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/controller/MeController.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/controller/MeController.java @@ -14,8 +14,7 @@ public class MeController { @GetMapping("/me") public UserAccountInfo me(UserAuthenticatedInfoToken authToken) { - String userId = authToken.getPrincipal(); - User user = userRepository.findById(userId).orElseThrow(); + User user = userRepository.findById(authToken.getPrincipal().userId()).orElseThrow(); return new UserAccountInfo(user.getUsername(), user.getBalance()); } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/controller/OrderController.java b/src/main/java/com/leungcheng/spring_simple_backend/controller/OrderController.java index 96a1155..80535c2 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/controller/OrderController.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/controller/OrderController.java @@ -5,8 +5,8 @@ import com.leungcheng.spring_simple_backend.domain.order.OrderService; import com.leungcheng.spring_simple_backend.domain.order.PurchaseItems; import jakarta.validation.Valid; -import jakarta.validation.constraints.Size; import java.util.Map; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; @@ -23,14 +23,13 @@ public class OrderController { Order newOrder( @Valid @RequestBody CreateOrderRequest createOrderRequest, UserAuthenticatedInfoToken authToken) { - String userId = authToken.getPrincipal(); PurchaseItems purchaseItems = new PurchaseItems(); for (var entry : createOrderRequest.productIdToQuantity().entrySet()) { purchaseItems.setPurchaseItem(entry.getKey(), entry.getValue()); } - return orderService.createOrder(userId, purchaseItems, createOrderRequest.requestId()); + return orderService.createOrder( + authToken.getPrincipal().userId(), purchaseItems, createOrderRequest.requestId()); } - public record CreateOrderRequest( - @Size(max = 36) String requestId, Map productIdToQuantity) {} + public record CreateOrderRequest(UUID requestId, Map productIdToQuantity) {} } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductController.java b/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductController.java index 80b9fd0..9cc8764 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductController.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductController.java @@ -5,6 +5,7 @@ import com.leungcheng.spring_simple_backend.domain.ProductRepository; import jakarta.validation.Valid; import java.math.BigDecimal; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; @@ -18,19 +19,18 @@ public class ProductController { Product newProduct( @Valid @RequestBody CreateProductRequest createProductRequest, UserAuthenticatedInfoToken authToken) { - String userId = authToken.getPrincipal(); Product product = new Product.Builder() .name(createProductRequest.name()) .price(createProductRequest.price()) .quantity(createProductRequest.quantity()) - .userId(userId) + .userId(authToken.getPrincipal().userId()) .build(); return repository.save(product); } @GetMapping("/products/{id}") - Product one(@PathVariable String id) { + Product one(@PathVariable UUID id) { return repository.findById(id).orElseThrow(() -> new ProductNotFoundException(id)); } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductNotFoundException.java b/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductNotFoundException.java index 10974a2..ed92fd1 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductNotFoundException.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/controller/ProductNotFoundException.java @@ -1,7 +1,9 @@ package com.leungcheng.spring_simple_backend.controller; +import java.util.UUID; + class ProductNotFoundException extends RuntimeException { - ProductNotFoundException(String id) { + ProductNotFoundException(UUID id) { super("Could not find product " + id); } } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/Product.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/Product.java index f474343..ce0a1dc 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/Product.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/Product.java @@ -8,7 +8,9 @@ import jakarta.persistence.Table; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; +import java.util.UUID; @Entity @Table(name = "products") @@ -17,10 +19,10 @@ public static class Builder { private String name; private BigDecimal price; private int quantity; - private String userId; - private String id = java.util.UUID.randomUUID().toString(); + private UUID userId; + private UUID id = UUID.randomUUID(); - private Builder id(String id) { + private Builder id(UUID id) { this.id = id; return this; } @@ -40,7 +42,7 @@ public Builder quantity(int quantity) { return this; } - public Builder userId(String userId) { + public Builder userId(UUID userId) { this.userId = userId; return this; } @@ -66,9 +68,9 @@ public Builder toBuilder() { @Id @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private String id; + private UUID id; - @NotBlank private String userId; + @NotNull private UUID userId; @NotBlank private String name; @@ -79,11 +81,11 @@ public Builder toBuilder() { @Min(0) private int quantity; - public String getId() { + public UUID getId() { return id; } - public String getUserId() { + public UUID getUserId() { return userId; } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/ProductRepository.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/ProductRepository.java index ba78e1e..4b52d29 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/ProductRepository.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/ProductRepository.java @@ -1,5 +1,6 @@ package com.leungcheng.spring_simple_backend.domain; +import java.util.UUID; import org.springframework.data.repository.CrudRepository; -public interface ProductRepository extends CrudRepository {} +public interface ProductRepository extends CrudRepository {} diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/User.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/User.java index e8cfbdd..8955c69 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/User.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/User.java @@ -10,6 +10,7 @@ import java.math.BigDecimal; import java.util.Collection; import java.util.List; +import java.util.UUID; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -22,9 +23,9 @@ public static class Builder { private String username; private String password; private BigDecimal balance; - private String id = java.util.UUID.randomUUID().toString(); + private UUID id = java.util.UUID.randomUUID(); - private Builder id(String id) { + private Builder id(UUID id) { this.id = id; return this; } @@ -63,7 +64,7 @@ public User.Builder toBuilder() { return new User.Builder().username(username).password(password).balance(balance).id(id); } - @Id private String id; + @Id private UUID id; @Column(unique = true) @NotBlank @@ -80,7 +81,7 @@ public Collection getAuthorities() { return List.of(); // TODO: May be not return empty list } - public String getId() { + public UUID getId() { return id; } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/UserRepository.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/UserRepository.java index f695836..972e8fe 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/UserRepository.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/UserRepository.java @@ -1,8 +1,9 @@ package com.leungcheng.spring_simple_backend.domain; import java.util.Optional; +import java.util.UUID; import org.springframework.data.repository.CrudRepository; -public interface UserRepository extends CrudRepository { +public interface UserRepository extends CrudRepository { Optional findByUsername(String username); } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java index 6f66742..92f4dbb 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/Order.java @@ -1,31 +1,32 @@ package com.leungcheng.spring_simple_backend.domain.order; import jakarta.persistence.*; +import java.util.UUID; @Entity @Table( name = "orders", uniqueConstraints = {@UniqueConstraint(columnNames = {"requestId", "buyerUserId"})}) public class Order { - @Id private final String id = java.util.UUID.randomUUID().toString(); - private String buyerUserId; + @Id private final UUID id = UUID.randomUUID(); + private UUID buyerUserId; private PurchaseItems purchaseItems; - private String requestId; + private UUID requestId; private Order() {} - Order(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + Order(UUID buyerUserId, PurchaseItems purchaseItems, UUID requestId) { this.buyerUserId = buyerUserId; this.purchaseItems = purchaseItems; this.requestId = requestId; } - public String getId() { + public UUID getId() { return id; } - public String getBuyerUserId() { + public UUID getBuyerUserId() { return buyerUserId; } @@ -33,7 +34,7 @@ public PurchaseItems getPurchaseItems() { return purchaseItems; } - public String getRequestId() { + public UUID getRequestId() { return requestId; } } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java index bf67b2f..a3ff278 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderRepository.java @@ -1,8 +1,9 @@ package com.leungcheng.spring_simple_backend.domain.order; import java.util.Optional; +import java.util.UUID; import org.springframework.data.repository.CrudRepository; -public interface OrderRepository extends CrudRepository { - Optional findByBuyerUserIdAndRequestId(String buyerUserId, String requestId); +public interface OrderRepository extends CrudRepository { + Optional findByBuyerUserIdAndRequestId(UUID buyerUserId, UUID requestId); } diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java index 8ac9bc4..fd3cb08 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/OrderService.java @@ -9,6 +9,7 @@ import java.math.BigDecimal; import java.util.Map; import java.util.Optional; +import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @@ -25,7 +26,7 @@ public static class CreateOrderException extends MyIllegalArgumentException { // Add this static method to reduce duplication because one test in api level is interested in // this message. But it may not be necessary to move other error messages to this class until we // need them. - public static String insufficientStockMsg(String productId) { + public static String insufficientStockMsg(UUID productId) { return "Insufficient stock for product: " + productId; } @@ -36,7 +37,11 @@ public CreateOrderException(String message) { @Retryable(noRetryFor = CreateOrderException.class) @Transactional(isolation = Isolation.SERIALIZABLE) - public Order createOrder(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + public Order createOrder(UUID buyerUserId, PurchaseItems purchaseItems, UUID requestId) { + if (requestId == null) { + throw new CreateOrderException("Request ID cannot be null"); + } + Optional order = orderRepository.findByBuyerUserIdAndRequestId(buyerUserId, requestId); if (order.isPresent()) { return order.get(); @@ -55,7 +60,7 @@ public Order createOrder(String buyerUserId, PurchaseItems purchaseItems, String return addNewOrder(buyerUserId, purchaseItems, requestId); } - private Optional getUser(String userId) { + private Optional getUser(UUID userId) { return userRepository.findById(userId); } @@ -64,20 +69,20 @@ private void saveNewBalance(User buyer, BigDecimal newBalance) { userRepository.save(updatedBuyer); } - private Order addNewOrder(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + private Order addNewOrder(UUID buyerUserId, PurchaseItems purchaseItems, UUID requestId) { Order order = new Order(buyerUserId, purchaseItems, requestId); return orderRepository.save(order); } private BigDecimal processPurchaseItems(PurchaseItems purchaseItems) { - ImmutableMap productIdToQuantity = purchaseItems.getProductIdToQuantity(); + ImmutableMap productIdToQuantity = purchaseItems.getProductIdToQuantity(); if (productIdToQuantity.isEmpty()) { throw new CreateOrderException("Purchase items cannot be empty"); } BigDecimal totalCost = BigDecimal.ZERO; - for (Map.Entry entry : productIdToQuantity.entrySet()) { - String productId = entry.getKey(); + for (Map.Entry entry : productIdToQuantity.entrySet()) { + UUID productId = entry.getKey(); int purchaseQuantity = entry.getValue(); Product product = getProduct(productId); @@ -106,7 +111,7 @@ private void addProfitToSeller(Product product, int purchaseQuantity) { saveNewBalance(seller, newBalance); } - private Product getProduct(String productId) { + private Product getProduct(UUID productId) { return productRepository .findById(productId) .orElseThrow(() -> new CreateOrderException("Product: " + productId + " does not exist")); diff --git a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItems.java b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItems.java index 54f4167..69006cf 100644 --- a/src/main/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItems.java +++ b/src/main/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItems.java @@ -4,6 +4,7 @@ import com.leungcheng.spring_simple_backend.validation.MyIllegalArgumentException; import jakarta.persistence.*; import java.util.Map; +import java.util.UUID; @Embeddable public class PurchaseItems { @@ -15,16 +16,16 @@ public class PurchaseItems { joinColumns = {@JoinColumn(name = "order_id", referencedColumnName = "id")}) @MapKeyColumn(name = "product_id") @Column(name = "quantity") - private final Map productIdToQuantity = new java.util.HashMap<>(); + private final Map productIdToQuantity = new java.util.HashMap<>(); - public void setPurchaseItem(String productId, int quantity) { + public void setPurchaseItem(UUID productId, int quantity) { if (quantity < 1) { throw new MyIllegalArgumentException(INVALID_QUANTITY_MSG); } productIdToQuantity.put(productId, quantity); } - public ImmutableMap getProductIdToQuantity() { + public ImmutableMap getProductIdToQuantity() { return ImmutableMap.copyOf(productIdToQuantity); } } diff --git a/src/main/resources/db/migration/V1__initial_schema.sql b/src/main/resources/db/migration/V1__initial_schema.sql index fe99c54..22d4633 100644 --- a/src/main/resources/db/migration/V1__initial_schema.sql +++ b/src/main/resources/db/migration/V1__initial_schema.sql @@ -1,9 +1,9 @@ CREATE TABLE orders( - buyer_user_id VARCHAR(36), - id VARCHAR(36) NOT NULL, - request_id VARCHAR(36) NOT NULL, + buyer_user_id UUID, + id UUID NOT NULL, + request_id UUID NOT NULL, PRIMARY KEY(id), UNIQUE( request_id, @@ -19,9 +19,9 @@ CREATE 10 ), quantity INTEGER NOT NULL, - id VARCHAR(36) NOT NULL, + id UUID NOT NULL, name VARCHAR(255) NOT NULL, - user_id VARCHAR(36) NOT NULL, + user_id UUID NOT NULL, PRIMARY KEY(id) ); @@ -29,8 +29,8 @@ CREATE TABLE purchase_items( quantity INTEGER NOT NULL, - order_id VARCHAR(36) NOT NULL, - product_id VARCHAR(36) NOT NULL, + order_id UUID NOT NULL, + product_id UUID NOT NULL, PRIMARY KEY( order_id, product_id @@ -45,7 +45,7 @@ CREATE 19, 10 ) NOT NULL, - id VARCHAR(36) NOT NULL, + id UUID NOT NULL, password VARCHAR(60) NOT NULL, username VARCHAR(20) NOT NULL UNIQUE, PRIMARY KEY(id) diff --git a/src/test/java/com/leungcheng/spring_simple_backend/SpringSimpleBackendApplicationTests.java b/src/test/java/com/leungcheng/spring_simple_backend/SpringSimpleBackendApplicationTests.java index 9c1c8ee..5a2d115 100644 --- a/src/test/java/com/leungcheng/spring_simple_backend/SpringSimpleBackendApplicationTests.java +++ b/src/test/java/com/leungcheng/spring_simple_backend/SpringSimpleBackendApplicationTests.java @@ -15,6 +15,7 @@ import com.leungcheng.spring_simple_backend.domain.order.OrderService; import com.leungcheng.spring_simple_backend.validation.ObjectValidator.ObjectValidationException; import java.math.BigDecimal; +import java.util.UUID; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,7 +55,7 @@ private boolean isAccessTokenSet() { return !this.accessToken.isEmpty(); } - private String useNewUserAccessToken() throws Exception { + private UUID useNewUserAccessToken() throws Exception { UserCredentials userCredentials = UserCredentials.sample(); signup(userCredentials).andExpect(status().isCreated()); @@ -109,7 +110,7 @@ void shouldIgnoreIdWhenCreateProduct() throws Exception { @Test void shouldHandleObjectValidationException_AndIncludeTheProperInfoInTheResponse() throws Exception { - String userId = useNewUserAccessToken(); + UUID userId = useNewUserAccessToken(); CreateProductParams params = CreateProductParams.sample(); params.price = "-1"; @@ -140,9 +141,11 @@ void shouldHandleObjectValidationException_AndIncludeTheProperInfoInTheResponse( void shouldGet404WhenProductNotFound() throws Exception { useNewUserAccessToken(); - getProduct("invalid-id") + UUID productId = UUID.randomUUID(); + + getProduct(productId.toString()) .andExpect(status().isNotFound()) - .andExpect(content().string("Could not find product invalid-id")); + .andExpect(content().string("Could not find product " + productId)); } @Test @@ -229,12 +232,12 @@ void shouldRejectIfAuthHeaderIsNotSetCorrectly() throws Exception { @Test void shouldCreateProductWithUserIdSameAsCreator() throws Exception { - String userId = useNewUserAccessToken(); + UUID userId = useNewUserAccessToken(); CreateProductParams params = CreateProductParams.sample(); createProduct(params) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.userId").value(userId)); + .andExpect(jsonPath("$.userId").value(userId.toString())); } @Test @@ -260,7 +263,7 @@ void shouldGetAccountInfo() throws Exception { @Test void shouldCreateOrder() throws Exception { - String userId = useNewUserAccessToken(); + UUID userId = useNewUserAccessToken(); CreateProductParams productParams = CreateProductParams.sample(); @@ -273,8 +276,9 @@ void shouldCreateOrder() throws Exception { String product2Id = createProductAndGetId(productParams); // Create Order + String requestId = UUID.randomUUID().toString(); CreateOrderParams createOrderParams = - new CreateOrderParams("request-001", ImmutableMap.of(product1Id, 4, product2Id, 5)); + new CreateOrderParams(requestId, ImmutableMap.of(product1Id, 4, product2Id, 5)); createOrder(createOrderParams) .andExpect(status().isCreated()) @@ -282,23 +286,8 @@ void shouldCreateOrder() throws Exception { .andExpect(jsonPath("$.purchaseItems").exists()) .andExpect(jsonPath("$.purchaseItems.productIdToQuantity." + product1Id).value(4)) .andExpect(jsonPath("$.purchaseItems.productIdToQuantity." + product2Id).value(5)) - .andExpect(jsonPath("$.requestId").value("request-001")) - .andExpect(jsonPath("$.buyerUserId").value(userId)); - } - - @Test - void shouldCreateOrderRejectTooLongRequestId() throws Exception { - useNewUserAccessToken(); - - CreateProductParams productParams = CreateProductParams.sample(); - productParams.price = "1"; - productParams.quantity = 99; - String productId = createProductAndGetId(productParams); - - CreateOrderParams createOrderParams = - new CreateOrderParams("1".repeat(37), ImmutableMap.of(productId, 1)); - - createOrder(createOrderParams).andExpect(status().isBadRequest()); + .andExpect(jsonPath("$.requestId").value(requestId)) + .andExpect(jsonPath("$.buyerUserId").value(userId.toString())); } @Test @@ -308,8 +297,7 @@ void shouldCreateOrderApiHandleExceptionDueToNegativeQuantity() throws Exception CreateProductParams productParams = CreateProductParams.sample(); String productId = createProductAndGetId(productParams); - CreateOrderParams createOrderParams = - new CreateOrderParams("request-001", ImmutableMap.of(productId, -1)); + CreateOrderParams createOrderParams = new CreateOrderParams(ImmutableMap.of(productId, -1)); createOrder(createOrderParams) .andExpect(status().isBadRequest()) @@ -324,13 +312,15 @@ void shouldCreateOrderApiHandleCreateOrderExceptionFromOrderService() throws Exc productParams.quantity = 0; String productId = createProductAndGetId(productParams); - CreateOrderParams createOrderParams = - new CreateOrderParams("request-001", ImmutableMap.of(productId, 1)); + CreateOrderParams createOrderParams = new CreateOrderParams(ImmutableMap.of(productId, 1)); createOrder(createOrderParams) .andExpect(status().isBadRequest()) .andExpect( - content().string(OrderService.CreateOrderException.insufficientStockMsg(productId))); + content() + .string( + OrderService.CreateOrderException.insufficientStockMsg( + UUID.fromString(productId)))); } private static class CreateProductParams { @@ -427,6 +417,10 @@ private static class CreateOrderParams { this.productIdToQuantity = productIdToQuantity; } + CreateOrderParams(ImmutableMap productIdToQuantity) { + this(UUID.randomUUID().toString(), productIdToQuantity); + } + String toContent() { return "{\"requestId\": \"" + this.requestId diff --git a/src/test/java/com/leungcheng/spring_simple_backend/domain/ProductTest.java b/src/test/java/com/leungcheng/spring_simple_backend/domain/ProductTest.java index 2098ac9..a7c372b 100644 --- a/src/test/java/com/leungcheng/spring_simple_backend/domain/ProductTest.java +++ b/src/test/java/com/leungcheng/spring_simple_backend/domain/ProductTest.java @@ -5,23 +5,26 @@ import com.leungcheng.spring_simple_backend.validation.ObjectValidator; import java.math.BigDecimal; +import java.util.UUID; import org.junit.jupiter.api.Test; class ProductTest { @Test void shouldCreateProduct() { + UUID userId = UUID.randomUUID(); + Product product = productBuilder() .name("Product 1") .price(new BigDecimal("1.0")) .quantity(50) - .userId("user_001") + .userId(userId) .build(); assertEquals("Product 1", product.getName()); assertEquals(new BigDecimal("1.0"), product.getPrice()); - assertEquals("user_001", product.getUserId()); + assertEquals(userId, product.getUserId()); assertEquals(50, product.getQuantity()); } @@ -56,7 +59,6 @@ void shouldRaiseExceptionWhenBuild_IfParamsViolateTheValidationConstraints() { assertThrowValidationException(productBuilder().name("")); assertThrowValidationException(productBuilder().name(null)); - assertThrowValidationException(productBuilder().userId("")); assertThrowValidationException(productBuilder().userId(null)); } diff --git a/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java b/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java index 9d10c3a..9411c83 100644 --- a/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java +++ b/src/test/java/com/leungcheng/spring_simple_backend/domain/order/OrderServiceTest.java @@ -11,6 +11,7 @@ import com.leungcheng.spring_simple_backend.testutil.DefaultBuilders; import java.math.BigDecimal; import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -39,6 +40,9 @@ private User.Builder uniqueUsernameUserBuilder() { private final User seedSeller = uniqueUsernameUserBuilder().build(); + private final UUID seedRequestId1 = UUID.randomUUID(); + private final UUID seedRequestId2 = UUID.randomUUID(); + @BeforeEach void setUp() { orderRepository.deleteAll(); @@ -58,10 +62,27 @@ void shouldRejectCreateOrderWithNonExistingBuyer() { CreateOrderException exception = assertThrows( - CreateOrderException.class, () -> createOrder("non_existing_buyer_id", purchaseItems)); + CreateOrderException.class, () -> createOrder(UUID.randomUUID(), purchaseItems)); assertEquals("Buyer does not exist", exception.getMessage()); } + @Test + void shouldRejectCreateOrderWithNullRequestId() { + Product product = productBuilder().price(BigDecimal.valueOf(1)).quantity(99).build(); + productRepository.save(product); + + PurchaseItems purchaseItems = new PurchaseItems(); + purchaseItems.setPurchaseItem(product.getId(), 1); + + User buyer = uniqueUsernameUserBuilder().balance(BigDecimal.valueOf(999)).build(); + userRepository.save(buyer); + + CreateOrderException exception = + assertThrows( + CreateOrderException.class, () -> createOrder(buyer.getId(), purchaseItems, null)); + assertEquals("Request ID cannot be null", exception.getMessage()); + } + @Test void shouldRejectCreateOrderWithEmptyPurchaseItems() { User buyer = uniqueUsernameUserBuilder().build(); @@ -79,12 +100,14 @@ void shouldRejectCreateOrderWithNonExistingProduct() { User buyer = uniqueUsernameUserBuilder().build(); userRepository.save(buyer); + UUID nonExistingProductId = UUID.randomUUID(); + PurchaseItems purchaseItems = new PurchaseItems(); - purchaseItems.setPurchaseItem("non_existing_product_id", 1); + purchaseItems.setPurchaseItem(nonExistingProductId, 1); CreateOrderException exception = assertThrows(CreateOrderException.class, () -> createOrder(buyer.getId(), purchaseItems)); - assertEquals("Product: non_existing_product_id does not exist", exception.getMessage()); + assertEquals("Product: " + nonExistingProductId + " does not exist", exception.getMessage()); } @Test @@ -226,8 +249,8 @@ void shouldEachCreatedOrderHasDifferentId() { PurchaseItems purchaseItems = new PurchaseItems(); purchaseItems.setPurchaseItem(product.getId(), 1); - Order order1 = createOrder(buyer.getId(), purchaseItems, "request-01"); - Order order2 = createOrder(buyer.getId(), purchaseItems, "request-02"); + Order order1 = createOrder(buyer.getId(), purchaseItems, seedRequestId1); + Order order2 = createOrder(buyer.getId(), purchaseItems, seedRequestId2); assertNotEquals(order1.getId(), order2.getId()); } @@ -243,8 +266,8 @@ void shouldOneOrderBeingCreatedOnly_IfCreateOrderWithSameRequestIdTwice() { PurchaseItems purchaseItems = new PurchaseItems(); purchaseItems.setPurchaseItem(product.getId(), 1); - Order order1 = createOrder(buyer.getId(), purchaseItems, "request_id"); - Order order2 = createOrder(buyer.getId(), purchaseItems, "request_id"); + Order order1 = createOrder(buyer.getId(), purchaseItems, seedRequestId1); + Order order2 = createOrder(buyer.getId(), purchaseItems, seedRequestId1); assertOrderEquals(order2, order1); assertEquals(4, productRepository.findById(product.getId()).orElseThrow().getQuantity()); @@ -264,8 +287,8 @@ void shouldDifferentBuyerUsingSameRequestId_WillCreateTwoOrders() { PurchaseItems purchaseItems = new PurchaseItems(); purchaseItems.setPurchaseItem(product.getId(), 1); - Order order1 = createOrder(buyer1.getId(), purchaseItems, "request_id"); - Order order2 = createOrder(buyer2.getId(), purchaseItems, "request_id"); + Order order1 = createOrder(buyer1.getId(), purchaseItems, seedRequestId1); + Order order2 = createOrder(buyer2.getId(), purchaseItems, seedRequestId1); assertNotEquals(order1.getId(), order2.getId()); } @@ -284,8 +307,8 @@ void shouldAutoRetry_WhenOneThreadMayFailJustDueToRacing() { purchaseItems.setPurchaseItem(product.getId(), 1); // 2 threads try to buy the same product at the same time - Thread thread1 = new Thread(() -> createOrder(buyer.getId(), purchaseItems, "request-01")); - Thread thread2 = new Thread(() -> createOrder(buyer.getId(), purchaseItems, "request-02")); + Thread thread1 = new Thread(() -> createOrder(buyer.getId(), purchaseItems, seedRequestId1)); + Thread thread2 = new Thread(() -> createOrder(buyer.getId(), purchaseItems, seedRequestId2)); thread1.start(); thread2.start(); @@ -300,11 +323,11 @@ void shouldAutoRetry_WhenOneThreadMayFailJustDueToRacing() { assertEquals(0, productRepository.findById(product.getId()).orElseThrow().getQuantity()); } - private Order createOrder(String buyerUserId, PurchaseItems purchaseItems) { - return orderService.createOrder(buyerUserId, purchaseItems, "dummy_request_id"); + private Order createOrder(UUID buyerUserId, PurchaseItems purchaseItems) { + return orderService.createOrder(buyerUserId, purchaseItems, UUID.randomUUID()); } - private Order createOrder(String buyerUserId, PurchaseItems purchaseItems, String requestId) { + private Order createOrder(UUID buyerUserId, PurchaseItems purchaseItems, UUID requestId) { return orderService.createOrder(buyerUserId, purchaseItems, requestId); } diff --git a/src/test/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItemsTest.java b/src/test/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItemsTest.java index 4c6221e..7f18177 100644 --- a/src/test/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItemsTest.java +++ b/src/test/java/com/leungcheng/spring_simple_backend/domain/order/PurchaseItemsTest.java @@ -5,27 +5,31 @@ import com.google.common.collect.ImmutableMap; import com.leungcheng.spring_simple_backend.validation.MyIllegalArgumentException; +import java.util.UUID; import org.junit.jupiter.api.Test; class PurchaseItemsTest { + UUID seedProductId1 = UUID.randomUUID(); + UUID seedProductId2 = UUID.randomUUID(); + @Test void shouldSetPurchaseItemAndGetThem() { PurchaseItems purchaseItems = new PurchaseItems(); - purchaseItems.setPurchaseItem("product_id_1", 1); - purchaseItems.setPurchaseItem("product_id_2", 2); + purchaseItems.setPurchaseItem(seedProductId1, 1); + purchaseItems.setPurchaseItem(seedProductId2, 2); - ImmutableMap map = purchaseItems.getProductIdToQuantity(); - assertEquals(ImmutableMap.of("product_id_1", 1, "product_id_2", 2), map); + assertEquals( + ImmutableMap.of(seedProductId1, 1, seedProductId2, 2), + purchaseItems.getProductIdToQuantity()); } @Test void shouldDuplicateCallToSetExistingProductId_WillOverwriteTheQuantity() { PurchaseItems purchaseItems = new PurchaseItems(); - purchaseItems.setPurchaseItem("product_id_1", 1); - purchaseItems.setPurchaseItem("product_id_1", 2); + purchaseItems.setPurchaseItem(seedProductId1, 1); + purchaseItems.setPurchaseItem(seedProductId1, 2); - ImmutableMap map = purchaseItems.getProductIdToQuantity(); - assertEquals(ImmutableMap.of("product_id_1", 2), map); + assertEquals(ImmutableMap.of(seedProductId1, 2), purchaseItems.getProductIdToQuantity()); } @Test @@ -33,11 +37,10 @@ void shouldNotAllowLessThanZeroQuantity() { PurchaseItems purchaseItems = new PurchaseItems(); assertThrows( - MyIllegalArgumentException.class, () -> purchaseItems.setPurchaseItem("product_id", 0)); + MyIllegalArgumentException.class, () -> purchaseItems.setPurchaseItem(seedProductId1, 0)); assertThrows( - MyIllegalArgumentException.class, () -> purchaseItems.setPurchaseItem("product_id", -1)); + MyIllegalArgumentException.class, () -> purchaseItems.setPurchaseItem(seedProductId1, -1)); - ImmutableMap map = purchaseItems.getProductIdToQuantity(); - assertEquals(0, map.size()); + assertEquals(0, purchaseItems.getProductIdToQuantity().size()); } } diff --git a/src/test/java/com/leungcheng/spring_simple_backend/testutil/DefaultBuilders.java b/src/test/java/com/leungcheng/spring_simple_backend/testutil/DefaultBuilders.java index 7d6c085..6fbc183 100644 --- a/src/test/java/com/leungcheng/spring_simple_backend/testutil/DefaultBuilders.java +++ b/src/test/java/com/leungcheng/spring_simple_backend/testutil/DefaultBuilders.java @@ -16,7 +16,7 @@ public static Product.Builder productBuilder() { return new Product.Builder() .name("Default Product") .price(new BigDecimal("0.1")) - .userId("user_01") + .userId(java.util.UUID.randomUUID()) .quantity(1); } }