From b4d3f04d2707b5670397a0c3cf10c2242cec440f Mon Sep 17 00:00:00 2001 From: nahye Date: Tue, 4 Mar 2025 16:23:47 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Cart,=20LineItem,=20Product=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80=20=ED=9B=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/demo/model/Cart.java | 63 +++++++++++ .../java/com/example/demo/model/LineItem.java | 38 +++++++ .../java/com/example/demo/model/Product.java | 34 ++++++ .../java/com/example/demo/model/CartTest.java | 101 ++++++++++++++++++ .../com/example/demo/model/LineItemTest.java | 25 +++++ .../com/example/demo/model/ProductTest.java | 34 ++++++ 6 files changed, 295 insertions(+) create mode 100644 src/main/java/com/example/demo/model/Cart.java create mode 100644 src/main/java/com/example/demo/model/LineItem.java create mode 100644 src/main/java/com/example/demo/model/Product.java create mode 100644 src/test/java/com/example/demo/model/CartTest.java create mode 100644 src/test/java/com/example/demo/model/LineItemTest.java create mode 100644 src/test/java/com/example/demo/model/ProductTest.java diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java new file mode 100644 index 0000000..15dcd06 --- /dev/null +++ b/src/main/java/com/example/demo/model/Cart.java @@ -0,0 +1,63 @@ +package com.example.demo.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Cart { + + private List lineItems; + private int totalQuantity; + + public Cart(List lineItems) { + this.lineItems = new ArrayList<>(lineItems); + calculateTotalQuantity(); + } + + public List getLineItems() { + return Collections.unmodifiableList(lineItems); + } + + public int getTotalQuantity() { + return totalQuantity; + } + + public void addProduct(String productId, String productOption, int quantity) { + + checkValidQuantity(quantity); + + LineItem lineItem = findLineItem(productId, productOption); + + if (lineItem != null) { + lineItem.addQuantity(quantity); + calculateTotalQuantity(); + return; + } + + lineItem = new LineItem(productId, productOption, quantity); + lineItems.add(lineItem); + calculateTotalQuantity(); + } + + public void checkValidQuantity(int quantity) { + if (totalQuantity + quantity> 20) { + throw new IllegalArgumentException("담을수 있는 수량을 초과했습니다."); + } + } + + private void calculateTotalQuantity() { + this.totalQuantity = lineItems.stream() + .mapToInt(LineItem::getQuantity) + .sum(); + } + + + private LineItem findLineItem(String productId, String productOption) { + return lineItems.stream() + .filter(i -> i.getProductId().equals(productId) + && i.getProductOption().equals(productOption)) + .findFirst() + .orElse(null); + } + +} diff --git a/src/main/java/com/example/demo/model/LineItem.java b/src/main/java/com/example/demo/model/LineItem.java new file mode 100644 index 0000000..28ef63e --- /dev/null +++ b/src/main/java/com/example/demo/model/LineItem.java @@ -0,0 +1,38 @@ +package com.example.demo.model; + +import java.util.UUID; + +public class LineItem { + + private final String id; + private final String productId; + private final String productOption; + + private int quantity; + + + public LineItem(String productId, String productOption, int quantity) { + this.id = "LineItem-"+ UUID.randomUUID(); + this.productId = productId; + this.productOption = productOption; + this.quantity = quantity; + } + + public int getQuantity() { + return quantity; + } + + public String getProductId() { + return productId; + } + + public String getProductOption() { + return productOption; + } + + + + public void addQuantity(int quantity) { + this.quantity += quantity; + } +} diff --git a/src/main/java/com/example/demo/model/Product.java b/src/main/java/com/example/demo/model/Product.java new file mode 100644 index 0000000..173c720 --- /dev/null +++ b/src/main/java/com/example/demo/model/Product.java @@ -0,0 +1,34 @@ +package com.example.demo.model; + +public class Product { + + private String id; + private String name; + private int price; + + public Product(String id, String name, int price) { + this.id = id; + this.name = name; + this.price = price; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public void changePrice(int price) { + this.price = price; + } + + public void changeName(String name) { + this.name = name; + } +} diff --git a/src/test/java/com/example/demo/model/CartTest.java b/src/test/java/com/example/demo/model/CartTest.java new file mode 100644 index 0000000..7594be8 --- /dev/null +++ b/src/test/java/com/example/demo/model/CartTest.java @@ -0,0 +1,101 @@ +package com.example.demo.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CartTest { + + private Product product1; + private Product product2; + private String productOption1; + private String productOption2; + + @BeforeEach + void setUp() { + product1 = new Product("product-1", "product #1", 5000); + product2 = new Product("product-2", "product #2", 3000); + + productOption1 = "productOption-1"; + productOption2 = "productOption-2"; + } + + @Test + @DisplayName("빈 장바구니의 수량은 0이다.") + void cartTotalQuantityIsZero() { + Cart cart = new Cart(List.of()); + + assertThat(cart.getTotalQuantity()).isEqualTo(0); + } + + @Test + @DisplayName("빈 장바구니에 물건 추가하면 장바구니의 전체수량은 추가한 수량과 같다.") + void addProduct() { + Cart cart = new Cart(List.of()); + + int quantity = 1; + cart.addProduct(product1.getId(), productOption1, quantity); + + assertThat(cart.getTotalQuantity()).isEqualTo(quantity); + } + + @Test + @DisplayName("장바구니에 이미 있는 물건 추가하면 전체 수량은 이미 있던 수량과 새로 추가하는 수량의 합이다.") + void addExistingProduct() { + + int oldQuantity = 1; + Cart cart = new Cart(List.of( + createLineItem(product1, productOption1, oldQuantity) + )); + + int newQuantity = 1; + cart.addProduct(product1.getId(), productOption1, newQuantity); + + assertThat(cart.getLineItems()).hasSize(1); + assertThat(cart.getTotalQuantity()).isEqualTo(oldQuantity + newQuantity); + } + + @Test + @DisplayName("장바구니에 새로운 있는 물건 추가하면 전체 수량은 이미 있던 물건의 수량과 " + + "새로 추가하는 물건의 수량의 합이다.") + void addNewProduct() { + + int oldQuantity = 1; + Cart cart = new Cart(List.of( + createLineItem(product2, productOption2, oldQuantity) + )); + + int newQuantity = 1; + cart.addProduct(product1.getId(), productOption1, newQuantity); + + assertThat(cart.getLineItems()).hasSize(2); + assertThat(cart.getTotalQuantity()).isEqualTo(oldQuantity + newQuantity); + } + + @Test + @DisplayName("전체 장바구니의 수량이 20개가 넘어가면 예외가 발생한다.") + void totalQuantityCanNotOverLimit() { + Cart cart = new Cart(List.of( + createLineItem(product1, productOption1, 19) + )); + + int newQuantity = 5; + + assertThatThrownBy( + () -> cart.addProduct(product1.getId(), productOption1, newQuantity) + ).isInstanceOf(IllegalArgumentException.class); + + } + + + + private LineItem createLineItem(Product product, String productOption, int quantity) { + return new LineItem(product.getId(), productOption, quantity); + } + +} diff --git a/src/test/java/com/example/demo/model/LineItemTest.java b/src/test/java/com/example/demo/model/LineItemTest.java new file mode 100644 index 0000000..2810c57 --- /dev/null +++ b/src/test/java/com/example/demo/model/LineItemTest.java @@ -0,0 +1,25 @@ +package com.example.demo.model; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +class LineItemTest { + + @Test + void addQuantity() { + + String productId = "productId-1"; + String productOption = "productOption-1"; + int quantity = 5; + int delta = 5; + LineItem lineItem = new LineItem(productId, productOption, quantity); + + lineItem.addQuantity(delta); + + assertThat(lineItem.getQuantity()).isEqualTo(quantity + delta); + + } + +} diff --git a/src/test/java/com/example/demo/model/ProductTest.java b/src/test/java/com/example/demo/model/ProductTest.java new file mode 100644 index 0000000..95f796f --- /dev/null +++ b/src/test/java/com/example/demo/model/ProductTest.java @@ -0,0 +1,34 @@ +package com.example.demo.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProductTest { + + private Product product; + + @BeforeEach + void setUp() { + product = new Product("product-1", "Product #1", 5000); + } + + @Test + void changeProductName(){ + + String newName = "NewProduct"; + product.changeName(newName); + + assertThat(product.getName()).isEqualTo(newName); + } + + @Test + void changeProductPrice(){ + int newPrice = 10000; + product.changePrice(newPrice); + + assertThat(product.getPrice()).isEqualTo(newPrice); + } + +} From 074e5edc11665333154cdbb78b5d1945a97115f2 Mon Sep 17 00:00:00 2001 From: nahye Date: Tue, 4 Mar 2025 20:10:25 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=ED=9B=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cart의 checkValidQuantity() 수정 - LineItem에 isSameProduct() 추가 - LineItemId, ProductId, ProductOption VO 추가 - Test 추가 --- .../java/com/example/demo/model/Cart.java | 17 +++++----- .../java/com/example/demo/model/LineItem.java | 27 +++++++--------- .../com/example/demo/model/LineItemId.java | 12 +++++++ .../java/com/example/demo/model/Product.java | 6 ++-- .../com/example/demo/model/ProductId.java | 6 ++++ .../com/example/demo/model/ProductOption.java | 7 +++++ .../java/com/example/demo/model/CartTest.java | 14 ++++----- .../example/demo/model/LineItemIdTest.java | 30 ++++++++++++++++++ .../com/example/demo/model/LineItemTest.java | 31 ++++++++++++++----- .../com/example/demo/model/ProductIdTest.java | 20 ++++++++++++ .../example/demo/model/ProductOptionTest.java | 28 +++++++++++++++++ .../com/example/demo/model/ProductTest.java | 2 +- 12 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/example/demo/model/LineItemId.java create mode 100644 src/main/java/com/example/demo/model/ProductId.java create mode 100644 src/main/java/com/example/demo/model/ProductOption.java create mode 100644 src/test/java/com/example/demo/model/LineItemIdTest.java create mode 100644 src/test/java/com/example/demo/model/ProductIdTest.java create mode 100644 src/test/java/com/example/demo/model/ProductOptionTest.java diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java index 15dcd06..d640ab7 100644 --- a/src/main/java/com/example/demo/model/Cart.java +++ b/src/main/java/com/example/demo/model/Cart.java @@ -22,25 +22,26 @@ public int getTotalQuantity() { return totalQuantity; } - public void addProduct(String productId, String productOption, int quantity) { + public void addProduct(ProductId productId, ProductOption productOption, int quantity) { - checkValidQuantity(quantity); LineItem lineItem = findLineItem(productId, productOption); if (lineItem != null) { - lineItem.addQuantity(quantity); + lineItem.addQuantity(quantity, productOption); calculateTotalQuantity(); + checkValidQuantity(); return; } lineItem = new LineItem(productId, productOption, quantity); lineItems.add(lineItem); calculateTotalQuantity(); + checkValidQuantity(); } - public void checkValidQuantity(int quantity) { - if (totalQuantity + quantity> 20) { + public void checkValidQuantity() { + if (totalQuantity > 20) { throw new IllegalArgumentException("담을수 있는 수량을 초과했습니다."); } } @@ -52,10 +53,10 @@ private void calculateTotalQuantity() { } - private LineItem findLineItem(String productId, String productOption) { + private LineItem findLineItem(ProductId productId, ProductOption productOption) { return lineItems.stream() - .filter(i -> i.getProductId().equals(productId) - && i.getProductOption().equals(productOption)) + .filter(lineItem -> + lineItem.isSameProduct(productId,productOption)) .findFirst() .orElse(null); } diff --git a/src/main/java/com/example/demo/model/LineItem.java b/src/main/java/com/example/demo/model/LineItem.java index 28ef63e..79204f8 100644 --- a/src/main/java/com/example/demo/model/LineItem.java +++ b/src/main/java/com/example/demo/model/LineItem.java @@ -1,18 +1,17 @@ package com.example.demo.model; -import java.util.UUID; public class LineItem { - private final String id; - private final String productId; - private final String productOption; + private final LineItemId id; + private final ProductId productId; + private final ProductOption productOption; private int quantity; - public LineItem(String productId, String productOption, int quantity) { - this.id = "LineItem-"+ UUID.randomUUID(); + public LineItem(ProductId productId, ProductOption productOption, int quantity) { + this.id = LineItemId.generate(); this.productId = productId; this.productOption = productOption; this.quantity = quantity; @@ -22,17 +21,15 @@ public int getQuantity() { return quantity; } - public String getProductId() { - return productId; - } - public String getProductOption() { - return productOption; + public void addQuantity(int quantity, ProductOption productOption) { + if(!this.productOption.equals(productOption)) { + return; + } + this.quantity += quantity; } - - - public void addQuantity(int quantity) { - this.quantity += quantity; + public boolean isSameProduct(ProductId productId, ProductOption productOption) { + return this.productId.equals(productId) && this.productOption.equals(productOption); } } diff --git a/src/main/java/com/example/demo/model/LineItemId.java b/src/main/java/com/example/demo/model/LineItemId.java new file mode 100644 index 0000000..c0e1f57 --- /dev/null +++ b/src/main/java/com/example/demo/model/LineItemId.java @@ -0,0 +1,12 @@ +package com.example.demo.model; + +import java.util.UUID; + +public record LineItemId( + String id +) { + + public static LineItemId generate() { + return new LineItemId("lineItemId-" + UUID.randomUUID()); + } +} diff --git a/src/main/java/com/example/demo/model/Product.java b/src/main/java/com/example/demo/model/Product.java index 173c720..f0b49f1 100644 --- a/src/main/java/com/example/demo/model/Product.java +++ b/src/main/java/com/example/demo/model/Product.java @@ -2,17 +2,17 @@ public class Product { - private String id; + private ProductId id; private String name; private int price; - public Product(String id, String name, int price) { + public Product(ProductId id, String name, int price) { this.id = id; this.name = name; this.price = price; } - public String getId() { + public ProductId getId() { return id; } diff --git a/src/main/java/com/example/demo/model/ProductId.java b/src/main/java/com/example/demo/model/ProductId.java new file mode 100644 index 0000000..4951e6b --- /dev/null +++ b/src/main/java/com/example/demo/model/ProductId.java @@ -0,0 +1,6 @@ +package com.example.demo.model; + +public record ProductId ( + String id +){ +} diff --git a/src/main/java/com/example/demo/model/ProductOption.java b/src/main/java/com/example/demo/model/ProductOption.java new file mode 100644 index 0000000..aa16fdb --- /dev/null +++ b/src/main/java/com/example/demo/model/ProductOption.java @@ -0,0 +1,7 @@ +package com.example.demo.model; + +public record ProductOption( + String color, + String size +) { +} diff --git a/src/test/java/com/example/demo/model/CartTest.java b/src/test/java/com/example/demo/model/CartTest.java index 7594be8..972473e 100644 --- a/src/test/java/com/example/demo/model/CartTest.java +++ b/src/test/java/com/example/demo/model/CartTest.java @@ -13,16 +13,16 @@ class CartTest { private Product product1; private Product product2; - private String productOption1; - private String productOption2; + private ProductOption productOption1; + private ProductOption productOption2; @BeforeEach void setUp() { - product1 = new Product("product-1", "product #1", 5000); - product2 = new Product("product-2", "product #2", 3000); + product1 = new Product(new ProductId("product-1"), "product #1", 5000); + product2 = new Product(new ProductId("product-2"), "product #2", 3000); - productOption1 = "productOption-1"; - productOption2 = "productOption-2"; + productOption1 = new ProductOption("red", "M"); + productOption2 = new ProductOption("black", "L"); } @Test @@ -94,7 +94,7 @@ void totalQuantityCanNotOverLimit() { - private LineItem createLineItem(Product product, String productOption, int quantity) { + private LineItem createLineItem(Product product, ProductOption productOption, int quantity) { return new LineItem(product.getId(), productOption, quantity); } diff --git a/src/test/java/com/example/demo/model/LineItemIdTest.java b/src/test/java/com/example/demo/model/LineItemIdTest.java new file mode 100644 index 0000000..0883233 --- /dev/null +++ b/src/test/java/com/example/demo/model/LineItemIdTest.java @@ -0,0 +1,30 @@ +package com.example.demo.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class LineItemIdTest { + + @Test + @DisplayName("같은 값이면 같은 객체로 취급된다.") + void sameLineItemId() { + LineItemId id1 = new LineItemId("LineItem-1"); + LineItemId id2 = new LineItemId("LineItem-1"); + + assertThat(id1).isEqualTo(id2); + } + + @Test + @DisplayName("generate()를 사용하면 서로다른 아이디가 생성된다.") + void lineItemIdIsUnique() { + LineItemId id1 = LineItemId.generate(); + LineItemId id2 = LineItemId.generate(); + + assertThat(id1).isNotEqualTo(id2); + } + + +} diff --git a/src/test/java/com/example/demo/model/LineItemTest.java b/src/test/java/com/example/demo/model/LineItemTest.java index 2810c57..659e53f 100644 --- a/src/test/java/com/example/demo/model/LineItemTest.java +++ b/src/test/java/com/example/demo/model/LineItemTest.java @@ -1,5 +1,6 @@ package com.example.demo.model; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -8,18 +9,34 @@ class LineItemTest { @Test - void addQuantity() { - - String productId = "productId-1"; - String productOption = "productOption-1"; - int quantity = 5; + @DisplayName("같은 상품과 옵션일 경우, 수량을 추가할 수 있어야 한다.") + void isSameProduct(){ + ProductId productId = new ProductId("productId"); + ProductOption productOption = new ProductOption("red", "M"); + int quantity = 1; int delta = 5; - LineItem lineItem = new LineItem(productId, productOption, quantity); - lineItem.addQuantity(delta); + LineItem lineItem = new LineItem(productId, productOption, quantity); + lineItem.addQuantity(delta, productOption); + assertThat(lineItem.isSameProduct(productId, new ProductOption("red", "M"))).isTrue(); assertThat(lineItem.getQuantity()).isEqualTo(quantity + delta); } + @Test + @DisplayName("같은 상품이라도 옵션이 다르면, 수량은 추가되지 않는다.") + void sameProductAndDifferentOption(){ + ProductId productId = new ProductId("productId"); + ProductOption productOption = new ProductOption("red", "M"); + int quantity = 1; + int delta = 5; + LineItem lineItem = new LineItem(productId, productOption, quantity); + + ProductOption productOption2 = new ProductOption("black", "M"); + lineItem.addQuantity(delta, productOption2); + + assertThat(lineItem.isSameProduct(productId, productOption2)).isFalse(); + assertThat(lineItem.getQuantity()).isEqualTo(quantity); + } } diff --git a/src/test/java/com/example/demo/model/ProductIdTest.java b/src/test/java/com/example/demo/model/ProductIdTest.java new file mode 100644 index 0000000..e8361fa --- /dev/null +++ b/src/test/java/com/example/demo/model/ProductIdTest.java @@ -0,0 +1,20 @@ +package com.example.demo.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class ProductIdTest { + + @Test + @DisplayName("같은 값이면 같은 객체로 취급된다.") + void sameProductId() { + ProductId id1 = new ProductId("productId-1"); + ProductId id2 = new ProductId("productId-1"); + + assertThat(id1).isEqualTo(id2); + } + +} diff --git a/src/test/java/com/example/demo/model/ProductOptionTest.java b/src/test/java/com/example/demo/model/ProductOptionTest.java new file mode 100644 index 0000000..9f148df --- /dev/null +++ b/src/test/java/com/example/demo/model/ProductOptionTest.java @@ -0,0 +1,28 @@ +package com.example.demo.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProductOptionTest { + + @Test + @DisplayName("값이 같으면 같은 객체로 취급된다.") + void sameValue() { + ProductOption option1 = new ProductOption("red", "M"); + ProductOption option2 = new ProductOption("red", "M"); + + assertThat(option1).isEqualTo(option2); + } + + @Test + @DisplayName("값이 다르면 다른 객체로 취급된다.") + void differentValue() { + ProductOption option1 = new ProductOption("red", "M"); + ProductOption option2 = new ProductOption("black", "M"); + + assertThat(option1).isNotEqualTo(option2); + } + +} diff --git a/src/test/java/com/example/demo/model/ProductTest.java b/src/test/java/com/example/demo/model/ProductTest.java index 95f796f..6838595 100644 --- a/src/test/java/com/example/demo/model/ProductTest.java +++ b/src/test/java/com/example/demo/model/ProductTest.java @@ -11,7 +11,7 @@ class ProductTest { @BeforeEach void setUp() { - product = new Product("product-1", "Product #1", 5000); + product = new Product(new ProductId("product-1"), "Product #1", 5000); } @Test From ee4ab60d94fbce862b5de1cf76a9d00142abab92 Mon Sep 17 00:00:00 2001 From: nahye Date: Thu, 3 Apr 2025 23:22:45 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Cart=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90?= =?UTF-8?q?=20JPA=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4/=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC/=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 6 + .../example/demo/application/CartService.java | 54 ++++++ .../demo/controllers/CartController.java | 42 ++++- .../demo/controllers/LineItemController.java | 33 +++- .../demo/controllers/dto/CartResponseDto.java | 27 +++ .../controllers/dto/LineItemRequestDto.java | 31 ++++ .../controllers/dto/LineItemResponseDto.java | 32 ++++ .../java/com/example/demo/model/Cart.java | 34 +++- .../java/com/example/demo/model/LineItem.java | 42 ++++- .../com/example/demo/model/LineItemId.java | 3 + .../java/com/example/demo/model/Product.java | 8 + .../com/example/demo/model/ProductId.java | 3 + .../com/example/demo/model/ProductOption.java | 3 + .../demo/repository/CartRepository.java | 7 + .../demo/repository/ProductRepository.java | 8 + .../demo/application/CartServiceTest.java | 159 ++++++++++++++++++ .../demo/controllers/CartControllerTest.java | 33 +++- .../controllers/LineItemControllerTest.java | 40 ++++- 18 files changed, 547 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/example/demo/application/CartService.java create mode 100644 src/main/java/com/example/demo/controllers/dto/CartResponseDto.java create mode 100644 src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java create mode 100644 src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java create mode 100644 src/main/java/com/example/demo/repository/CartRepository.java create mode 100644 src/main/java/com/example/demo/repository/ProductRepository.java create mode 100644 src/test/java/com/example/demo/application/CartServiceTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 10a77c4..0ec3584 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,12 +16,18 @@ repositories { } dependencies { + implementation ("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") + compileOnly ("org.projectlombok:lombok") + annotationProcessor ("org.projectlombok:lombok") + developmentOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") + testCompileOnly ("org.projectlombok:lombok") + testAnnotationProcessor ("org.projectlombok:lombok") } tasks.withType { diff --git a/src/main/java/com/example/demo/application/CartService.java b/src/main/java/com/example/demo/application/CartService.java new file mode 100644 index 0000000..4975b07 --- /dev/null +++ b/src/main/java/com/example/demo/application/CartService.java @@ -0,0 +1,54 @@ +package com.example.demo.application; + +import com.example.demo.model.Cart; +import com.example.demo.model.LineItemId; +import com.example.demo.model.Product; +import com.example.demo.model.ProductId; +import com.example.demo.model.ProductOption; +import com.example.demo.repository.CartRepository; +import com.example.demo.repository.ProductRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class CartService { + + private final CartRepository cartRepository; + private final ProductRepository productRepository; + + public CartService(CartRepository cartRepository, ProductRepository productRepository) { + this.cartRepository = cartRepository; + this.productRepository = productRepository; + } + + public void addItemToCart(Long cartId, ProductId productId, ProductOption option, int quantity) { + Cart cart = cartRepository.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); + Product product = productRepository.findById(productId) + .orElseThrow(() -> new IllegalArgumentException("상품이 존재하지 않습니다.")); + + cart.addProduct(productId, option, quantity); + } + + public void removeItemFromCart(Long cartId, LineItemId lineItemId) { + Cart cart = cartRepository.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); + + cart.removeLineItemById(lineItemId); + } + + public void clearCart(Long cartId) { + Cart cart = cartRepository.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); + + cart.clearItems(); + } + + @Transactional(readOnly = true) + public Cart getCart(Long cartId) { + return cartRepository.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); + } + +} diff --git a/src/main/java/com/example/demo/controllers/CartController.java b/src/main/java/com/example/demo/controllers/CartController.java index 0e1469a..9ac7ba4 100644 --- a/src/main/java/com/example/demo/controllers/CartController.java +++ b/src/main/java/com/example/demo/controllers/CartController.java @@ -1,14 +1,48 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.controllers.dto.CartResponseDto; +import com.example.demo.controllers.dto.LineItemResponseDto; +import com.example.demo.model.Cart; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; 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; @RestController -@RequestMapping("/cart") +@RequestMapping("/carts") +@RequiredArgsConstructor public class CartController { - @GetMapping - String detail() { - return ""; + + private final CartService cartService; + + @GetMapping("/{cartId}") + public CartResponseDto getCart(@PathVariable Long cartId) { + + Cart cart = cartService.getCart(cartId); + + return new CartResponseDto( + cartId, + cart.getTotalQuantity(), + cart.getLineItems().stream() + .map(lineItem -> new LineItemResponseDto( + lineItem.getProductId().id(), + lineItem.getProductOption().color(), + lineItem.getProductOption().size(), + lineItem.getQuantity() + ) + ).toList() + ); + } + + + @DeleteMapping("/{cartId}") + public ResponseEntity deleteCart(@PathVariable Long cartId) { + cartService.clearCart(cartId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/demo/controllers/LineItemController.java b/src/main/java/com/example/demo/controllers/LineItemController.java index 20dc170..f693bc1 100644 --- a/src/main/java/com/example/demo/controllers/LineItemController.java +++ b/src/main/java/com/example/demo/controllers/LineItemController.java @@ -1,17 +1,44 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.controllers.dto.LineItemRequestDto; +import com.example.demo.model.LineItemId; +import com.example.demo.model.ProductId; +import com.example.demo.model.ProductOption; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/cart/line-items") +@RequestMapping("/carts/{cartId}/line-items") +@RequiredArgsConstructor public class LineItemController { + + private final CartService cartService; + @PostMapping @ResponseStatus(HttpStatus.CREATED) - void create() { - // + public void create(@PathVariable Long cartId, @RequestBody LineItemRequestDto requestDto) { + + cartService.addItemToCart( + cartId, + new ProductId(requestDto.getProductId()), + new ProductOption(requestDto.getColor(), requestDto.getSize()), + requestDto.getQuantity()); + } + + @DeleteMapping("/{lineItemId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteLineItem(@PathVariable Long cartId, @PathVariable String lineItemId) { + cartService.removeItemFromCart( + cartId, new LineItemId(lineItemId)); + } + } diff --git a/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java b/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java new file mode 100644 index 0000000..85381ff --- /dev/null +++ b/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java @@ -0,0 +1,27 @@ +package com.example.demo.controllers.dto; + +import java.util.List; + +public class CartResponseDto { + private Long cartId; + private int totalQuantity; + private List lineItems; + + public CartResponseDto(Long cartId, int totalQuantity, List lineItems) { + this.cartId = cartId; + this.totalQuantity = totalQuantity; + this.lineItems = lineItems; + } + + public Long getCartId() { + return cartId; + } + + public int getTotalQuantity() { + return totalQuantity; + } + + public List getLineItems() { + return lineItems; + } +} diff --git a/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java b/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java new file mode 100644 index 0000000..4aa133c --- /dev/null +++ b/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java @@ -0,0 +1,31 @@ +package com.example.demo.controllers.dto; + +public class LineItemRequestDto { + private String productId; + private String color; + private String size; + private int quantity; + + public LineItemRequestDto(String productId, String color, String size, int quantity) { + this.productId = productId; + this.color = color; + this.size = size; + this.quantity = quantity; + } + + public String getProductId() { + return productId; + } + + public String getColor() { + return color; + } + + public String getSize() { + return size; + } + + public int getQuantity() { + return quantity; + } +} diff --git a/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java b/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java new file mode 100644 index 0000000..2da4ab5 --- /dev/null +++ b/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java @@ -0,0 +1,32 @@ +package com.example.demo.controllers.dto; + +public class LineItemResponseDto { + + private String productId; + private String color; + private String size; + private int quantity; + + public LineItemResponseDto(String productId, String color, String size, int quantity) { + this.productId = productId; + this.color = color; + this.size = size; + this.quantity = quantity; + } + + public String getProductId() { + return productId; + } + + public String getColor() { + return color; + } + + public String getSize() { + return size; + } + + public int getQuantity() { + return quantity; + } +} diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java index d640ab7..45356b7 100644 --- a/src/main/java/com/example/demo/model/Cart.java +++ b/src/main/java/com/example/demo/model/Cart.java @@ -1,16 +1,36 @@ package com.example.demo.model; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + import java.util.ArrayList; import java.util.Collections; import java.util.List; +@Entity +@Table(name="carts") public class Cart { - private List lineItems; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true) + private List lineItems = new ArrayList<>(); + private int totalQuantity; + public Cart() { + } + public Cart(List lineItems) { this.lineItems = new ArrayList<>(lineItems); + lineItems.forEach(lineItem -> lineItem.setCart(this)); calculateTotalQuantity(); } @@ -24,7 +44,6 @@ public int getTotalQuantity() { public void addProduct(ProductId productId, ProductOption productOption, int quantity) { - LineItem lineItem = findLineItem(productId, productOption); if (lineItem != null) { @@ -36,6 +55,7 @@ public void addProduct(ProductId productId, ProductOption productOption, int qua lineItem = new LineItem(productId, productOption, quantity); lineItems.add(lineItem); + lineItem.setCart(this); calculateTotalQuantity(); checkValidQuantity(); } @@ -61,4 +81,14 @@ private LineItem findLineItem(ProductId productId, ProductOption productOption) .orElse(null); } + public void clearItems() { + lineItems.clear(); + calculateTotalQuantity(); + } + + public void removeLineItemById(LineItemId lineItemId) { + if (lineItemId == null) return; + lineItems.removeIf(lineItem ->lineItem.getId().equals(lineItemId)); + calculateTotalQuantity(); + } } diff --git a/src/main/java/com/example/demo/model/LineItem.java b/src/main/java/com/example/demo/model/LineItem.java index 79204f8..6033517 100644 --- a/src/main/java/com/example/demo/model/LineItem.java +++ b/src/main/java/com/example/demo/model/LineItem.java @@ -1,14 +1,34 @@ package com.example.demo.model; +import jakarta.persistence.Embedded; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +@Entity +@Table(name="line_items") public class LineItem { - private final LineItemId id; - private final ProductId productId; - private final ProductOption productOption; + @EmbeddedId + private LineItemId id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="cart_id") + private Cart cart; + + @Embedded + private ProductId productId; + + @Embedded + private ProductOption productOption; private int quantity; + protected LineItem() { + } public LineItem(ProductId productId, ProductOption productOption, int quantity) { this.id = LineItemId.generate(); @@ -17,6 +37,18 @@ public LineItem(ProductId productId, ProductOption productOption, int quantity) this.quantity = quantity; } + public LineItemId getId() { + return id; + } + + public ProductId getProductId() { + return productId; + } + + public ProductOption getProductOption() { + return productOption; + } + public int getQuantity() { return quantity; } @@ -32,4 +64,8 @@ public void addQuantity(int quantity, ProductOption productOption) { public boolean isSameProduct(ProductId productId, ProductOption productOption) { return this.productId.equals(productId) && this.productOption.equals(productOption); } + + public void setCart(Cart cart) { + this.cart = cart; + } } diff --git a/src/main/java/com/example/demo/model/LineItemId.java b/src/main/java/com/example/demo/model/LineItemId.java index c0e1f57..0d7de74 100644 --- a/src/main/java/com/example/demo/model/LineItemId.java +++ b/src/main/java/com/example/demo/model/LineItemId.java @@ -1,7 +1,10 @@ package com.example.demo.model; +import jakarta.persistence.Embeddable; + import java.util.UUID; +@Embeddable public record LineItemId( String id ) { diff --git a/src/main/java/com/example/demo/model/Product.java b/src/main/java/com/example/demo/model/Product.java index f0b49f1..4f9bb90 100644 --- a/src/main/java/com/example/demo/model/Product.java +++ b/src/main/java/com/example/demo/model/Product.java @@ -1,8 +1,16 @@ package com.example.demo.model; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +@Entity +@Table(name="products") public class Product { + @EmbeddedId private ProductId id; + private String name; private int price; diff --git a/src/main/java/com/example/demo/model/ProductId.java b/src/main/java/com/example/demo/model/ProductId.java index 4951e6b..958c0ac 100644 --- a/src/main/java/com/example/demo/model/ProductId.java +++ b/src/main/java/com/example/demo/model/ProductId.java @@ -1,5 +1,8 @@ package com.example.demo.model; +import jakarta.persistence.Embeddable; + +@Embeddable public record ProductId ( String id ){ diff --git a/src/main/java/com/example/demo/model/ProductOption.java b/src/main/java/com/example/demo/model/ProductOption.java index aa16fdb..28c6919 100644 --- a/src/main/java/com/example/demo/model/ProductOption.java +++ b/src/main/java/com/example/demo/model/ProductOption.java @@ -1,5 +1,8 @@ package com.example.demo.model; +import jakarta.persistence.Embeddable; + +@Embeddable public record ProductOption( String color, String size diff --git a/src/main/java/com/example/demo/repository/CartRepository.java b/src/main/java/com/example/demo/repository/CartRepository.java new file mode 100644 index 0000000..c1a665d --- /dev/null +++ b/src/main/java/com/example/demo/repository/CartRepository.java @@ -0,0 +1,7 @@ +package com.example.demo.repository; + +import com.example.demo.model.Cart; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CartRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/demo/repository/ProductRepository.java b/src/main/java/com/example/demo/repository/ProductRepository.java new file mode 100644 index 0000000..82a4216 --- /dev/null +++ b/src/main/java/com/example/demo/repository/ProductRepository.java @@ -0,0 +1,8 @@ +package com.example.demo.repository; + +import com.example.demo.model.Product; +import com.example.demo.model.ProductId; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProductRepository extends JpaRepository { +} diff --git a/src/test/java/com/example/demo/application/CartServiceTest.java b/src/test/java/com/example/demo/application/CartServiceTest.java new file mode 100644 index 0000000..2c48564 --- /dev/null +++ b/src/test/java/com/example/demo/application/CartServiceTest.java @@ -0,0 +1,159 @@ +package com.example.demo.application; + +import com.example.demo.model.*; +import com.example.demo.repository.CartRepository; +import com.example.demo.repository.ProductRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CartServiceTest { + + private CartRepository cartRepository; + private ProductRepository productRepository; + private CartService cartService; + + private Long cartId; + private ProductId productId; + private ProductOption option; + private Product product; + + private Cart cart; + + @BeforeEach + void setUp() { + cartRepository = mock(CartRepository.class); + productRepository = mock(ProductRepository.class); + cartService = new CartService(cartRepository, productRepository); + + cartId = 1L; + productId = new ProductId("product-1"); + option = new ProductOption("red", "M"); + product = new Product(productId, "Product #1", 5000); + + cart = new Cart(List.of()); + } + + + @Test + @DisplayName("상품을 추가할때, 장바구니가 존재하지 않으면 예외가 발생한다") + void cartNotFound() { + when(cartRepository.findById(cartId)).thenReturn(Optional.empty()); + + assertThatThrownBy(()->cartService.addItemToCart(cartId, productId, option, 1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("상품을 추가할때, 상품이 존재하지 않으면 예외가 발생한다") + void productNotFound() { + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + when(productRepository.findById(productId)).thenReturn(Optional.empty()); + + assertThatThrownBy(()->cartService.addItemToCart(cartId, productId, option, 1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("빈 장바구니에 상품을 추가하면, 전체 갯수는 상품의 갯수와 같다.") + void addProductToCart() { + //given + mockCartAndProduct(); + int quantity = 2; + + // when + cartService.addItemToCart(cartId, productId, option, quantity); + + // then + assertThat(cart.getLineItems()).hasSize(1); + assertThat(cart.getTotalQuantity()).isEqualTo(quantity); + } + + + @Test + @DisplayName("상품이 있는 장바구니에 다른 상품을 추가하면, LineItem이 추가되고 전체 수량은 합쳐진다") + void addAnotherProductToCart() { + // given + int existedQuantity = 2; + LineItem lineItem = new LineItem(productId, option, existedQuantity); + cart = new Cart( + List.of(lineItem) + ); + + mockCartAndProduct(); + + ProductId newProductId = new ProductId("product-2"); + Product newProduct = new Product(newProductId, "Product #2", 10000); + when(productRepository.findById(newProductId)).thenReturn(Optional.of(newProduct)); + + int newQuantity = 2; + + // when + cartService.addItemToCart(cartId, newProductId, option, newQuantity); + + // then + assertThat(cart.getLineItems()).hasSize(2); + assertThat(cart.getTotalQuantity()).isEqualTo(existedQuantity + newQuantity); + } + + @Test + @DisplayName("장바구니에서 LineItem을 삭제하면 해당 상품을 제거할수있다.") + void removeItemFromCart() { + // given + LineItem lineItem = new LineItem(productId, option, 1); + cart = new Cart(List.of(lineItem)); + + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + + // when + cartService.removeItemFromCart(cartId, lineItem.getId()); + + // then + assertThat(cart.getLineItems()).isEmpty(); + assertThat(cart.getTotalQuantity()).isZero(); + } + + @Test + @DisplayName("상품 수량이 20개가 초과하면 예외가 발생한다.") + void throwExceptionWhenTotalQuantityOverLimit() { + // given + mockCartAndProduct(); + cartService.addItemToCart(cartId, productId, option, 20); + + // when & then + assertThatThrownBy(()-> + cartService.addItemToCart(cartId, productId, option, 1) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("장바구니를 비우면 모든 상품이 제거가 된다.") + void clearCart() { + // given + LineItem lineItem = new LineItem(productId, option, 1); + cart = new Cart( + List.of(lineItem) + ); + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + + // when + cartService.clearCart(cartId); + + // then + assertThat(cart.getLineItems()).isEmpty(); + assertThat(cart.getTotalQuantity()).isZero(); + } + + private void mockCartAndProduct() { + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + when(productRepository.findById(productId)).thenReturn(Optional.of(product)); + } +} diff --git a/src/test/java/com/example/demo/controllers/CartControllerTest.java b/src/test/java/com/example/demo/controllers/CartControllerTest.java index 619307d..0638c22 100644 --- a/src/test/java/com/example/demo/controllers/CartControllerTest.java +++ b/src/test/java/com/example/demo/controllers/CartControllerTest.java @@ -1,12 +1,18 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.model.Cart; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(CartController.class) @@ -14,10 +20,29 @@ class CartControllerTest { @Autowired private MockMvc mockMvc; + @MockBean + private CartService cartService; + + @Test - @DisplayName("GET /cart") - void detail() throws Exception { - mockMvc.perform(get("/cart")) + @DisplayName("GET /carts/{cartId}") + void getCart() throws Exception { + Long cartId = 1L; + Cart cart = new Cart(); + when(cartService.getCart(cartId)).thenReturn(cart); + + mockMvc.perform(get("/carts/{cartId}", cartId)) .andExpect(status().isOk()); } + + @Test + @DisplayName("DELETE /carts/{cartId}") + void deleteCart() throws Exception { + Long cartId = 1L; + + mockMvc.perform(delete("/carts/{cartId}", cartId)) + .andExpect(status().isNoContent()); + + verify(cartService).clearCart(cartId); + } } diff --git a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java index 41eabbc..65baa2e 100644 --- a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java +++ b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java @@ -1,12 +1,20 @@ package com.example.demo.controllers; +import com.example.demo.application.CartService; +import com.example.demo.model.LineItemId; +import com.example.demo.model.ProductId; +import com.example.demo.model.ProductOption; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -15,19 +23,47 @@ class LineItemControllerTest { @Autowired private MockMvc mockMvc; + @MockBean + private CartService cartService; + @Test - @DisplayName("POST /cart/line-items") + @DisplayName("POST /carts/{cartId}/line-items") void addProduct() throws Exception { + Long cartId = 1L; String json = """ { "productId": "product-1", + "color": "blue", + "size": "M", "quantity": 2 } """; - mockMvc.perform(post("/cart/line-items") + mockMvc.perform(post("/carts/{cartId}/line-items", cartId) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isCreated()); + + verify(cartService).addItemToCart( + eq(cartId), + eq(new ProductId("product-1")), + eq(new ProductOption("blue", "M")), + eq(2) + ); + } + + @Test + @DisplayName("DELETE /carts/{cartId}/line-items/{lineItemId}") + void removeProduct() throws Exception { + Long cartId = 1L; + String lineItemId = "lineItem-1"; + + mockMvc.perform(delete("/carts/{cartId}/line-items/{lineItemId}", cartId, lineItemId)) + .andExpect(status().isNoContent()); + + verify(cartService).removeItemFromCart( + eq(cartId), + eq(new LineItemId(lineItemId)) + ); } } From 2ee1d4aa8936066a7b75d8ca71dd58e5e82d63e3 Mon Sep 17 00:00:00 2001 From: nahye Date: Tue, 8 Apr 2025 19:16:21 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -URI변경 후 컨트롤러 테스트 수정 -DTO record로 변경 -CartService 메서드 이름 수정 -CartTest 케이스 추가 -LineItem addQuantity 메서드 수정 -LineItemTest 케이스 수정 및 추가 -CartServiceTest는 삭제 --- build.gradle.kts | 3 - .../example/demo/application/CartService.java | 11 +- .../demo/controllers/CartController.java | 19 ++- .../demo/controllers/LineItemController.java | 27 ++- .../demo/controllers/dto/CartResponseDto.java | 27 +-- .../controllers/dto/LineItemRequestDto.java | 37 ++-- .../controllers/dto/LineItemResponseDto.java | 34 +--- .../java/com/example/demo/model/Cart.java | 29 ++-- .../java/com/example/demo/model/LineItem.java | 12 +- .../com/example/demo/model/LineItemId.java | 4 +- .../demo/application/CartServiceTest.java | 159 ------------------ .../demo/controllers/CartControllerTest.java | 16 +- .../controllers/LineItemControllerTest.java | 14 +- .../java/com/example/demo/model/CartTest.java | 70 ++++++-- .../example/demo/model/LineItemIdTest.java | 9 - .../com/example/demo/model/LineItemTest.java | 67 +++++--- 16 files changed, 185 insertions(+), 353 deletions(-) delete mode 100644 src/test/java/com/example/demo/application/CartServiceTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 0ec3584..49d9922 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,9 +20,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") - compileOnly ("org.projectlombok:lombok") - annotationProcessor ("org.projectlombok:lombok") - developmentOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/src/main/java/com/example/demo/application/CartService.java b/src/main/java/com/example/demo/application/CartService.java index 4975b07..f292408 100644 --- a/src/main/java/com/example/demo/application/CartService.java +++ b/src/main/java/com/example/demo/application/CartService.java @@ -2,7 +2,6 @@ import com.example.demo.model.Cart; import com.example.demo.model.LineItemId; -import com.example.demo.model.Product; import com.example.demo.model.ProductId; import com.example.demo.model.ProductOption; import com.example.demo.repository.CartRepository; @@ -25,17 +24,17 @@ public CartService(CartRepository cartRepository, ProductRepository productRepos public void addItemToCart(Long cartId, ProductId productId, ProductOption option, int quantity) { Cart cart = cartRepository.findById(cartId) .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); - Product product = productRepository.findById(productId) - .orElseThrow(() -> new IllegalArgumentException("상품이 존재하지 않습니다.")); - + if (!productRepository.existsById(productId)) { + throw new IllegalArgumentException("상품이 존재하지 않습니다."); + } cart.addProduct(productId, option, quantity); } - public void removeItemFromCart(Long cartId, LineItemId lineItemId) { + public void removeLineItem(Long cartId, LineItemId lineItemId) { Cart cart = cartRepository.findById(cartId) .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); - cart.removeLineItemById(lineItemId); + cart.removeLineItem(lineItemId); } public void clearCart(Long cartId) { diff --git a/src/main/java/com/example/demo/controllers/CartController.java b/src/main/java/com/example/demo/controllers/CartController.java index 9ac7ba4..266f8e0 100644 --- a/src/main/java/com/example/demo/controllers/CartController.java +++ b/src/main/java/com/example/demo/controllers/CartController.java @@ -4,24 +4,27 @@ import com.example.demo.controllers.dto.CartResponseDto; import com.example.demo.controllers.dto.LineItemResponseDto; import com.example.demo.model.Cart; -import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; 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; @RestController -@RequestMapping("/carts") -@RequiredArgsConstructor +@RequestMapping("/cart") public class CartController { + private Long cartId = 1L; + + public CartController(CartService cartService) { + this.cartService = cartService; + } + private final CartService cartService; - @GetMapping("/{cartId}") - public CartResponseDto getCart(@PathVariable Long cartId) { + @GetMapping + public CartResponseDto getCart() { Cart cart = cartService.getCart(cartId); @@ -40,8 +43,8 @@ public CartResponseDto getCart(@PathVariable Long cartId) { } - @DeleteMapping("/{cartId}") - public ResponseEntity deleteCart(@PathVariable Long cartId) { + @DeleteMapping() + public ResponseEntity deleteCart() { cartService.clearCart(cartId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/example/demo/controllers/LineItemController.java b/src/main/java/com/example/demo/controllers/LineItemController.java index f693bc1..ed76a55 100644 --- a/src/main/java/com/example/demo/controllers/LineItemController.java +++ b/src/main/java/com/example/demo/controllers/LineItemController.java @@ -5,7 +5,7 @@ import com.example.demo.model.LineItemId; import com.example.demo.model.ProductId; import com.example.demo.model.ProductOption; -import lombok.RequiredArgsConstructor; +import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -15,29 +15,38 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import java.util.UUID; + @RestController -@RequestMapping("/carts/{cartId}/line-items") -@RequiredArgsConstructor +@RequestMapping("/cart/line-items") public class LineItemController { + public LineItemController(CartService cartService) { + this.cartService = cartService; + } + private final CartService cartService; + private Long cartId =1L; + @PostMapping @ResponseStatus(HttpStatus.CREATED) - public void create(@PathVariable Long cartId, @RequestBody LineItemRequestDto requestDto) { + public void create( + @Valid @RequestBody LineItemRequestDto requestDto) { cartService.addItemToCart( cartId, - new ProductId(requestDto.getProductId()), - new ProductOption(requestDto.getColor(), requestDto.getSize()), - requestDto.getQuantity()); + new ProductId(requestDto.productId()), + new ProductOption(requestDto.color(), requestDto.size()), + requestDto.quantity()); } @DeleteMapping("/{lineItemId}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteLineItem(@PathVariable Long cartId, @PathVariable String lineItemId) { - cartService.removeItemFromCart( + public void deleteLineItem(@PathVariable UUID lineItemId) { + + cartService.removeLineItem( cartId, new LineItemId(lineItemId)); } diff --git a/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java b/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java index 85381ff..bde9bef 100644 --- a/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java +++ b/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java @@ -2,26 +2,9 @@ import java.util.List; -public class CartResponseDto { - private Long cartId; - private int totalQuantity; - private List lineItems; - - public CartResponseDto(Long cartId, int totalQuantity, List lineItems) { - this.cartId = cartId; - this.totalQuantity = totalQuantity; - this.lineItems = lineItems; - } - - public Long getCartId() { - return cartId; - } - - public int getTotalQuantity() { - return totalQuantity; - } - - public List getLineItems() { - return lineItems; - } +public record CartResponseDto( + Long cartId, + int totalQuantity, + List lineItems +) { } diff --git a/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java b/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java index 4aa133c..466dca0 100644 --- a/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java +++ b/src/main/java/com/example/demo/controllers/dto/LineItemRequestDto.java @@ -1,31 +1,22 @@ package com.example.demo.controllers.dto; -public class LineItemRequestDto { - private String productId; - private String color; - private String size; - private int quantity; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; - public LineItemRequestDto(String productId, String color, String size, int quantity) { - this.productId = productId; - this.color = color; - this.size = size; - this.quantity = quantity; - } +public record LineItemRequestDto( - public String getProductId() { - return productId; - } + @NotBlank(message = "상품 ID는 필수입니다.") + String productId, - public String getColor() { - return color; - } + @NotBlank(message = "색상은 필수입니다.") + String color, - public String getSize() { - return size; - } + @NotBlank(message = "사이즈는 필수입니다.") + String size, - public int getQuantity() { - return quantity; - } + @Min(value = 1, message = "최소 수량은 1개입니다.") + @Max(value = 20, message = "최대 수량은 20개입니다.") + int quantity +) { } diff --git a/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java b/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java index 2da4ab5..525fadf 100644 --- a/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java +++ b/src/main/java/com/example/demo/controllers/dto/LineItemResponseDto.java @@ -1,32 +1,10 @@ package com.example.demo.controllers.dto; -public class LineItemResponseDto { +public record LineItemResponseDto( + String productId, + String color, + String size, + int quantity +) { - private String productId; - private String color; - private String size; - private int quantity; - - public LineItemResponseDto(String productId, String color, String size, int quantity) { - this.productId = productId; - this.color = color; - this.size = size; - this.quantity = quantity; - } - - public String getProductId() { - return productId; - } - - public String getColor() { - return color; - } - - public String getSize() { - return size; - } - - public int getQuantity() { - return quantity; - } } diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java index 45356b7..1c6f79e 100644 --- a/src/main/java/com/example/demo/model/Cart.java +++ b/src/main/java/com/example/demo/model/Cart.java @@ -30,8 +30,7 @@ public Cart() { public Cart(List lineItems) { this.lineItems = new ArrayList<>(lineItems); - lineItems.forEach(lineItem -> lineItem.setCart(this)); - calculateTotalQuantity(); + updateTotalQuantity(); } public List getLineItems() { @@ -47,29 +46,24 @@ public void addProduct(ProductId productId, ProductOption productOption, int qua LineItem lineItem = findLineItem(productId, productOption); if (lineItem != null) { - lineItem.addQuantity(quantity, productOption); - calculateTotalQuantity(); - checkValidQuantity(); + lineItem.addQuantity(quantity); + updateTotalQuantity(); return; } lineItem = new LineItem(productId, productOption, quantity); lineItems.add(lineItem); - lineItem.setCart(this); - calculateTotalQuantity(); - checkValidQuantity(); + updateTotalQuantity(); } - public void checkValidQuantity() { - if (totalQuantity > 20) { - throw new IllegalArgumentException("담을수 있는 수량을 초과했습니다."); - } - } - private void calculateTotalQuantity() { + private void updateTotalQuantity() { this.totalQuantity = lineItems.stream() .mapToInt(LineItem::getQuantity) .sum(); + if (totalQuantity > 20) { + throw new IllegalArgumentException("담을수 있는 수량을 초과했습니다."); + } } @@ -83,12 +77,11 @@ private LineItem findLineItem(ProductId productId, ProductOption productOption) public void clearItems() { lineItems.clear(); - calculateTotalQuantity(); + updateTotalQuantity(); } - public void removeLineItemById(LineItemId lineItemId) { - if (lineItemId == null) return; + public void removeLineItem(LineItemId lineItemId) { lineItems.removeIf(lineItem ->lineItem.getId().equals(lineItemId)); - calculateTotalQuantity(); + updateTotalQuantity(); } } diff --git a/src/main/java/com/example/demo/model/LineItem.java b/src/main/java/com/example/demo/model/LineItem.java index 6033517..65d8ddc 100644 --- a/src/main/java/com/example/demo/model/LineItem.java +++ b/src/main/java/com/example/demo/model/LineItem.java @@ -15,10 +15,6 @@ public class LineItem { @EmbeddedId private LineItemId id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name="cart_id") - private Cart cart; - @Embedded private ProductId productId; @@ -54,10 +50,7 @@ public int getQuantity() { } - public void addQuantity(int quantity, ProductOption productOption) { - if(!this.productOption.equals(productOption)) { - return; - } + public void addQuantity(int quantity) { this.quantity += quantity; } @@ -65,7 +58,4 @@ public boolean isSameProduct(ProductId productId, ProductOption productOption) { return this.productId.equals(productId) && this.productOption.equals(productOption); } - public void setCart(Cart cart) { - this.cart = cart; - } } diff --git a/src/main/java/com/example/demo/model/LineItemId.java b/src/main/java/com/example/demo/model/LineItemId.java index 0d7de74..f63c0f8 100644 --- a/src/main/java/com/example/demo/model/LineItemId.java +++ b/src/main/java/com/example/demo/model/LineItemId.java @@ -6,10 +6,10 @@ @Embeddable public record LineItemId( - String id + UUID id ) { public static LineItemId generate() { - return new LineItemId("lineItemId-" + UUID.randomUUID()); + return new LineItemId(UUID.randomUUID()); } } diff --git a/src/test/java/com/example/demo/application/CartServiceTest.java b/src/test/java/com/example/demo/application/CartServiceTest.java deleted file mode 100644 index 2c48564..0000000 --- a/src/test/java/com/example/demo/application/CartServiceTest.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.example.demo.application; - -import com.example.demo.model.*; -import com.example.demo.repository.CartRepository; -import com.example.demo.repository.ProductRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class CartServiceTest { - - private CartRepository cartRepository; - private ProductRepository productRepository; - private CartService cartService; - - private Long cartId; - private ProductId productId; - private ProductOption option; - private Product product; - - private Cart cart; - - @BeforeEach - void setUp() { - cartRepository = mock(CartRepository.class); - productRepository = mock(ProductRepository.class); - cartService = new CartService(cartRepository, productRepository); - - cartId = 1L; - productId = new ProductId("product-1"); - option = new ProductOption("red", "M"); - product = new Product(productId, "Product #1", 5000); - - cart = new Cart(List.of()); - } - - - @Test - @DisplayName("상품을 추가할때, 장바구니가 존재하지 않으면 예외가 발생한다") - void cartNotFound() { - when(cartRepository.findById(cartId)).thenReturn(Optional.empty()); - - assertThatThrownBy(()->cartService.addItemToCart(cartId, productId, option, 1)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("상품을 추가할때, 상품이 존재하지 않으면 예외가 발생한다") - void productNotFound() { - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); - when(productRepository.findById(productId)).thenReturn(Optional.empty()); - - assertThatThrownBy(()->cartService.addItemToCart(cartId, productId, option, 1)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("빈 장바구니에 상품을 추가하면, 전체 갯수는 상품의 갯수와 같다.") - void addProductToCart() { - //given - mockCartAndProduct(); - int quantity = 2; - - // when - cartService.addItemToCart(cartId, productId, option, quantity); - - // then - assertThat(cart.getLineItems()).hasSize(1); - assertThat(cart.getTotalQuantity()).isEqualTo(quantity); - } - - - @Test - @DisplayName("상품이 있는 장바구니에 다른 상품을 추가하면, LineItem이 추가되고 전체 수량은 합쳐진다") - void addAnotherProductToCart() { - // given - int existedQuantity = 2; - LineItem lineItem = new LineItem(productId, option, existedQuantity); - cart = new Cart( - List.of(lineItem) - ); - - mockCartAndProduct(); - - ProductId newProductId = new ProductId("product-2"); - Product newProduct = new Product(newProductId, "Product #2", 10000); - when(productRepository.findById(newProductId)).thenReturn(Optional.of(newProduct)); - - int newQuantity = 2; - - // when - cartService.addItemToCart(cartId, newProductId, option, newQuantity); - - // then - assertThat(cart.getLineItems()).hasSize(2); - assertThat(cart.getTotalQuantity()).isEqualTo(existedQuantity + newQuantity); - } - - @Test - @DisplayName("장바구니에서 LineItem을 삭제하면 해당 상품을 제거할수있다.") - void removeItemFromCart() { - // given - LineItem lineItem = new LineItem(productId, option, 1); - cart = new Cart(List.of(lineItem)); - - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); - - // when - cartService.removeItemFromCart(cartId, lineItem.getId()); - - // then - assertThat(cart.getLineItems()).isEmpty(); - assertThat(cart.getTotalQuantity()).isZero(); - } - - @Test - @DisplayName("상품 수량이 20개가 초과하면 예외가 발생한다.") - void throwExceptionWhenTotalQuantityOverLimit() { - // given - mockCartAndProduct(); - cartService.addItemToCart(cartId, productId, option, 20); - - // when & then - assertThatThrownBy(()-> - cartService.addItemToCart(cartId, productId, option, 1) - ).isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("장바구니를 비우면 모든 상품이 제거가 된다.") - void clearCart() { - // given - LineItem lineItem = new LineItem(productId, option, 1); - cart = new Cart( - List.of(lineItem) - ); - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); - - // when - cartService.clearCart(cartId); - - // then - assertThat(cart.getLineItems()).isEmpty(); - assertThat(cart.getTotalQuantity()).isZero(); - } - - private void mockCartAndProduct() { - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); - when(productRepository.findById(productId)).thenReturn(Optional.of(product)); - } -} diff --git a/src/test/java/com/example/demo/controllers/CartControllerTest.java b/src/test/java/com/example/demo/controllers/CartControllerTest.java index 0638c22..2bc21c0 100644 --- a/src/test/java/com/example/demo/controllers/CartControllerTest.java +++ b/src/test/java/com/example/demo/controllers/CartControllerTest.java @@ -8,11 +8,11 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(CartController.class) @@ -23,24 +23,24 @@ class CartControllerTest { @MockBean private CartService cartService; - @Test - @DisplayName("GET /carts/{cartId}") + @DisplayName("GET /cart") void getCart() throws Exception { - Long cartId = 1L; Cart cart = new Cart(); + Long cartId = 1L; when(cartService.getCart(cartId)).thenReturn(cart); - mockMvc.perform(get("/carts/{cartId}", cartId)) + mockMvc.perform(get("/cart")) .andExpect(status().isOk()); } @Test - @DisplayName("DELETE /carts/{cartId}") + @DisplayName("DELETE /cart") void deleteCart() throws Exception { + Long cartId = 1L; - mockMvc.perform(delete("/carts/{cartId}", cartId)) + mockMvc.perform(delete("/cart")) .andExpect(status().isNoContent()); verify(cartService).clearCart(cartId); diff --git a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java index 65baa2e..5d21589 100644 --- a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java +++ b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java @@ -12,6 +12,8 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import java.util.UUID; + import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -27,7 +29,7 @@ class LineItemControllerTest { private CartService cartService; @Test - @DisplayName("POST /carts/{cartId}/line-items") + @DisplayName("POST /cart/line-items") void addProduct() throws Exception { Long cartId = 1L; String json = """ @@ -39,7 +41,7 @@ void addProduct() throws Exception { } """; - mockMvc.perform(post("/carts/{cartId}/line-items", cartId) + mockMvc.perform(post("/cart/line-items") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isCreated()); @@ -53,15 +55,15 @@ void addProduct() throws Exception { } @Test - @DisplayName("DELETE /carts/{cartId}/line-items/{lineItemId}") + @DisplayName("DELETE /cart/line-items/{lineItemId}") void removeProduct() throws Exception { Long cartId = 1L; - String lineItemId = "lineItem-1"; + UUID lineItemId = UUID.randomUUID(); - mockMvc.perform(delete("/carts/{cartId}/line-items/{lineItemId}", cartId, lineItemId)) + mockMvc.perform(delete("/cart/line-items/{lineItemId}", lineItemId)) .andExpect(status().isNoContent()); - verify(cartService).removeItemFromCart( + verify(cartService).removeLineItem( eq(cartId), eq(new LineItemId(lineItemId)) ); diff --git a/src/test/java/com/example/demo/model/CartTest.java b/src/test/java/com/example/demo/model/CartTest.java index 972473e..e1bdb4c 100644 --- a/src/test/java/com/example/demo/model/CartTest.java +++ b/src/test/java/com/example/demo/model/CartTest.java @@ -30,12 +30,12 @@ void setUp() { void cartTotalQuantityIsZero() { Cart cart = new Cart(List.of()); - assertThat(cart.getTotalQuantity()).isEqualTo(0); + assertThat(cart.getTotalQuantity()).isZero(); } @Test @DisplayName("빈 장바구니에 물건 추가하면 장바구니의 전체수량은 추가한 수량과 같다.") - void addProduct() { + void addProductToEmptyCart() { Cart cart = new Cart(List.of()); int quantity = 1; @@ -49,11 +49,11 @@ void addProduct() { void addExistingProduct() { int oldQuantity = 1; - Cart cart = new Cart(List.of( - createLineItem(product1, productOption1, oldQuantity) - )); - int newQuantity = 1; + + Cart cart = new Cart(); + + cart.addProduct(product1.getId(), productOption1, oldQuantity); cart.addProduct(product1.getId(), productOption1, newQuantity); assertThat(cart.getLineItems()).hasSize(1); @@ -61,41 +61,75 @@ void addExistingProduct() { } @Test - @DisplayName("장바구니에 새로운 있는 물건 추가하면 전체 수량은 이미 있던 물건의 수량과 " + - "새로 추가하는 물건의 수량의 합이다.") - void addNewProduct() { + @DisplayName("같은 상품의 옵션이 다른 경우를 추가할때, 장바구니의 전체수량은 이미 있던 수량과 새로 추가하는 수량의 합이다. ") + void addSameProductAndDifferentOption() { int oldQuantity = 1; - Cart cart = new Cart(List.of( - createLineItem(product2, productOption2, oldQuantity) - )); + int newQuantity = 1; + Cart cart = new Cart(); + + cart.addProduct(product1.getId(), productOption1, oldQuantity); + cart.addProduct(product1.getId(), productOption2, newQuantity); + + assertThat(cart.getLineItems()).hasSize(2); + assertThat(cart.getTotalQuantity()).isEqualTo(oldQuantity + newQuantity); + } + + @Test + @DisplayName("상품의 옵션이 모두 다른 경우를 추가할때, 장바구니의 전체수량은 이미 있던 수량과 새로 추가하는 수량의 합이다. ") + void addDifferentProductAndDifferentOption() { + int oldQuantity = 1; int newQuantity = 1; - cart.addProduct(product1.getId(), productOption1, newQuantity); + Cart cart = new Cart(); + + cart.addProduct(product1.getId(), productOption1, oldQuantity); + cart.addProduct(product2.getId(), productOption2, newQuantity); assertThat(cart.getLineItems()).hasSize(2); assertThat(cart.getTotalQuantity()).isEqualTo(oldQuantity + newQuantity); } + @Test @DisplayName("전체 장바구니의 수량이 20개가 넘어가면 예외가 발생한다.") void totalQuantityCanNotOverLimit() { - Cart cart = new Cart(List.of( - createLineItem(product1, productOption1, 19) - )); + Cart cart = new Cart(); int newQuantity = 5; + cart.addProduct(product1.getId(), productOption1, 20); + assertThatThrownBy( () -> cart.addProduct(product1.getId(), productOption1, newQuantity) ).isInstanceOf(IllegalArgumentException.class); + } + @Test + @DisplayName("장바구니를 비우면 전체수량이 0이 된다.") + void clearCart() { + Cart cart = new Cart(); + cart.addProduct(product1.getId(), productOption1, 1); + cart.addProduct(product2.getId(), productOption2, 2); + + cart.clearItems(); + + assertThat(cart.getLineItems()).isEmpty(); + assertThat(cart.getTotalQuantity()).isZero(); } + @Test + @DisplayName("장바구니에서 LineItem을 삭제하면 해당 상품을 제거할수있다.") + void removeLineItem() { + Cart cart = new Cart(); + cart.addProduct(product1.getId(), productOption1, 1); + cart.addProduct(product2.getId(), productOption2, 2); + LineItem lineItem = cart.getLineItems().get(0); + cart.removeLineItem(lineItem.getId()); - private LineItem createLineItem(Product product, ProductOption productOption, int quantity) { - return new LineItem(product.getId(), productOption, quantity); + assertThat(cart.getLineItems()).hasSize(1); + assertThat(cart.getTotalQuantity()).isEqualTo(2); } } diff --git a/src/test/java/com/example/demo/model/LineItemIdTest.java b/src/test/java/com/example/demo/model/LineItemIdTest.java index 0883233..5574c98 100644 --- a/src/test/java/com/example/demo/model/LineItemIdTest.java +++ b/src/test/java/com/example/demo/model/LineItemIdTest.java @@ -8,15 +8,6 @@ class LineItemIdTest { - @Test - @DisplayName("같은 값이면 같은 객체로 취급된다.") - void sameLineItemId() { - LineItemId id1 = new LineItemId("LineItem-1"); - LineItemId id2 = new LineItemId("LineItem-1"); - - assertThat(id1).isEqualTo(id2); - } - @Test @DisplayName("generate()를 사용하면 서로다른 아이디가 생성된다.") void lineItemIdIsUnique() { diff --git a/src/test/java/com/example/demo/model/LineItemTest.java b/src/test/java/com/example/demo/model/LineItemTest.java index 659e53f..d90ddf9 100644 --- a/src/test/java/com/example/demo/model/LineItemTest.java +++ b/src/test/java/com/example/demo/model/LineItemTest.java @@ -1,5 +1,6 @@ package com.example.demo.model; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -8,35 +9,55 @@ class LineItemTest { + private ProductId productId; + private ProductOption productOption; + + @BeforeEach + void setUp() { + productId = new ProductId("product-1"); + productOption = new ProductOption("red", "M"); + } + @Test - @DisplayName("같은 상품과 옵션일 경우, 수량을 추가할 수 있어야 한다.") - void isSameProduct(){ - ProductId productId = new ProductId("productId"); - ProductOption productOption = new ProductOption("red", "M"); - int quantity = 1; - int delta = 5; + @DisplayName("수량을 증가시키면 현재 수량에 더해진다.") + void addQuantity() { + + int oldQuantity = 2; + LineItem lineItem = new LineItem(productId, productOption, oldQuantity); + int newQuantity = 3; - LineItem lineItem = new LineItem(productId, productOption, quantity); - lineItem.addQuantity(delta, productOption); + lineItem.addQuantity(newQuantity); + + assertThat(lineItem.getQuantity()).isEqualTo(oldQuantity + newQuantity); + } + + @Test + @DisplayName("상품ID와 옵션이 같으면 같은 라인아이템이다.") + void whenSameProductIdAndProductOption() { + LineItem lineItem = new LineItem(productId, productOption, 1); - assertThat(lineItem.isSameProduct(productId, new ProductOption("red", "M"))).isTrue(); - assertThat(lineItem.getQuantity()).isEqualTo(quantity + delta); + boolean result = lineItem.isSameProduct(new ProductId("product-1"), new ProductOption("red", "M")); + assertThat(result).isTrue(); } @Test - @DisplayName("같은 상품이라도 옵션이 다르면, 수량은 추가되지 않는다.") - void sameProductAndDifferentOption(){ - ProductId productId = new ProductId("productId"); - ProductOption productOption = new ProductOption("red", "M"); - int quantity = 1; - int delta = 5; - LineItem lineItem = new LineItem(productId, productOption, quantity); - - ProductOption productOption2 = new ProductOption("black", "M"); - lineItem.addQuantity(delta, productOption2); - - assertThat(lineItem.isSameProduct(productId, productOption2)).isFalse(); - assertThat(lineItem.getQuantity()).isEqualTo(quantity); + @DisplayName("상품ID는 같지만 옵션이 다르면 다른 라인아이템이다.") + void whenSameProductIdAndDiffrentProductOption() { + LineItem lineItem = new LineItem(productId, productOption, 1); + + boolean result = lineItem.isSameProduct(new ProductId("product-1"), new ProductOption("black", "L")); + + assertThat(result).isFalse(); + } + + @Test + @DisplayName("상품ID가 다르면 다른 라인아이템이다.") + void whenDifferentProductId() { + LineItem lineItem = new LineItem(productId, productOption, 1); + + boolean result = lineItem.isSameProduct(new ProductId("product-2"), new ProductOption("red", "M")); + + assertThat(result).isFalse(); } } From cd431d96d1ecdb1b4cd80b4f00d8f3ca33ac25fb Mon Sep 17 00:00:00 2001 From: nahye Date: Fri, 11 Apr 2025 17:21:49 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cartId 세션에서 가져오기 - cartServiceTest 추가 --- .../example/demo/application/CartService.java | 40 +++--- .../demo/controllers/CartController.java | 7 +- .../demo/controllers/LineItemController.java | 8 +- .../demo/controllers/dto/CartResponseDto.java | 1 - .../java/com/example/demo/model/Cart.java | 4 + .../com/example/demo/model/LineItemId.java | 4 +- .../demo/application/CartServiceTest.java | 128 ++++++++++++++++++ .../demo/controllers/CartControllerTest.java | 8 +- .../controllers/LineItemControllerTest.java | 6 +- 9 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 src/test/java/com/example/demo/application/CartServiceTest.java diff --git a/src/main/java/com/example/demo/application/CartService.java b/src/main/java/com/example/demo/application/CartService.java index f292408..1716b4d 100644 --- a/src/main/java/com/example/demo/application/CartService.java +++ b/src/main/java/com/example/demo/application/CartService.java @@ -6,6 +6,7 @@ import com.example.demo.model.ProductOption; import com.example.demo.repository.CartRepository; import com.example.demo.repository.ProductRepository; +import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,38 +17,43 @@ public class CartService { private final CartRepository cartRepository; private final ProductRepository productRepository; - public CartService(CartRepository cartRepository, ProductRepository productRepository) { + private final HttpSession session; + + public CartService(CartRepository cartRepository, ProductRepository productRepository, HttpSession session) { this.cartRepository = cartRepository; this.productRepository = productRepository; + this.session = session; } - public void addItemToCart(Long cartId, ProductId productId, ProductOption option, int quantity) { - Cart cart = cartRepository.findById(cartId) - .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); + public void addItemToCart(ProductId productId, ProductOption option, int quantity) { + Cart cart = getCart(); if (!productRepository.existsById(productId)) { throw new IllegalArgumentException("상품이 존재하지 않습니다."); } cart.addProduct(productId, option, quantity); } - public void removeLineItem(Long cartId, LineItemId lineItemId) { - Cart cart = cartRepository.findById(cartId) - .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); - + public void removeLineItem(LineItemId lineItemId) { + Cart cart = getCart(); cart.removeLineItem(lineItemId); } - public void clearCart(Long cartId) { - Cart cart = cartRepository.findById(cartId) - .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); - + public void clearCart() { + Cart cart = getCart(); cart.clearItems(); } - @Transactional(readOnly = true) - public Cart getCart(Long cartId) { - return cartRepository.findById(cartId) - .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); - } + public Cart getCart() { + Long cartId = (Long) session.getAttribute("CART_ID"); + if (cartId != null) { + return cartRepository.findById(cartId) + .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); + } + + Cart cart = new Cart(); + Cart savedCart = cartRepository.save(cart); + session.setAttribute("CART_ID", savedCart.getId()); + return savedCart; + } } diff --git a/src/main/java/com/example/demo/controllers/CartController.java b/src/main/java/com/example/demo/controllers/CartController.java index 266f8e0..abb058d 100644 --- a/src/main/java/com/example/demo/controllers/CartController.java +++ b/src/main/java/com/example/demo/controllers/CartController.java @@ -15,8 +15,6 @@ @RequestMapping("/cart") public class CartController { - private Long cartId = 1L; - public CartController(CartService cartService) { this.cartService = cartService; } @@ -26,10 +24,9 @@ public CartController(CartService cartService) { @GetMapping public CartResponseDto getCart() { - Cart cart = cartService.getCart(cartId); + Cart cart = cartService.getCart(); return new CartResponseDto( - cartId, cart.getTotalQuantity(), cart.getLineItems().stream() .map(lineItem -> new LineItemResponseDto( @@ -45,7 +42,7 @@ public CartResponseDto getCart() { @DeleteMapping() public ResponseEntity deleteCart() { - cartService.clearCart(cartId); + cartService.clearCart(); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/src/main/java/com/example/demo/controllers/LineItemController.java b/src/main/java/com/example/demo/controllers/LineItemController.java index ed76a55..86bcff7 100644 --- a/src/main/java/com/example/demo/controllers/LineItemController.java +++ b/src/main/java/com/example/demo/controllers/LineItemController.java @@ -15,7 +15,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import java.util.UUID; @RestController @RequestMapping("/cart/line-items") @@ -27,15 +26,12 @@ public LineItemController(CartService cartService) { private final CartService cartService; - private Long cartId =1L; - @PostMapping @ResponseStatus(HttpStatus.CREATED) public void create( @Valid @RequestBody LineItemRequestDto requestDto) { cartService.addItemToCart( - cartId, new ProductId(requestDto.productId()), new ProductOption(requestDto.color(), requestDto.size()), requestDto.quantity()); @@ -44,10 +40,10 @@ public void create( @DeleteMapping("/{lineItemId}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteLineItem(@PathVariable UUID lineItemId) { + public void deleteLineItem(@PathVariable String lineItemId) { cartService.removeLineItem( - cartId, new LineItemId(lineItemId)); + new LineItemId(lineItemId)); } } diff --git a/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java b/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java index bde9bef..29c9ebd 100644 --- a/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java +++ b/src/main/java/com/example/demo/controllers/dto/CartResponseDto.java @@ -3,7 +3,6 @@ import java.util.List; public record CartResponseDto( - Long cartId, int totalQuantity, List lineItems ) { diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java index 1c6f79e..dc570a6 100644 --- a/src/main/java/com/example/demo/model/Cart.java +++ b/src/main/java/com/example/demo/model/Cart.java @@ -33,6 +33,10 @@ public Cart(List lineItems) { updateTotalQuantity(); } + public Long getId() { + return id; + } + public List getLineItems() { return Collections.unmodifiableList(lineItems); } diff --git a/src/main/java/com/example/demo/model/LineItemId.java b/src/main/java/com/example/demo/model/LineItemId.java index f63c0f8..8437fd4 100644 --- a/src/main/java/com/example/demo/model/LineItemId.java +++ b/src/main/java/com/example/demo/model/LineItemId.java @@ -6,10 +6,10 @@ @Embeddable public record LineItemId( - UUID id + String id ) { public static LineItemId generate() { - return new LineItemId(UUID.randomUUID()); + return new LineItemId(UUID.randomUUID().toString()); } } diff --git a/src/test/java/com/example/demo/application/CartServiceTest.java b/src/test/java/com/example/demo/application/CartServiceTest.java new file mode 100644 index 0000000..a3b5d0d --- /dev/null +++ b/src/test/java/com/example/demo/application/CartServiceTest.java @@ -0,0 +1,128 @@ +package com.example.demo.application; + +import com.example.demo.model.Cart; +import com.example.demo.model.LineItemId; +import com.example.demo.model.ProductId; +import com.example.demo.model.ProductOption; +import com.example.demo.repository.CartRepository; +import com.example.demo.repository.ProductRepository; +import jakarta.servlet.http.HttpSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class CartServiceTest { + + private CartRepository cartRepository; + private ProductRepository productRepository; + private HttpSession session; + private CartService cartService; + private Cart cart; + + @BeforeEach + void setUp() { + cartRepository = mock(CartRepository.class); + productRepository = mock(ProductRepository.class); + session = mock(HttpSession.class); + cartService = new CartService(cartRepository, productRepository, session); + cart = mock(Cart.class); + } + + @DisplayName("세션에 CART_ID가 존재하면 해당 ID로 장바구니를 조회한다") + @Test + void getExistingCartId() { + // given + Long existingCartId = 123L; + + when(session.getAttribute("CART_ID")).thenReturn(existingCartId); + when(cartRepository.findById(existingCartId)).thenReturn(Optional.of(cart)); + + // when + Cart result = cartService.getCart(); + + // then + assertThat(result).isSameAs(cart); + verify(session).getAttribute("CART_ID"); + verify(cartRepository).findById(existingCartId); + } + + @DisplayName("세션에 CART_ID가 없으면 새 장바구니를 생성하고 세션에 저장한다 ") + @Test + void getNonExistingCartId() { + // given + Long newCartId = 456L; + + when(session.getAttribute("CART_ID")).thenReturn(null); + when(cartRepository.save(any(Cart.class))).thenReturn(cart); + when(cart.getId()).thenReturn(newCartId); + + // when + Cart result = cartService.getCart(); + + // then + assertThat(result).isSameAs(cart); + verify(session).setAttribute("CART_ID", newCartId); + } + + @DisplayName("상품을 장바구니에 추가한다") + @Test + void addItemToCart_AddsProductToCart() { + // given + Long cartId = 123L; + ProductId productId = new ProductId("product-1"); + ProductOption option = new ProductOption("Red", "L"); + int quantity = 2; + + when(session.getAttribute("CART_ID")).thenReturn(cartId); + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + when(productRepository.existsById(productId)).thenReturn(true); + + // when + cartService.addItemToCart(productId, option, quantity); + + // then + verify(cart).addProduct(productId, option, quantity); + } + + @DisplayName("장바구니를 비울 수 있다.") + @Test + void clearCart() { + // given + Long cartId = 123L; + + when(session.getAttribute("CART_ID")).thenReturn(cartId); + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + + // when + cartService.clearCart(); + + // then + verify(cart).clearItems(); + } + + @DisplayName("장바구니에서 라인 아이템을 제거한다") + @Test + void removeLineItem() { + // given + Long cartId = 123L; + LineItemId lineItemId = new LineItemId("lineItem-1"); + + when(session.getAttribute("CART_ID")).thenReturn(cartId); + when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + + // when + cartService.removeLineItem(lineItemId); + + // then + verify(cart).removeLineItem(lineItemId); + } + +} diff --git a/src/test/java/com/example/demo/controllers/CartControllerTest.java b/src/test/java/com/example/demo/controllers/CartControllerTest.java index 2bc21c0..68e3643 100644 --- a/src/test/java/com/example/demo/controllers/CartControllerTest.java +++ b/src/test/java/com/example/demo/controllers/CartControllerTest.java @@ -27,8 +27,8 @@ class CartControllerTest { @DisplayName("GET /cart") void getCart() throws Exception { Cart cart = new Cart(); - Long cartId = 1L; - when(cartService.getCart(cartId)).thenReturn(cart); + + when(cartService.getCart()).thenReturn(cart); mockMvc.perform(get("/cart")) .andExpect(status().isOk()); @@ -38,11 +38,9 @@ void getCart() throws Exception { @DisplayName("DELETE /cart") void deleteCart() throws Exception { - Long cartId = 1L; - mockMvc.perform(delete("/cart")) .andExpect(status().isNoContent()); - verify(cartService).clearCart(cartId); + verify(cartService).clearCart(); } } diff --git a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java index 5d21589..66c33f4 100644 --- a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java +++ b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java @@ -31,7 +31,6 @@ class LineItemControllerTest { @Test @DisplayName("POST /cart/line-items") void addProduct() throws Exception { - Long cartId = 1L; String json = """ { "productId": "product-1", @@ -47,7 +46,6 @@ void addProduct() throws Exception { .andExpect(status().isCreated()); verify(cartService).addItemToCart( - eq(cartId), eq(new ProductId("product-1")), eq(new ProductOption("blue", "M")), eq(2) @@ -57,14 +55,12 @@ void addProduct() throws Exception { @Test @DisplayName("DELETE /cart/line-items/{lineItemId}") void removeProduct() throws Exception { - Long cartId = 1L; - UUID lineItemId = UUID.randomUUID(); + String lineItemId = UUID.randomUUID().toString(); mockMvc.perform(delete("/cart/line-items/{lineItemId}", lineItemId)) .andExpect(status().isNoContent()); verify(cartService).removeLineItem( - eq(cartId), eq(new LineItemId(lineItemId)) ); } From e62dda1256fd991e153223e15f94abe5c28ff091 Mon Sep 17 00:00:00 2001 From: nahye Date: Sat, 12 Apr 2025 20:21:25 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Authorization 헤더에서 userId 받아오기 - 커스텀 예외 추가 --- .../example/demo/application/CartService.java | 37 +++----- .../demo/controllers/CartController.java | 29 ++++-- .../demo/controllers/LineItemController.java | 19 +++- .../demo/exception/CartNotFoundException.java | 7 ++ .../exception/CartQuantityLimitException.java | 7 ++ .../demo/exception/ErrorResponseDto.java | 8 ++ .../exception/GlobalExceptionHandler.java | 40 ++++++++ .../exception/ProductNotFoundException.java | 9 ++ .../demo/exception/UnauthorizedException.java | 7 ++ .../java/com/example/demo/model/Cart.java | 14 ++- .../java/com/example/demo/model/LineItem.java | 5 +- .../demo/repository/CartRepository.java | 3 + .../example/demo/util/AuthorizationUtils.java | 17 ++++ .../demo/application/CartServiceTest.java | 91 ++++++++++--------- .../demo/controllers/CartControllerTest.java | 59 +++++++++++- .../controllers/LineItemControllerTest.java | 16 +++- .../java/com/example/demo/model/CartTest.java | 7 +- 17 files changed, 284 insertions(+), 91 deletions(-) create mode 100644 src/main/java/com/example/demo/exception/CartNotFoundException.java create mode 100644 src/main/java/com/example/demo/exception/CartQuantityLimitException.java create mode 100644 src/main/java/com/example/demo/exception/ErrorResponseDto.java create mode 100644 src/main/java/com/example/demo/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/demo/exception/ProductNotFoundException.java create mode 100644 src/main/java/com/example/demo/exception/UnauthorizedException.java create mode 100644 src/main/java/com/example/demo/util/AuthorizationUtils.java diff --git a/src/main/java/com/example/demo/application/CartService.java b/src/main/java/com/example/demo/application/CartService.java index 1716b4d..c1ed2fc 100644 --- a/src/main/java/com/example/demo/application/CartService.java +++ b/src/main/java/com/example/demo/application/CartService.java @@ -1,12 +1,13 @@ package com.example.demo.application; +import com.example.demo.exception.CartNotFoundException; +import com.example.demo.exception.ProductNotFoundException; import com.example.demo.model.Cart; import com.example.demo.model.LineItemId; import com.example.demo.model.ProductId; import com.example.demo.model.ProductOption; import com.example.demo.repository.CartRepository; import com.example.demo.repository.ProductRepository; -import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,43 +18,31 @@ public class CartService { private final CartRepository cartRepository; private final ProductRepository productRepository; - private final HttpSession session; - - public CartService(CartRepository cartRepository, ProductRepository productRepository, HttpSession session) { + public CartService(CartRepository cartRepository, ProductRepository productRepository) { this.cartRepository = cartRepository; this.productRepository = productRepository; - this.session = session; } - public void addItemToCart(ProductId productId, ProductOption option, int quantity) { - Cart cart = getCart(); + public void addItemToCart(String userId, ProductId productId, ProductOption option, int quantity) { + Cart cart = getCart(userId); if (!productRepository.existsById(productId)) { - throw new IllegalArgumentException("상품이 존재하지 않습니다."); + throw new ProductNotFoundException(productId); } cart.addProduct(productId, option, quantity); } - public void removeLineItem(LineItemId lineItemId) { - Cart cart = getCart(); + public void removeLineItem(String userId, LineItemId lineItemId) { + Cart cart = getCart(userId); cart.removeLineItem(lineItemId); } - public void clearCart() { - Cart cart = getCart(); + public void clearCart(String userId) { + Cart cart = getCart(userId); cart.clearItems(); } - public Cart getCart() { - Long cartId = (Long) session.getAttribute("CART_ID"); - - if (cartId != null) { - return cartRepository.findById(cartId) - .orElseThrow(() -> new IllegalArgumentException("장바구니가 존재하지 않습니다.")); - } - - Cart cart = new Cart(); - Cart savedCart = cartRepository.save(cart); - session.setAttribute("CART_ID", savedCart.getId()); - return savedCart; + public Cart getCart(String userId) { + return cartRepository.findByUserId(userId) + .orElseThrow(CartNotFoundException::new); } } diff --git a/src/main/java/com/example/demo/controllers/CartController.java b/src/main/java/com/example/demo/controllers/CartController.java index abb058d..4399f13 100644 --- a/src/main/java/com/example/demo/controllers/CartController.java +++ b/src/main/java/com/example/demo/controllers/CartController.java @@ -4,29 +4,37 @@ import com.example.demo.controllers.dto.CartResponseDto; import com.example.demo.controllers.dto.LineItemResponseDto; import com.example.demo.model.Cart; +import com.example.demo.util.AuthorizationUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import static com.example.demo.util.AuthorizationUtils.extractUserIdFromAuthorization; + @RestController @RequestMapping("/cart") public class CartController { + private final CartService cartService; + public CartController(CartService cartService) { this.cartService = cartService; } - private final CartService cartService; @GetMapping - public CartResponseDto getCart() { + public ResponseEntity getCart( + @RequestHeader(name = "Authorization", required = false) + String authorization) { - Cart cart = cartService.getCart(); + String userId = extractUserIdFromAuthorization(authorization); + Cart cart = cartService.getCart(userId); - return new CartResponseDto( + CartResponseDto responseDto = new CartResponseDto( cart.getTotalQuantity(), cart.getLineItems().stream() .map(lineItem -> new LineItemResponseDto( @@ -37,12 +45,21 @@ public CartResponseDto getCart() { ) ).toList() ); + + return new ResponseEntity<>(responseDto, HttpStatus.OK); } @DeleteMapping() - public ResponseEntity deleteCart() { - cartService.clearCart(); + public ResponseEntity deleteCart( + @RequestHeader(name = "Authorization", required = false) + String authorization) { + + String userId = extractUserIdFromAuthorization(authorization); + + cartService.clearCart(userId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + + } diff --git a/src/main/java/com/example/demo/controllers/LineItemController.java b/src/main/java/com/example/demo/controllers/LineItemController.java index 86bcff7..07feed9 100644 --- a/src/main/java/com/example/demo/controllers/LineItemController.java +++ b/src/main/java/com/example/demo/controllers/LineItemController.java @@ -2,6 +2,8 @@ import com.example.demo.application.CartService; import com.example.demo.controllers.dto.LineItemRequestDto; +import com.example.demo.exception.UnauthorizedException; +import com.example.demo.model.Cart; import com.example.demo.model.LineItemId; import com.example.demo.model.ProductId; import com.example.demo.model.ProductOption; @@ -11,10 +13,13 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import static com.example.demo.util.AuthorizationUtils.extractUserIdFromAuthorization; + @RestController @RequestMapping("/cart/line-items") @@ -29,9 +34,13 @@ public LineItemController(CartService cartService) { @PostMapping @ResponseStatus(HttpStatus.CREATED) public void create( - @Valid @RequestBody LineItemRequestDto requestDto) { + @Valid @RequestBody LineItemRequestDto requestDto, + @RequestHeader("Authorization") String authorization) { + + String userId = extractUserIdFromAuthorization(authorization); cartService.addItemToCart( + userId, new ProductId(requestDto.productId()), new ProductOption(requestDto.color(), requestDto.size()), requestDto.quantity()); @@ -40,10 +49,14 @@ public void create( @DeleteMapping("/{lineItemId}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteLineItem(@PathVariable String lineItemId) { + public void deleteLineItem( + @PathVariable String lineItemId, + @RequestHeader("Authorization") String authorization) { + + String userId = extractUserIdFromAuthorization(authorization); cartService.removeLineItem( + userId, new LineItemId(lineItemId)); } - } diff --git a/src/main/java/com/example/demo/exception/CartNotFoundException.java b/src/main/java/com/example/demo/exception/CartNotFoundException.java new file mode 100644 index 0000000..0cf6931 --- /dev/null +++ b/src/main/java/com/example/demo/exception/CartNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.demo.exception; + +public class CartNotFoundException extends RuntimeException { + public CartNotFoundException() { + super("장바구니를 찾을 수 없습니다."); + } +} diff --git a/src/main/java/com/example/demo/exception/CartQuantityLimitException.java b/src/main/java/com/example/demo/exception/CartQuantityLimitException.java new file mode 100644 index 0000000..31cf94c --- /dev/null +++ b/src/main/java/com/example/demo/exception/CartQuantityLimitException.java @@ -0,0 +1,7 @@ +package com.example.demo.exception; + +public class CartQuantityLimitException extends RuntimeException { + public CartQuantityLimitException() { + super("장바구니에 담을 수 있는 수량을 초과했습니다."); + } +} diff --git a/src/main/java/com/example/demo/exception/ErrorResponseDto.java b/src/main/java/com/example/demo/exception/ErrorResponseDto.java new file mode 100644 index 0000000..da5bc94 --- /dev/null +++ b/src/main/java/com/example/demo/exception/ErrorResponseDto.java @@ -0,0 +1,8 @@ +package com.example.demo.exception; + + +public record ErrorResponseDto( + String code, + String message +) { +} diff --git a/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..64d7995 --- /dev/null +++ b/src/main/java/com/example/demo/exception/GlobalExceptionHandler.java @@ -0,0 +1,40 @@ +package com.example.demo.exception; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(CartNotFoundException.class) + public ResponseEntity handleCartNotFound(CartNotFoundException e) { + ErrorResponseDto responseDto = new ErrorResponseDto("CART_NOT_FOUND", e.getMessage()); + return new ResponseEntity<>(responseDto, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(ProductNotFoundException.class) + public ResponseEntity handleProductNotFound(ProductNotFoundException e) { + ErrorResponseDto responseDto = new ErrorResponseDto("PRODUCT_NOT_FOUND", e.getMessage()); + return new ResponseEntity<>(responseDto, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(CartQuantityLimitException.class) + public ResponseEntity handleCartQuantityLimit(CartQuantityLimitException e) { + ErrorResponseDto responseDto = new ErrorResponseDto("CART_QUANTITY_LIMIT", e.getMessage()); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .contentType(MediaType.APPLICATION_JSON) + .body(responseDto); + } + + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity handleUnauthorized(UnauthorizedException e) { + ErrorResponseDto responseDto = new ErrorResponseDto("UNAUTHORIZED", e.getMessage()); + return new ResponseEntity<>(responseDto, HttpStatus.UNAUTHORIZED); + } + +} diff --git a/src/main/java/com/example/demo/exception/ProductNotFoundException.java b/src/main/java/com/example/demo/exception/ProductNotFoundException.java new file mode 100644 index 0000000..7cd79be --- /dev/null +++ b/src/main/java/com/example/demo/exception/ProductNotFoundException.java @@ -0,0 +1,9 @@ +package com.example.demo.exception; + +import com.example.demo.model.ProductId; + +public class ProductNotFoundException extends RuntimeException { + public ProductNotFoundException(ProductId productId) { + super("상품을 찾을 수 없습니다. ID: " + productId.id()); + } +} diff --git a/src/main/java/com/example/demo/exception/UnauthorizedException.java b/src/main/java/com/example/demo/exception/UnauthorizedException.java new file mode 100644 index 0000000..c949d60 --- /dev/null +++ b/src/main/java/com/example/demo/exception/UnauthorizedException.java @@ -0,0 +1,7 @@ +package com.example.demo.exception; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException() { + super("인증이 필요합니다."); + } +} diff --git a/src/main/java/com/example/demo/model/Cart.java b/src/main/java/com/example/demo/model/Cart.java index dc570a6..7ff7ecb 100644 --- a/src/main/java/com/example/demo/model/Cart.java +++ b/src/main/java/com/example/demo/model/Cart.java @@ -1,6 +1,8 @@ package com.example.demo.model; +import com.example.demo.exception.CartQuantityLimitException; import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -20,6 +22,9 @@ public class Cart { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + private String userId; + @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true) private List lineItems = new ArrayList<>(); @@ -28,7 +33,12 @@ public class Cart { public Cart() { } - public Cart(List lineItems) { + public Cart(String userId) { + this.userId = userId; + } + + public Cart(String userId, List lineItems) { + this.userId = userId; this.lineItems = new ArrayList<>(lineItems); updateTotalQuantity(); } @@ -66,7 +76,7 @@ private void updateTotalQuantity() { .mapToInt(LineItem::getQuantity) .sum(); if (totalQuantity > 20) { - throw new IllegalArgumentException("담을수 있는 수량을 초과했습니다."); + throw new CartQuantityLimitException(); } } diff --git a/src/main/java/com/example/demo/model/LineItem.java b/src/main/java/com/example/demo/model/LineItem.java index 65d8ddc..5734100 100644 --- a/src/main/java/com/example/demo/model/LineItem.java +++ b/src/main/java/com/example/demo/model/LineItem.java @@ -3,13 +3,10 @@ import jakarta.persistence.Embedded; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @Entity -@Table(name="line_items") +@Table(name = "line_items") public class LineItem { @EmbeddedId diff --git a/src/main/java/com/example/demo/repository/CartRepository.java b/src/main/java/com/example/demo/repository/CartRepository.java index c1a665d..5d43b6f 100644 --- a/src/main/java/com/example/demo/repository/CartRepository.java +++ b/src/main/java/com/example/demo/repository/CartRepository.java @@ -3,5 +3,8 @@ import com.example.demo.model.Cart; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CartRepository extends JpaRepository { + Optional findByUserId(String userId); } diff --git a/src/main/java/com/example/demo/util/AuthorizationUtils.java b/src/main/java/com/example/demo/util/AuthorizationUtils.java new file mode 100644 index 0000000..6d5742c --- /dev/null +++ b/src/main/java/com/example/demo/util/AuthorizationUtils.java @@ -0,0 +1,17 @@ +package com.example.demo.util; + +import com.example.demo.exception.UnauthorizedException; + +public class AuthorizationUtils { + + private AuthorizationUtils() { + } + + public static String extractUserIdFromAuthorization(String authorization) { + if (authorization == null || !authorization.startsWith("Bearer ")) { + throw new UnauthorizedException(); + } + return authorization.substring(7); + } + +} diff --git a/src/test/java/com/example/demo/application/CartServiceTest.java b/src/test/java/com/example/demo/application/CartServiceTest.java index a3b5d0d..42ffe3d 100644 --- a/src/test/java/com/example/demo/application/CartServiceTest.java +++ b/src/test/java/com/example/demo/application/CartServiceTest.java @@ -1,12 +1,13 @@ package com.example.demo.application; +import com.example.demo.exception.CartNotFoundException; +import com.example.demo.exception.ProductNotFoundException; import com.example.demo.model.Cart; import com.example.demo.model.LineItemId; import com.example.demo.model.ProductId; import com.example.demo.model.ProductOption; import com.example.demo.repository.CartRepository; import com.example.demo.repository.ProductRepository; -import jakarta.servlet.http.HttpSession; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,7 +15,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -23,7 +24,6 @@ class CartServiceTest { private CartRepository cartRepository; private ProductRepository productRepository; - private HttpSession session; private CartService cartService; private Cart cart; @@ -31,78 +31,87 @@ class CartServiceTest { void setUp() { cartRepository = mock(CartRepository.class); productRepository = mock(ProductRepository.class); - session = mock(HttpSession.class); - cartService = new CartService(cartRepository, productRepository, session); + cartService = new CartService(cartRepository, productRepository); cart = mock(Cart.class); } - @DisplayName("세션에 CART_ID가 존재하면 해당 ID로 장바구니를 조회한다") + @DisplayName("userId가 있을때 카트를 찾을수있다.") @Test - void getExistingCartId() { + void getCart() { // given - Long existingCartId = 123L; - - when(session.getAttribute("CART_ID")).thenReturn(existingCartId); - when(cartRepository.findById(existingCartId)).thenReturn(Optional.of(cart)); + String userId = "userA"; + when(cartRepository.findByUserId(userId)) + .thenReturn(Optional.of(cart)); // when - Cart result = cartService.getCart(); + Cart result = cartService.getCart(userId); // then assertThat(result).isSameAs(cart); - verify(session).getAttribute("CART_ID"); - verify(cartRepository).findById(existingCartId); + verify(cartRepository).findByUserId(userId); } - @DisplayName("세션에 CART_ID가 없으면 새 장바구니를 생성하고 세션에 저장한다 ") + @DisplayName("장바구니를 못찾으면 예외가 발생한다.") @Test - void getNonExistingCartId() { - // given - Long newCartId = 456L; - - when(session.getAttribute("CART_ID")).thenReturn(null); - when(cartRepository.save(any(Cart.class))).thenReturn(cart); - when(cart.getId()).thenReturn(newCartId); - - // when - Cart result = cartService.getCart(); + void cannotFindCart() { + String userId = "userA"; + when(cartRepository.findByUserId(userId)) + .thenReturn(Optional.empty()); - // then - assertThat(result).isSameAs(cart); - verify(session).setAttribute("CART_ID", newCartId); + assertThatThrownBy(() -> cartService.getCart(userId)) + .isInstanceOf(CartNotFoundException.class); } @DisplayName("상품을 장바구니에 추가한다") @Test void addItemToCart_AddsProductToCart() { // given - Long cartId = 123L; + String userId = "userA"; ProductId productId = new ProductId("product-1"); ProductOption option = new ProductOption("Red", "L"); int quantity = 2; - when(session.getAttribute("CART_ID")).thenReturn(cartId); - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); - when(productRepository.existsById(productId)).thenReturn(true); + when(cartRepository.findByUserId(userId)) + .thenReturn(Optional.of(cart)); + when(productRepository.existsById(productId)) + .thenReturn(true); // when - cartService.addItemToCart(productId, option, quantity); + cartService.addItemToCart(userId, productId, option, quantity); // then verify(cart).addProduct(productId, option, quantity); } + @DisplayName("장바구니에 넣을 상품이 존재하지 않으면 예외가 발생한다.") + @Test + void throwExceptionWhenProductNotFound() { + String userId = "userA"; + ProductId productId = new ProductId("product-1"); + ProductOption option = new ProductOption("Red", "L"); + int quantity = 2; + + when(cartRepository.findByUserId(userId)) + .thenReturn(Optional.of(cart)); + when(productRepository.existsById(productId)) + .thenReturn(false); + + assertThatThrownBy( + () -> cartService.addItemToCart(userId, productId, option, quantity)) + .isInstanceOf(ProductNotFoundException.class); + } + @DisplayName("장바구니를 비울 수 있다.") @Test void clearCart() { // given - Long cartId = 123L; + String userId = "userA"; - when(session.getAttribute("CART_ID")).thenReturn(cartId); - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + when(cartRepository.findByUserId(userId)) + .thenReturn(Optional.of(cart)); // when - cartService.clearCart(); + cartService.clearCart(userId); // then verify(cart).clearItems(); @@ -112,14 +121,14 @@ void clearCart() { @Test void removeLineItem() { // given - Long cartId = 123L; + String userId = "userA"; LineItemId lineItemId = new LineItemId("lineItem-1"); - when(session.getAttribute("CART_ID")).thenReturn(cartId); - when(cartRepository.findById(cartId)).thenReturn(Optional.of(cart)); + when(cartRepository.findByUserId(userId)) + .thenReturn(Optional.of(cart)); // when - cartService.removeLineItem(lineItemId); + cartService.removeLineItem(userId, lineItemId); // then verify(cart).removeLineItem(lineItemId); diff --git a/src/test/java/com/example/demo/controllers/CartControllerTest.java b/src/test/java/com/example/demo/controllers/CartControllerTest.java index 68e3643..1992009 100644 --- a/src/test/java/com/example/demo/controllers/CartControllerTest.java +++ b/src/test/java/com/example/demo/controllers/CartControllerTest.java @@ -1,18 +1,21 @@ package com.example.demo.controllers; import com.example.demo.application.CartService; +import com.example.demo.exception.CartNotFoundException; import com.example.demo.model.Cart; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(CartController.class) @@ -26,21 +29,69 @@ class CartControllerTest { @Test @DisplayName("GET /cart") void getCart() throws Exception { + String userId = "userA"; Cart cart = new Cart(); - when(cartService.getCart()).thenReturn(cart); + when(cartService.getCart(userId)).thenReturn(cart); - mockMvc.perform(get("/cart")) + mockMvc.perform(get("/cart") + .header("Authorization", "Bearer " + userId)) .andExpect(status().isOk()); } + @Test + @DisplayName("GET /cart - 인증 헤더가 없으면 401 응답") + void getCart_unauthorized() throws Exception { + String userId = "userA"; + Cart cart = new Cart(); + + when(cartService.getCart(userId)).thenReturn(cart); + + String json = """ + { + "code": "UNAUTHORIZED", + "message": "인증이 필요합니다." + } + """; + + mockMvc.perform(get("/cart")) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(json)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("GET /cart - 장바구니가 없으면 404를 응답한다") + void getCart_notFound() throws Exception { + String userId = "userA"; + + when(cartService.getCart(userId)) + .thenThrow(new CartNotFoundException()); + + String json = """ + { + "code": "CART_NOT_FOUND", + "message": "장바구니를 찾을 수 없습니다." + } + """; + + + mockMvc.perform(get("/cart") + .header("Authorization", "Bearer " + userId)) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(json)) + .andExpect(status().isNotFound()); + } + @Test @DisplayName("DELETE /cart") void deleteCart() throws Exception { + String userId = "userA"; - mockMvc.perform(delete("/cart")) + mockMvc.perform(delete("/cart") + .header("Authorization", "Bearer " + userId)) .andExpect(status().isNoContent()); - verify(cartService).clearCart(); + verify(cartService).clearCart(userId); } } diff --git a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java index 66c33f4..df075be 100644 --- a/src/test/java/com/example/demo/controllers/LineItemControllerTest.java +++ b/src/test/java/com/example/demo/controllers/LineItemControllerTest.java @@ -1,6 +1,7 @@ package com.example.demo.controllers; import com.example.demo.application.CartService; +import com.example.demo.exception.GlobalExceptionHandler; import com.example.demo.model.LineItemId; import com.example.demo.model.ProductId; import com.example.demo.model.ProductOption; @@ -9,11 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.util.UUID; - import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -21,6 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(LineItemController.class) +@Import(GlobalExceptionHandler.class) class LineItemControllerTest { @Autowired private MockMvc mockMvc; @@ -31,6 +32,8 @@ class LineItemControllerTest { @Test @DisplayName("POST /cart/line-items") void addProduct() throws Exception { + String userId = "userA"; + String json = """ { "productId": "product-1", @@ -41,11 +44,13 @@ void addProduct() throws Exception { """; mockMvc.perform(post("/cart/line-items") + .header("Authorization", "Bearer " + userId) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isCreated()); verify(cartService).addItemToCart( + eq(userId), eq(new ProductId("product-1")), eq(new ProductOption("blue", "M")), eq(2) @@ -55,12 +60,15 @@ void addProduct() throws Exception { @Test @DisplayName("DELETE /cart/line-items/{lineItemId}") void removeProduct() throws Exception { - String lineItemId = UUID.randomUUID().toString(); + String userId = "userA"; + String lineItemId = "lineItem-1"; - mockMvc.perform(delete("/cart/line-items/{lineItemId}", lineItemId)) + mockMvc.perform(delete("/cart/line-items/{lineItemId}", lineItemId) + .header("Authorization", "Bearer " + userId)) .andExpect(status().isNoContent()); verify(cartService).removeLineItem( + eq(userId), eq(new LineItemId(lineItemId)) ); } diff --git a/src/test/java/com/example/demo/model/CartTest.java b/src/test/java/com/example/demo/model/CartTest.java index e1bdb4c..2b5fbde 100644 --- a/src/test/java/com/example/demo/model/CartTest.java +++ b/src/test/java/com/example/demo/model/CartTest.java @@ -1,5 +1,6 @@ package com.example.demo.model; +import com.example.demo.exception.CartQuantityLimitException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,7 +29,7 @@ void setUp() { @Test @DisplayName("빈 장바구니의 수량은 0이다.") void cartTotalQuantityIsZero() { - Cart cart = new Cart(List.of()); + Cart cart = new Cart(); assertThat(cart.getTotalQuantity()).isZero(); } @@ -36,7 +37,7 @@ void cartTotalQuantityIsZero() { @Test @DisplayName("빈 장바구니에 물건 추가하면 장바구니의 전체수량은 추가한 수량과 같다.") void addProductToEmptyCart() { - Cart cart = new Cart(List.of()); + Cart cart = new Cart(); int quantity = 1; cart.addProduct(product1.getId(), productOption1, quantity); @@ -102,7 +103,7 @@ void totalQuantityCanNotOverLimit() { assertThatThrownBy( () -> cart.addProduct(product1.getId(), productOption1, newQuantity) - ).isInstanceOf(IllegalArgumentException.class); + ).isInstanceOf(CartQuantityLimitException.class); } @Test