diff --git a/README.md b/README.md index 8d87935edb..b0d9b7642b 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ # jwp-shopping-cart + +## 기능 목록 Step2 + +- [x] settings 페이지 연동 + - [x] settings.html 변경 +- [x] 장바구니 페이지 연동 + - [x] cart.html 변경 + +- [x] DB 설계 + +- [x] 필터 작성 + +- [x] User 도메인 추가 + - [x] User 도메인에 Cart 도메인 추가 +- [x] Cart 도메인 추가 + +## 기능목록 + +- [x] 상품 목록 페이지 연동 + - [x] index.html 변경 +- [x] 상품 관리 CRUD API 작성 + - [x] create product + - [x] read product + - [x] update product + - [x] delete product +- [x] 관리자 도구 페이지 연동 + - [x] admin.html 변경 + - [x] admin.js 변경 + +- [x] DB 설계 + +# 도메인 구조 + +```mermaid +graph TD + +cart --> user +cart --many--> product +``` + +# DB 설계 + +```mermaid +graph TD + +cart --> user +cart --many--> cart_product +product --many--> cart_product +``` diff --git a/build.gradle b/build.gradle index ce5fbd8249..fda5f1d152 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:4.4.0' diff --git a/src/main/java/cart/JwpCartApplication.java b/src/main/java/cart/JwpCartApplication.java index 732c382995..fe51dde762 100644 --- a/src/main/java/cart/JwpCartApplication.java +++ b/src/main/java/cart/JwpCartApplication.java @@ -6,8 +6,7 @@ @SpringBootApplication public class JwpCartApplication { - public static void main(String[] args) { + public static void main(final String[] args) { SpringApplication.run(JwpCartApplication.class, args); } - } diff --git a/src/main/java/cart/config/SecurityConfig.java b/src/main/java/cart/config/SecurityConfig.java new file mode 100644 index 0000000000..361dcf12fd --- /dev/null +++ b/src/main/java/cart/config/SecurityConfig.java @@ -0,0 +1,34 @@ +package cart.config; + +import cart.filter.AuthenticationFilter; +import cart.repository.user.UserRepository; +import cart.resolver.AuthenticationResolver; +import java.util.List; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class SecurityConfig implements WebMvcConfigurer { + + private final UserRepository userRepository; + + public SecurityConfig(final UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + final FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new AuthenticationFilter(userRepository)); + registrationBean.addUrlPatterns("/carts/*, /products/*"); + return registrationBean; + } + + @Override + public void addArgumentResolvers(final List argumentResolvers) { + argumentResolvers.add(new AuthenticationResolver()); + } +} diff --git a/src/main/java/cart/controller/GlobalExceptionHandler.java b/src/main/java/cart/controller/GlobalExceptionHandler.java new file mode 100644 index 0000000000..421e9982d3 --- /dev/null +++ b/src/main/java/cart/controller/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package cart.controller; + +import cart.dto.ErrorResponse; +import cart.exception.AlreadyAddedProductException; +import cart.exception.CartNotFoundException; +import cart.exception.ProductInCartDeleteException; +import cart.exception.ProductNotFoundException; +import cart.exception.UserNotFoundException; +import java.util.Map; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + @ExceptionHandler + private ResponseEntity handleException(final Exception exception) { + log.error("예상치 못한 예외가 발생했습니다.", exception); + return ResponseEntity.internalServerError().body(new ErrorResponse("예상치 못한 예외가 발생했습니다.")); + } + + @ExceptionHandler + private ResponseEntity handleIllegalArgumentException(final IllegalArgumentException exception) { + log.warn("잘못된 인자가 들어왔습니다", exception); + return ResponseEntity.badRequest().body(new ErrorResponse(exception.getMessage())); + } + + @ExceptionHandler({CartNotFoundException.class, ProductNotFoundException.class, UserNotFoundException.class}) + private ResponseEntity handleNotFoundException(final Exception exception) { + log.warn("존재하지 않는 리소스에 접근했습니다.", exception); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(exception.getMessage())); + } + + @ExceptionHandler + private ResponseEntity handleAlreadyAddedProduct(final AlreadyAddedProductException e) { + log.warn("이미 장바구니에 담긴 상품입니다.", e); + return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler + private ResponseEntity handleProductInCartDelete(final ProductInCartDeleteException e) { + log.warn("장바구니에 담긴 상품을 삭제할 수 없습니다.", e); + return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + final MethodArgumentNotValidException exception, final HttpHeaders headers, final HttpStatus status, + final WebRequest request) { + log.warn("유효성 검사에 실패했습니다.", exception); + final Map body = exception.getFieldErrors() + .stream() + .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); + return handleExceptionInternal(exception, body, headers, status, request); + } +} diff --git a/src/main/java/cart/controller/cart/CartApiController.java b/src/main/java/cart/controller/cart/CartApiController.java new file mode 100644 index 0000000000..09fc898a81 --- /dev/null +++ b/src/main/java/cart/controller/cart/CartApiController.java @@ -0,0 +1,62 @@ +package cart.controller.cart; + +import cart.domain.cart.Cart; +import cart.domain.product.Product; +import cart.dto.cart.AddCartResponse; +import cart.dto.cart.AddProductRequest; +import cart.dto.cart.FindCartResponse; +import cart.resolver.AuthInfo; +import cart.resolver.Authentication; +import cart.service.cart.CartCommandService; +import cart.service.cart.CartQueryService; +import cart.service.product.ProductQueryService; +import java.util.List; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/carts/cartproduct") +public class CartApiController { + + private final CartCommandService cartCommandService; + private final ProductQueryService productQueryService; + private final CartQueryService cartQueryService; + + public CartApiController(final CartCommandService cartCommandService, + final ProductQueryService productQueryService, + final CartQueryService cartQueryService) { + this.cartCommandService = cartCommandService; + this.productQueryService = productQueryService; + this.cartQueryService = cartQueryService; + } + + @PostMapping + public ResponseEntity addProduct(@RequestBody final AddProductRequest addProductRequest, + @Authentication final AuthInfo authInfo) { + final Long productId = addProductRequest.getProductId(); + final String email = authInfo.getEmail(); + final Cart cart = cartCommandService.addProduct(productId, email); + return ResponseEntity.ok(AddCartResponse.from(cart)); + } + + @GetMapping + public ResponseEntity findCart(@Authentication final AuthInfo authInfo) { + final Cart cart = cartQueryService.findByEmail(authInfo.getEmail()); + final List products = productQueryService.findAllByIds(cart.getCartProducts().getProductIds()); + return ResponseEntity.ok(FindCartResponse.of(cart, products)); + } + + @DeleteMapping("/{cartProductId}") + public ResponseEntity deleteProduct(@Authentication final AuthInfo authInfo, + @PathVariable final Long cartProductId) { + final String email = authInfo.getEmail(); + cartCommandService.deleteProduct(cartProductId, email); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/cart/controller/cart/CartViewController.java b/src/main/java/cart/controller/cart/CartViewController.java new file mode 100644 index 0000000000..249c28a2e1 --- /dev/null +++ b/src/main/java/cart/controller/cart/CartViewController.java @@ -0,0 +1,13 @@ +package cart.controller.cart; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class CartViewController { + + @GetMapping("/cart") + public String getCartPage() { + return "cart"; + } +} diff --git a/src/main/java/cart/controller/product/ProductApiController.java b/src/main/java/cart/controller/product/ProductApiController.java new file mode 100644 index 0000000000..ecae01476c --- /dev/null +++ b/src/main/java/cart/controller/product/ProductApiController.java @@ -0,0 +1,73 @@ +package cart.controller.product; + +import cart.domain.product.Product; +import cart.dto.product.ProductRequest; +import cart.dto.product.ProductResponse; +import cart.service.product.ProductCommandService; +import cart.service.product.ProductQueryService; +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.Valid; +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.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ProductApiController { + + private final ProductCommandService productCommandService; + private final ProductQueryService productQueryService; + + public ProductApiController(final ProductCommandService productCommandService, + final ProductQueryService productQueryService) { + this.productCommandService = productCommandService; + this.productQueryService = productQueryService; + } + + @PostMapping("/products") + public ResponseEntity createProduct(@RequestBody @Valid final ProductRequest productRequest) { + final Product product = productCommandService.create( + productRequest.getName(), + productRequest.getImage(), + productRequest.getPrice()); + + final ProductResponse productResponse = ProductResponse.from(product); + return ResponseEntity.created(URI.create("/products/" + product.getProductId())) + .body(productResponse); + } + + @GetMapping("/products") + public ResponseEntity> getAllProducts() { + final List productResponses = productQueryService.findAll() + .stream() + .map(ProductResponse::from) + .collect(Collectors.toList()); + + return ResponseEntity.ok(productResponses); + } + + @PutMapping("/products/{id}") + public ResponseEntity updateProduct(@PathVariable final long id, + @RequestBody @Valid final ProductRequest productRequest) { + final Product product = productCommandService.update( + id, + productRequest.getName(), + productRequest.getImage(), + productRequest.getPrice()); + + final ProductResponse productResponse = ProductResponse.from(product); + return ResponseEntity.ok(productResponse); + } + + @DeleteMapping("/products/{id}") + public ResponseEntity deleteProduct(@PathVariable final long id) { + productCommandService.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/cart/controller/product/ProductViewController.java b/src/main/java/cart/controller/product/ProductViewController.java new file mode 100644 index 0000000000..f1cb2ce9a8 --- /dev/null +++ b/src/main/java/cart/controller/product/ProductViewController.java @@ -0,0 +1,42 @@ +package cart.controller.product; + +import cart.dto.product.ProductResponse; +import cart.service.product.ProductQueryService; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ProductViewController { + + private final ProductQueryService productSearchService; + + public ProductViewController(final ProductQueryService productSearchService) { + this.productSearchService = productSearchService; + } + + @GetMapping("/") + public String getIndexPage(final Model model) { + final List productResponses = productSearchService.findAll() + .stream() + .map(ProductResponse::from) + .collect(Collectors.toList()); + + model.addAttribute("products", productResponses); + return "index"; + } + + @GetMapping("/admin") + public String getAdminPage(final Model model) { + final List productResponses = productSearchService.findAll() + .stream() + .map(ProductResponse::from) + .collect(Collectors.toList()); + + model.addAttribute("products", productResponses); + return "admin"; + } + +} diff --git a/src/main/java/cart/controller/setting/SettingViewController.java b/src/main/java/cart/controller/setting/SettingViewController.java new file mode 100644 index 0000000000..00cec609d1 --- /dev/null +++ b/src/main/java/cart/controller/setting/SettingViewController.java @@ -0,0 +1,30 @@ +package cart.controller.setting; + +import cart.dto.user.UserResponse; +import cart.service.user.UserQueryService; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class SettingViewController { + + private final UserQueryService userQueryService; + + public SettingViewController(final UserQueryService userQueryService) { + this.userQueryService = userQueryService; + } + + @GetMapping("/settings") + public String getSettingsPage(final Model model) { + final List userResponses = userQueryService.findAll() + .stream() + .map(UserResponse::from) + .collect(Collectors.toList()); + + model.addAttribute("members", userResponses); + return "settings"; + } +} diff --git a/src/main/java/cart/domain/cart/Cart.java b/src/main/java/cart/domain/cart/Cart.java new file mode 100644 index 0000000000..48a5da8a2e --- /dev/null +++ b/src/main/java/cart/domain/cart/Cart.java @@ -0,0 +1,64 @@ +package cart.domain.cart; + +import cart.domain.product.ProductId; +import cart.domain.user.UserId; +import java.util.List; +import java.util.Objects; + +public class Cart { + + private final CartId cartId; + private final UserId userId; + private final CartProducts cartProducts; + + public Cart(final CartId cartId, final UserId userId, final List cartProducts) { + this(cartId, userId, new CartProducts(cartProducts)); + } + + public Cart(final UserId userId) { + this(new CartId(), userId, new CartProducts()); + } + + public Cart(final CartId cartId, final UserId userId, final CartProducts cartProducts) { + this.cartId = cartId; + this.userId = userId; + this.cartProducts = cartProducts; + } + + public void addProduct(final ProductId productId) { + cartProducts.add(productId); + } + + public void deleteProduct(final Long cartProductId) { + cartProducts.delete(cartProductId); + } + + public CartId getCartId() { + return cartId; + } + + public UserId getUserId() { + return userId; + } + + public CartProducts getCartProducts() { + return cartProducts; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Cart cart = (Cart) o; + return cartId.equals(cart.cartId); + } + + @Override + public int hashCode() { + return Objects.hash(cartId); + } +} diff --git a/src/main/java/cart/domain/cart/CartId.java b/src/main/java/cart/domain/cart/CartId.java new file mode 100644 index 0000000000..d8de8919f1 --- /dev/null +++ b/src/main/java/cart/domain/cart/CartId.java @@ -0,0 +1,37 @@ +package cart.domain.cart; + +import java.util.Objects; + +public class CartId { + + private final Long id; + + public CartId() { + this(null); + } + + public CartId(final Long id) { + this.id = id; + } + + public Long getValue() { + return id; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CartId cartId = (CartId) o; + return Objects.equals(id, cartId.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/cart/domain/cart/CartProduct.java b/src/main/java/cart/domain/cart/CartProduct.java new file mode 100644 index 0000000000..e0e327d137 --- /dev/null +++ b/src/main/java/cart/domain/cart/CartProduct.java @@ -0,0 +1,26 @@ +package cart.domain.cart; + +import cart.domain.product.ProductId; + +public class CartProduct { + + private final CartProductId cartProductId; + private final ProductId productId; + + public CartProduct(final ProductId productId) { + this(new CartProductId(), productId); + } + + public CartProduct(final CartProductId cartProductId, final ProductId productId) { + this.cartProductId = cartProductId; + this.productId = productId; + } + + public CartProductId getCartProductId() { + return cartProductId; + } + + public ProductId getProductId() { + return productId; + } +} diff --git a/src/main/java/cart/domain/cart/CartProductId.java b/src/main/java/cart/domain/cart/CartProductId.java new file mode 100644 index 0000000000..8a7b49184f --- /dev/null +++ b/src/main/java/cart/domain/cart/CartProductId.java @@ -0,0 +1,37 @@ +package cart.domain.cart; + +import java.util.Objects; + +public class CartProductId { + + private final Long value; + + public CartProductId() { + this(null); + } + + public CartProductId(final Long value) { + this.value = value; + } + + public Long getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CartProductId that = (CartProductId) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/cart/domain/cart/CartProducts.java b/src/main/java/cart/domain/cart/CartProducts.java new file mode 100644 index 0000000000..b369fe9086 --- /dev/null +++ b/src/main/java/cart/domain/cart/CartProducts.java @@ -0,0 +1,41 @@ +package cart.domain.cart; + +import cart.domain.product.ProductId; +import cart.exception.AlreadyAddedProductException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CartProducts { + + private final List cartProducts; + + public CartProducts() { + this(new ArrayList<>()); + } + + public CartProducts(final List cartProducts) { + this.cartProducts = cartProducts; + } + + public void add(final ProductId productId) { + if (cartProducts.stream().anyMatch(cartProduct -> cartProduct.getProductId().equals(productId))) { + throw new AlreadyAddedProductException("이미 장바구니에 담긴 상품입니다."); + } + cartProducts.add(new CartProduct(productId)); + } + + public void delete(final Long cartProductId) { + cartProducts.removeIf(cartProduct -> cartProduct.getCartProductId().getValue().equals(cartProductId)); + } + + public List getCartProducts() { + return cartProducts; + } + + public List getProductIds() { + return cartProducts.stream() + .map(cartProduct -> cartProduct.getProductId().getValue()) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/cart/domain/product/Image.java b/src/main/java/cart/domain/product/Image.java new file mode 100644 index 0000000000..7e000d6c02 --- /dev/null +++ b/src/main/java/cart/domain/product/Image.java @@ -0,0 +1,48 @@ +package cart.domain.product; + +import java.util.Objects; + +public class Image { + + private static final int MAX_IMAGE_LENGTH = 2048; + + private final String value; + + public Image(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (value == null) { + throw new IllegalArgumentException("이미지 url은 null일 수 없습니다."); + } + if (value.length() > MAX_IMAGE_LENGTH) { + throw new IllegalArgumentException("이미지 url은 " + MAX_IMAGE_LENGTH + "자 이하만 가능합니다."); + } + if (value.isBlank()) { + throw new IllegalArgumentException("이미지 url은 공백일 수 없습니다."); + } + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Image that = (Image) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/cart/domain/product/Price.java b/src/main/java/cart/domain/product/Price.java new file mode 100644 index 0000000000..56c58660bd --- /dev/null +++ b/src/main/java/cart/domain/product/Price.java @@ -0,0 +1,42 @@ +package cart.domain.product; + +import java.util.Objects; + +public class Price { + + private static final int MIN_PRICE = 0; + + private final int value; + + public Price(final int value) { + validate(value); + this.value = value; + } + + private void validate(final int value) { + if (value < MIN_PRICE) { + throw new IllegalArgumentException("가격은 " + MIN_PRICE + "원 이상이어야 합니다."); + } + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Price that = (Price) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/cart/domain/product/Product.java b/src/main/java/cart/domain/product/Product.java new file mode 100644 index 0000000000..1d81b35984 --- /dev/null +++ b/src/main/java/cart/domain/product/Product.java @@ -0,0 +1,83 @@ +package cart.domain.product; + +import java.util.Objects; + +public class Product { + + private static final int MAX_NAME_LENGTH = 255; + + private final ProductId productId; + private final String productName; + private final Image image; + private final Price price; + + public Product(final ProductId productId, final String productName, final Image image, + final Price price) { + validateName(productName); + this.productId = productId; + this.productName = productName; + this.image = image; + this.price = price; + } + + public Product(final String productName, final String productImage, final int productPrice) { + this(new ProductId(), productName, productImage, productPrice); + } + + public Product(final Long productId, final Product other) { + this(new ProductId(productId), other.productName, other.image, other.price); + } + + public Product(final Long productId, final String productName, final String productImage, final int productPrice) { + this(new ProductId(productId), productName, new Image(productImage), new Price(productPrice)); + } + + public Product(final ProductId id, final String name, final String image, final int price) { + this(id, name, new Image(image), new Price(price)); + } + + private void validateName(final String productName) { + if (productName == null) { + throw new IllegalArgumentException("이름은 null일 수 없습니다."); + } + if (productName.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("이름은 " + MAX_NAME_LENGTH + "자 이하만 가능합니다."); + } + if (productName.isBlank()) { + throw new IllegalArgumentException("이름은 공백일 수 없습니다."); + } + } + + public ProductId getProductId() { + return productId; + } + + public String getProductName() { + return productName; + } + + public Image getProductImage() { + return image; + } + + public Price getProductPrice() { + return price; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Product product = (Product) o; + return productId.equals(product.productId); + } + + @Override + public int hashCode() { + return Objects.hash(productId); + } +} diff --git a/src/main/java/cart/domain/product/ProductId.java b/src/main/java/cart/domain/product/ProductId.java new file mode 100644 index 0000000000..f9c38cb89a --- /dev/null +++ b/src/main/java/cart/domain/product/ProductId.java @@ -0,0 +1,37 @@ +package cart.domain.product; + +import java.util.Objects; + +public class ProductId { + + private final Long value; + + public ProductId(final Long value) { + this.value = value; + } + + public ProductId() { + this(null); + } + + public Long getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ProductId productId = (ProductId) o; + return Objects.equals(value, productId.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/cart/domain/user/Email.java b/src/main/java/cart/domain/user/Email.java new file mode 100644 index 0000000000..373db98cef --- /dev/null +++ b/src/main/java/cart/domain/user/Email.java @@ -0,0 +1,14 @@ +package cart.domain.user; + +public class Email { + + private final String value; + + public Email(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/cart/domain/user/Password.java b/src/main/java/cart/domain/user/Password.java new file mode 100644 index 0000000000..7de8015204 --- /dev/null +++ b/src/main/java/cart/domain/user/Password.java @@ -0,0 +1,14 @@ +package cart.domain.user; + +public class Password { + + private final String value; + + public Password(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/cart/domain/user/User.java b/src/main/java/cart/domain/user/User.java new file mode 100644 index 0000000000..76ee9dfeb7 --- /dev/null +++ b/src/main/java/cart/domain/user/User.java @@ -0,0 +1,57 @@ +package cart.domain.user; + +import java.util.Objects; + +public class User { + + private final UserId userId; + private final Email email; + private final Password password; + + public User(final UserId userId, final Email email, final Password password) { + this.userId = userId; + this.email = email; + this.password = password; + } + + public User(final String email, final String password) { + this(null, email, password); + } + + public User(final Long id, final String email, final String password) { + this(new UserId(id), new Email(email), new Password(password)); + } + + public User(final long userId, final User other) { + this(new UserId(userId), other.getEmail(), other.getPassword()); + } + + public UserId getUserId() { + return userId; + } + + public Email getEmail() { + return email; + } + + public Password getPassword() { + return password; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final User user = (User) o; + return userId.equals(user.userId); + } + + @Override + public int hashCode() { + return Objects.hash(userId); + } +} diff --git a/src/main/java/cart/domain/user/UserId.java b/src/main/java/cart/domain/user/UserId.java new file mode 100644 index 0000000000..5c9378c3e4 --- /dev/null +++ b/src/main/java/cart/domain/user/UserId.java @@ -0,0 +1,33 @@ +package cart.domain.user; + +import java.util.Objects; + +public class UserId { + + private final Long value; + + public UserId(final Long value) { + this.value = value; + } + + public Long getValue() { + return value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final UserId userId = (UserId) o; + return value.equals(userId.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/cart/dto/ErrorResponse.java b/src/main/java/cart/dto/ErrorResponse.java new file mode 100644 index 0000000000..3520eb2777 --- /dev/null +++ b/src/main/java/cart/dto/ErrorResponse.java @@ -0,0 +1,14 @@ +package cart.dto; + +public class ErrorResponse { + + private final String message; + + public ErrorResponse(final String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/cart/dto/cart/AddCartResponse.java b/src/main/java/cart/dto/cart/AddCartResponse.java new file mode 100644 index 0000000000..5f345e40a4 --- /dev/null +++ b/src/main/java/cart/dto/cart/AddCartResponse.java @@ -0,0 +1,51 @@ +package cart.dto.cart; + +import cart.domain.cart.Cart; +import cart.domain.cart.CartProduct; +import java.util.List; +import java.util.stream.Collectors; + +public class AddCartResponse { + + private final Long id; + private final List products; + + public AddCartResponse(final Long id, final List products) { + this.id = id; + this.products = products; + } + + public static AddCartResponse from(final Cart cart) { + final List productResponses = cart.getCartProducts() + .getCartProducts().stream() + .map(ProductResponse::from) + .collect(Collectors.toList()); + + return new AddCartResponse(cart.getCartId().getValue(), productResponses); + } + + public Long getId() { + return id; + } + + public List getProducts() { + return products; + } + + private static class ProductResponse { + + private final Long id; + + private ProductResponse(final Long id) { + this.id = id; + } + + private static ProductResponse from(final CartProduct cartProduct) { + return new ProductResponse(cartProduct.getProductId().getValue()); + } + + public long getId() { + return id; + } + } +} diff --git a/src/main/java/cart/dto/cart/AddProductRequest.java b/src/main/java/cart/dto/cart/AddProductRequest.java new file mode 100644 index 0000000000..844c4e71ed --- /dev/null +++ b/src/main/java/cart/dto/cart/AddProductRequest.java @@ -0,0 +1,22 @@ +package cart.dto.cart; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +public class AddProductRequest { + + @NotNull(message = "상품 id는 필수 입력값입니다.") + @Positive(message = "상품 id는 0보다 커야 합니다. 입력값 : ${validatedValue}") + private Long productId; + + private AddProductRequest() { + } + + public AddProductRequest(final Long productId) { + this.productId = productId; + } + + public Long getProductId() { + return productId; + } +} diff --git a/src/main/java/cart/dto/cart/FindCartResponse.java b/src/main/java/cart/dto/cart/FindCartResponse.java new file mode 100644 index 0000000000..acf4556325 --- /dev/null +++ b/src/main/java/cart/dto/cart/FindCartResponse.java @@ -0,0 +1,97 @@ +package cart.dto.cart; + +import cart.domain.cart.Cart; +import cart.domain.cart.CartProduct; +import cart.domain.product.Product; +import java.util.List; +import java.util.stream.Collectors; + +public class FindCartResponse { + + private final Long id; + private final List products; + + public FindCartResponse(final Long id, final List products) { + this.id = id; + this.products = products; + } + + public static FindCartResponse of(final Cart cart, final List products) { + final List productResponses = ProductResponse.of( + cart.getCartProducts().getCartProducts(), + products); + + return new FindCartResponse(cart.getCartId().getValue(), productResponses); + } + + public Long getId() { + return id; + } + + public List getProducts() { + return products; + } + + private static class ProductResponse { + + private final Long id; + private final Long productId; + private final String name; + private final String image; + private final int price; + + private ProductResponse(final Long id, final Long productId, final String name, final String image, + final int price) { + this.id = id; + this.productId = productId; + this.name = name; + this.image = image; + this.price = price; + } + + private static List of(final List cartProducts, + final List products) { + return cartProducts.stream() + .map(cartProduct -> toProductResponse(products, cartProduct)) + .collect(Collectors.toList()); + } + + private static ProductResponse toProductResponse(final List products, final CartProduct cartProduct) { + final Product product = filterProduct(products, cartProduct); + return new ProductResponse( + cartProduct.getCartProductId().getValue(), + product.getProductId().getValue(), + product.getProductName(), + product.getProductImage().getValue(), + product.getProductPrice().getValue() + ); + } + + private static Product filterProduct(final List products, final CartProduct cartProduct) { + return products.stream() + .filter(p -> p.getProductId().equals(cartProduct.getProductId())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 상품입니다.")); + } + + public Long getId() { + return id; + } + + public Long getProductId() { + return productId; + } + + public String getName() { + return name; + } + + public String getImage() { + return image; + } + + public int getPrice() { + return price; + } + } +} diff --git a/src/main/java/cart/dto/product/ProductRequest.java b/src/main/java/cart/dto/product/ProductRequest.java new file mode 100644 index 0000000000..85dbadd2fd --- /dev/null +++ b/src/main/java/cart/dto/product/ProductRequest.java @@ -0,0 +1,39 @@ +package cart.dto.product; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.PositiveOrZero; +import javax.validation.constraints.Size; + +public class ProductRequest { + + @Size(min = 1, max = 255, message = "상품명은 1자 이상 255자 이하입니다. 입력값 : ${validatedValue}") + @NotBlank(message = "상품명은 필수 입력값입니다.") + private final String name; + + @Size(min = 1, max = 2048, message = "이미지 url은 1자 이상 2048자 이하입니다. 입력값 : ${validatedValue}") + @NotBlank(message = "이미지 url은 필수 입력값입니다.") + private final String image; + + @NotNull(message = "상품 가격은 필수 입력값입니다.") + @PositiveOrZero(message = "상품 가격은 0원 이상이어야 합니다. 입력값 : ${validatedValue}") + private final Integer price; + + public ProductRequest(final String name, final String image, final Integer price) { + this.name = name; + this.image = image; + this.price = price; + } + + public String getName() { + return name; + } + + public String getImage() { + return image; + } + + public int getPrice() { + return price; + } +} diff --git a/src/main/java/cart/dto/product/ProductResponse.java b/src/main/java/cart/dto/product/ProductResponse.java new file mode 100644 index 0000000000..da1e2ec384 --- /dev/null +++ b/src/main/java/cart/dto/product/ProductResponse.java @@ -0,0 +1,63 @@ +package cart.dto.product; + +import cart.domain.product.Product; +import java.util.Objects; + +public class ProductResponse { + + private final long productId; + private final String name; + private final String image; + private final int price; + + public ProductResponse(final long productId, final String name, final String image, final int price) { + this.productId = productId; + this.name = name; + this.image = image; + this.price = price; + } + + public static ProductResponse from(final Product product) { + return new ProductResponse( + product.getProductId().getValue(), + product.getProductName(), + product.getProductImage().getValue(), + product.getProductPrice().getValue() + ); + } + + public long getProductId() { + return productId; + } + + public String getName() { + return name; + } + + public String getImage() { + return image; + } + + public int getPrice() { + return price; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ProductResponse that = (ProductResponse) o; + return productId == that.productId && price == that.price && Objects.equals(name, that.name) && Objects.equals( + image, + that.image); + } + + @Override + public int hashCode() { + return Objects.hash(productId, name, image, price); + } +} diff --git a/src/main/java/cart/dto/user/UserResponse.java b/src/main/java/cart/dto/user/UserResponse.java new file mode 100644 index 0000000000..5baca8db39 --- /dev/null +++ b/src/main/java/cart/dto/user/UserResponse.java @@ -0,0 +1,26 @@ +package cart.dto.user; + +import cart.domain.user.User; + +public class UserResponse { + + private final String email; + private final String password; + + public UserResponse(final String email, final String password) { + this.email = email; + this.password = password; + } + + public static UserResponse from(final User user) { + return new UserResponse(user.getEmail().getValue(), user.getPassword().getValue()); + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/cart/entiy/cart/CartEntity.java b/src/main/java/cart/entiy/cart/CartEntity.java new file mode 100644 index 0000000000..b32b19cedd --- /dev/null +++ b/src/main/java/cart/entiy/cart/CartEntity.java @@ -0,0 +1,36 @@ +package cart.entiy.cart; + +import cart.domain.cart.Cart; +import cart.domain.cart.CartId; +import cart.domain.user.UserId; + +public class CartEntity { + + private final CartId cartId; + private final UserId userId; + + public CartEntity(final CartId cartId, final UserId userId) { + this.cartId = cartId; + this.userId = userId; + } + + public CartEntity(final Long cartId, final Long userId) { + this(new CartId(cartId), new UserId(userId)); + } + + public CartEntity(final Long cartId, final CartEntity other) { + this(new CartId(cartId), other.userId); + } + + public static CartEntity from(final Cart cart) { + return new CartEntity(cart.getCartId(), cart.getUserId()); + } + + public CartId getCartId() { + return cartId; + } + + public UserId getUserId() { + return userId; + } +} diff --git a/src/main/java/cart/entiy/cart/CartProductEntity.java b/src/main/java/cart/entiy/cart/CartProductEntity.java new file mode 100644 index 0000000000..b5958b59e0 --- /dev/null +++ b/src/main/java/cart/entiy/cart/CartProductEntity.java @@ -0,0 +1,46 @@ +package cart.entiy.cart; + +import cart.domain.cart.CartId; +import cart.domain.cart.CartProduct; +import cart.domain.cart.CartProductId; +import cart.domain.product.ProductId; + +public class CartProductEntity { + + private final CartProductId cartProductId; + private final CartId cartId; + private final ProductId productId; + + public CartProductEntity(final CartProductId cartProductId, + final CartId cartId, + final ProductId productId) { + this.cartProductId = cartProductId; + this.cartId = cartId; + this.productId = productId; + } + + public CartProductEntity(final Long cartProductId, + final Long cartId, + final Long productId) { + this(new CartProductId(cartProductId), + new CartId(cartId), + new ProductId(productId)); + } + + + public CartId getCartEntityId() { + return cartId; + } + + public CartProductId getCartProductEntityId() { + return cartProductId; + } + + public ProductId getProductEntityId() { + return productId; + } + + public CartProduct toDomain() { + return new CartProduct(cartProductId, productId); + } +} diff --git a/src/main/java/cart/entiy/product/ProductEntity.java b/src/main/java/cart/entiy/product/ProductEntity.java new file mode 100644 index 0000000000..24c6336072 --- /dev/null +++ b/src/main/java/cart/entiy/product/ProductEntity.java @@ -0,0 +1,59 @@ +package cart.entiy.product; + +import cart.domain.product.Product; +import cart.domain.product.ProductId; + +public class ProductEntity { + + private final ProductId id; + private final String name; + private final String image; + private final int price; + + public ProductEntity(final ProductId productId, final String name, final String image, final int price) { + id = productId; + this.name = name; + this.image = image; + this.price = price; + } + + public ProductEntity(final Long id, final String name, final String image, final int price) { + this(new ProductId(id), name, image, price); + } + + public ProductEntity(final String name, final String image, final int price) { + this(new ProductId(), name, image, price); + } + + public ProductEntity(final Long id, final ProductEntity other) { + this(new ProductId(id), other.name, other.image, other.price); + } + + public static ProductEntity from(final Product product) { + return new ProductEntity( + product.getProductId(), + product.getProductName(), + product.getProductImage().getValue(), + product.getProductPrice().getValue()); + } + + public Product toDomain() { + return new Product(id, name, image, price); + } + + public ProductId getId() { + return id; + } + + public String getName() { + return name; + } + + public String getImage() { + return image; + } + + public int getPrice() { + return price; + } +} diff --git a/src/main/java/cart/entiy/user/UserEntity.java b/src/main/java/cart/entiy/user/UserEntity.java new file mode 100644 index 0000000000..fa4647359b --- /dev/null +++ b/src/main/java/cart/entiy/user/UserEntity.java @@ -0,0 +1,45 @@ +package cart.entiy.user; + +import cart.domain.user.User; +import cart.domain.user.UserId; + +public class UserEntity { + + private final UserId userId; + private final String email; + private final String password; + + public UserEntity(final UserId userId, final String email, final String password) { + this.userId = userId; + this.email = email; + this.password = password; + } + + public UserEntity(final Long userId, final UserEntity userEntity) { + this(new UserId(userId), userEntity.email, userEntity.password); + } + + + public static UserEntity from(final User user) { + return new UserEntity( + user.getUserId(), + user.getEmail().getValue(), + user.getPassword().getValue()); + } + + public User toDomain() { + return new User(userId.getValue(), email, password); + } + + public UserId getUserId() { + return userId; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/cart/event/user/UserRegisteredEvent.java b/src/main/java/cart/event/user/UserRegisteredEvent.java new file mode 100644 index 0000000000..4fe7f20751 --- /dev/null +++ b/src/main/java/cart/event/user/UserRegisteredEvent.java @@ -0,0 +1,17 @@ +package cart.event.user; + +import cart.domain.user.User; +import cart.domain.user.UserId; + +public class UserRegisteredEvent { + + private final UserId userId; + + public UserRegisteredEvent(final User user) { + userId = user.getUserId(); + } + + public UserId getUser() { + return userId; + } +} diff --git a/src/main/java/cart/exception/AlreadyAddedProductException.java b/src/main/java/cart/exception/AlreadyAddedProductException.java new file mode 100644 index 0000000000..6fb10cf2f8 --- /dev/null +++ b/src/main/java/cart/exception/AlreadyAddedProductException.java @@ -0,0 +1,8 @@ +package cart.exception; + +public class AlreadyAddedProductException extends RuntimeException { + + public AlreadyAddedProductException(final String message) { + super(message); + } +} diff --git a/src/main/java/cart/exception/CartNotFoundException.java b/src/main/java/cart/exception/CartNotFoundException.java new file mode 100644 index 0000000000..d7e35398a0 --- /dev/null +++ b/src/main/java/cart/exception/CartNotFoundException.java @@ -0,0 +1,9 @@ +package cart.exception; + +public class CartNotFoundException extends RuntimeException { + + public CartNotFoundException(final String message) { + super(message); + } + +} diff --git a/src/main/java/cart/exception/ProductInCartDeleteException.java b/src/main/java/cart/exception/ProductInCartDeleteException.java new file mode 100644 index 0000000000..cd6b027467 --- /dev/null +++ b/src/main/java/cart/exception/ProductInCartDeleteException.java @@ -0,0 +1,8 @@ +package cart.exception; + +public class ProductInCartDeleteException extends RuntimeException { + + public ProductInCartDeleteException(final String message) { + super(message); + } +} diff --git a/src/main/java/cart/exception/ProductNotFoundException.java b/src/main/java/cart/exception/ProductNotFoundException.java new file mode 100644 index 0000000000..265084aead --- /dev/null +++ b/src/main/java/cart/exception/ProductNotFoundException.java @@ -0,0 +1,8 @@ +package cart.exception; + +public class ProductNotFoundException extends RuntimeException { + + public ProductNotFoundException(final String message) { + super(message); + } +} diff --git a/src/main/java/cart/exception/UserNotFoundException.java b/src/main/java/cart/exception/UserNotFoundException.java new file mode 100644 index 0000000000..cb4bb4cda8 --- /dev/null +++ b/src/main/java/cart/exception/UserNotFoundException.java @@ -0,0 +1,8 @@ +package cart.exception; + +public class UserNotFoundException extends RuntimeException { + + public UserNotFoundException(final String message) { + super(message); + } +} diff --git a/src/main/java/cart/filter/AuthenticationFilter.java b/src/main/java/cart/filter/AuthenticationFilter.java new file mode 100644 index 0000000000..60b57464d3 --- /dev/null +++ b/src/main/java/cart/filter/AuthenticationFilter.java @@ -0,0 +1,56 @@ +package cart.filter; + +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import cart.repository.user.UserRepository; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.web.filter.OncePerRequestFilter; + +public class AuthenticationFilter extends OncePerRequestFilter { + + private static final String AUTHORIZATION = "Authorization"; + private static final String BASIC_TYPE = "Basic"; + private static final String DELIMITER = ":"; + + private final UserRepository userRepository; + + public AuthenticationFilter(final UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + protected void doFilterInternal( + final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) + throws ServletException, IOException { + final String basicHeader = getBasicAuthHeader(request.getHeader(AUTHORIZATION)); + + if (basicHeader == null) { + response.sendError(SC_UNAUTHORIZED, "Authorization basicHeader not found"); + return; + } + + final String decodedString = new String(Base64.decodeBase64(basicHeader)); + + final String[] credentials = decodedString.split(DELIMITER); + final String email = credentials[0]; + final String password = credentials[1]; + + if (!userRepository.existsByEmailAndPassword(email, password)) { + response.sendError(SC_UNAUTHORIZED, "Authorization basicHeader not found"); + } + + filterChain.doFilter(request, response); + } + + private String getBasicAuthHeader(final String authHeader) { + if (authHeader == null || !authHeader.toLowerCase().startsWith(BASIC_TYPE.toLowerCase())) { + return null; + } + return authHeader.substring(BASIC_TYPE.length()).trim(); + } +} diff --git a/src/main/java/cart/listener/InitialDataListener.java b/src/main/java/cart/listener/InitialDataListener.java new file mode 100644 index 0000000000..f99374f072 --- /dev/null +++ b/src/main/java/cart/listener/InitialDataListener.java @@ -0,0 +1,27 @@ +package cart.listener; + +import cart.domain.product.Product; +import cart.repository.product.ProductRepository; +import cart.service.user.UserCommandService; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class InitialDataListener { + + private final UserCommandService userCommandService; + private final ProductRepository productRepository; + + public InitialDataListener(final UserCommandService userCommandService, final ProductRepository productRepository) { + this.userCommandService = userCommandService; + this.productRepository = productRepository; + } + + @EventListener(ApplicationReadyEvent.class) + public void setDefaultUsers() { + userCommandService.save("a@naver.com", "1234"); + userCommandService.save("b@naver.com", "1234"); + productRepository.save(new Product("라빈", "https://avatars.githubusercontent.com/u/45747236?s=200&v=4", 100)); + } +} diff --git a/src/main/java/cart/repository/cart/CartRepository.java b/src/main/java/cart/repository/cart/CartRepository.java new file mode 100644 index 0000000000..4980091d6c --- /dev/null +++ b/src/main/java/cart/repository/cart/CartRepository.java @@ -0,0 +1,12 @@ +package cart.repository.cart; + +import cart.domain.cart.Cart; +import cart.domain.user.UserId; +import java.util.Optional; + +public interface CartRepository { + + Cart save(Cart cart); + + Optional findByUserId(UserId userId); +} diff --git a/src/main/java/cart/repository/cart/H2CartRepository.java b/src/main/java/cart/repository/cart/H2CartRepository.java new file mode 100644 index 0000000000..0655ab6d82 --- /dev/null +++ b/src/main/java/cart/repository/cart/H2CartRepository.java @@ -0,0 +1,78 @@ +package cart.repository.cart; + +import cart.domain.cart.Cart; +import cart.domain.cart.CartId; +import cart.domain.cart.CartProduct; +import cart.domain.user.UserId; +import cart.entiy.cart.CartEntity; +import cart.entiy.cart.CartProductEntity; +import cart.repository.cart.dao.CartDao; +import cart.repository.cart.dao.CartProductDao; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class H2CartRepository implements CartRepository { + + private final CartDao cartDao; + private final CartProductDao cartProductDao; + + public H2CartRepository(final JdbcTemplate jdbcTemplate) { + cartDao = new CartDao(jdbcTemplate); + cartProductDao = new CartProductDao(jdbcTemplate); + } + + @Override + public Cart save(final Cart cart) { + CartEntity cartEntity = CartEntity.from(cart); + if (cartEntity.getCartId().getValue() == null) { + cartEntity = cartDao.save(cartEntity); + } + + saveCartProducts(cart, cartEntity); + return findCart(cart.getUserId(), cartEntity); + } + + private void saveCartProducts(final Cart cart, final CartEntity cartEntity) { + cartProductDao.deleteAllByCartId(cartEntity.getCartId()); + final List cartProducts = cart.getCartProducts().getCartProducts(); + final List cartProductEntities = toCartProductEntities(cartProducts, + cartEntity.getCartId()); + cartProductDao.insertAll(cartProductEntities); + } + + private List toCartProductEntities(final List cartProducts, + final CartId cartEntityId) { + return cartProducts.stream() + .map(cartProduct -> toCartProductEntity(cartEntityId, cartProduct)) + .collect(Collectors.toList()); + } + + private CartProductEntity toCartProductEntity(final CartId cartId, final CartProduct cartProduct) { + return new CartProductEntity( + cartProduct.getCartProductId(), + cartId, + cartProduct.getProductId() + ); + } + + @Override + public Optional findByUserId(final UserId userId) { + final CartEntity cartEntity = cartDao.findByUserId(userId); + if (cartEntity == null) { + return Optional.empty(); + } + return Optional.of(findCart(userId, cartEntity)); + } + + private Cart findCart(final UserId userId, final CartEntity cartEntity) { + final List updatedCartProductEntities = cartProductDao.findAllByCartId( + cartEntity.getCartId()).stream() + .map(CartProductEntity::toDomain) + .collect(Collectors.toList()); + return new Cart(cartEntity.getCartId(), userId, updatedCartProductEntities); + } +} diff --git a/src/main/java/cart/repository/cart/dao/CartDao.java b/src/main/java/cart/repository/cart/dao/CartDao.java new file mode 100644 index 0000000000..3d03f7f24c --- /dev/null +++ b/src/main/java/cart/repository/cart/dao/CartDao.java @@ -0,0 +1,42 @@ +package cart.repository.cart.dao; + +import cart.domain.user.UserId; +import cart.entiy.cart.CartEntity; +import java.util.HashMap; +import java.util.Map; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; + +public class CartDao { + + private static final RowMapper rowMapper = (rs, rowNum) -> new CartEntity( + rs.getLong("cart_id"), + rs.getLong("member_id") + ); + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public CartDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("cart") + .usingGeneratedKeyColumns("cart_id"); + } + + public CartEntity save(final CartEntity cartEntity) { + final Map parameters = new HashMap<>(2); + parameters.put("cart_id", cartEntity.getCartId().getValue()); + parameters.put("member_id", cartEntity.getUserId().getValue()); + final long cartId = simpleJdbcInsert.executeAndReturnKey(parameters).longValue(); + return new CartEntity(cartId, cartEntity); + } + + public CartEntity findByUserId(final UserId userId) { + return jdbcTemplate.queryForObject( + "SELECT cart_id,member_id FROM cart WHERE member_id = ?", + rowMapper, + userId.getValue() + ); + } +} diff --git a/src/main/java/cart/repository/cart/dao/CartProductDao.java b/src/main/java/cart/repository/cart/dao/CartProductDao.java new file mode 100644 index 0000000000..7b860288df --- /dev/null +++ b/src/main/java/cart/repository/cart/dao/CartProductDao.java @@ -0,0 +1,49 @@ +package cart.repository.cart.dao; + +import cart.domain.cart.CartId; +import cart.entiy.cart.CartProductEntity; +import java.util.List; +import java.util.Map; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; + +public class CartProductDao { + + private static final RowMapper rowMapper = (rs, rowNum) -> new CartProductEntity( + rs.getLong("cart_product_id"), + rs.getLong("cart_id"), + rs.getLong("product_id") + ); + + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + + public CartProductDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("cart_product") + .usingGeneratedKeyColumns("cart_product_id"); + } + + public void insertAll(final List cartProductEntities) { + @SuppressWarnings("unchecked") final Map[] parameters = cartProductEntities.stream() + .map(cartProductEntity -> Map.of( + "cart_id", cartProductEntity.getCartEntityId().getValue(), + "product_id", cartProductEntity.getProductEntityId().getValue() + )) + .toArray(Map[]::new); + simpleJdbcInsert.executeBatch(parameters); + } + + public void deleteAllByCartId(final CartId cartId) { + jdbcTemplate.update("DELETE FROM cart_product WHERE cart_id = ?", cartId.getValue()); + } + + public List findAllByCartId(final CartId cartId) { + return jdbcTemplate.query("SELECT cart_product_id,cart_id,product_id FROM cart_product WHERE cart_id = ?", + rowMapper, + cartId.getValue() + ); + } +} diff --git a/src/main/java/cart/repository/product/H2ProductRepository.java b/src/main/java/cart/repository/product/H2ProductRepository.java new file mode 100644 index 0000000000..768fb4da8d --- /dev/null +++ b/src/main/java/cart/repository/product/H2ProductRepository.java @@ -0,0 +1,104 @@ +package cart.repository.product; + +import cart.domain.product.Product; +import cart.entiy.product.ProductEntity; +import cart.exception.ProductInCartDeleteException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; + +@Repository +public class H2ProductRepository implements ProductRepository { + + private final RowMapper productEntityRowMapper = (resultSet, rowNum) -> { + long productId = resultSet.getLong("product_id"); + String name = resultSet.getString("name"); + String image = resultSet.getString("image"); + int price = resultSet.getInt("price"); + return new ProductEntity(productId, name, image, price); + }; + + private final SimpleJdbcInsert simpleJdbcInsert; + private final JdbcTemplate jdbcTemplate; + + + public H2ProductRepository(final JdbcTemplate jdbcTemplate) { + simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("product") + .usingGeneratedKeyColumns("product_id"); + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Product save(final Product product) { + final ProductEntity productEntity = ProductEntity.from(product); + final Map parameters = new HashMap<>(4); + parameters.put("product_id", productEntity.getId().getValue()); + parameters.put("name", productEntity.getName()); + parameters.put("image", productEntity.getImage()); + parameters.put("price", productEntity.getPrice()); + final long productId = simpleJdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters)).longValue(); + return new ProductEntity(productId, productEntity).toDomain(); + } + + @Override + public Product update(final Product product) { + final ProductEntity productEntity = ProductEntity.from(product); + final String sql = "UPDATE PRODUCT SET name=?, image=?, price=? WHERE product_id=?"; + jdbcTemplate.update(sql, productEntity.getName(), productEntity.getImage(), productEntity.getPrice(), + productEntity.getId().getValue()); + return productEntity.toDomain(); + } + + @Override + public List findAll() { + final String sql = "SELECT product_id, name, image, price FROM PRODUCT"; + return jdbcTemplate.query(sql, productEntityRowMapper) + .stream() + .map(ProductEntity::toDomain) + .collect(Collectors.toList()); + } + + @Override + public Optional findById(final Long id) { + final String sql = "SELECT product_id, name, image, price FROM PRODUCT WHERE product_id=?"; + try { + final ProductEntity result = jdbcTemplate.queryForObject(sql, productEntityRowMapper, id); + return Optional.of(result).map(ProductEntity::toDomain); + } catch (final EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + @Override + public void deleteById(final Long id) { + final String sql = "DELETE FROM PRODUCT WHERE product_id=?"; + try { + jdbcTemplate.update(sql, id); + } catch (final DataIntegrityViolationException e) { + throw new ProductInCartDeleteException("상품이 장바구니에 존재합니다. 장바구니에서 먼저 삭제해주세요."); + } + } + + @Override + public List findAllById(final List ids) { + final String inParams = ids.stream() + .map(id -> "?") + .collect(Collectors.joining(",")); + final String sql = String.format( + "SELECT product_id, name, image, price FROM PRODUCT WHERE product_id IN (%s)", inParams); + return jdbcTemplate.query(sql, productEntityRowMapper, ids.toArray()) + .stream() + .map(ProductEntity::toDomain) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/cart/repository/product/ProductRepository.java b/src/main/java/cart/repository/product/ProductRepository.java new file mode 100644 index 0000000000..e02d96a1f2 --- /dev/null +++ b/src/main/java/cart/repository/product/ProductRepository.java @@ -0,0 +1,20 @@ +package cart.repository.product; + +import cart.domain.product.Product; +import java.util.List; +import java.util.Optional; + +public interface ProductRepository { + + Product save(Product product); + + Product update(Product product); + + List findAll(); + + Optional findById(Long id); + + void deleteById(Long id); + + List findAllById(List ids); +} diff --git a/src/main/java/cart/repository/user/H2UserRepository.java b/src/main/java/cart/repository/user/H2UserRepository.java new file mode 100644 index 0000000000..4b66779896 --- /dev/null +++ b/src/main/java/cart/repository/user/H2UserRepository.java @@ -0,0 +1,69 @@ +package cart.repository.user; + +import cart.domain.user.User; +import cart.domain.user.UserId; +import cart.entiy.user.UserEntity; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; + +@Repository +public class H2UserRepository implements UserRepository { + + private final RowMapper userEntityRowMapper = (rs, rowNum) -> new UserEntity( + new UserId(rs.getLong("member_id")), + rs.getString("email"), + rs.getString("password")); + + private final SimpleJdbcInsert simpleJdbcInsert; + private final JdbcTemplate jdbcTemplate; + + public H2UserRepository(final JdbcTemplate jdbcTemplate) { + simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("member") + .usingGeneratedKeyColumns("member_id"); + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public User save(final User user) { + final UserEntity userEntity = UserEntity.from(user); + final Map parameters = new HashMap<>(3); + parameters.put("member_id", userEntity.getUserId().getValue()); + parameters.put("email", userEntity.getEmail()); + parameters.put("password", userEntity.getPassword()); + final long userId = simpleJdbcInsert.executeAndReturnKey(parameters).longValue(); + return new UserEntity(userId, userEntity).toDomain(); + } + + @Override + public boolean existsByEmailAndPassword(final String email, final String password) { + final String sql = "SELECT EXISTS(SELECT 1 FROM MEMBER WHERE email=? AND password=?)"; + + return jdbcTemplate.queryForObject(sql, Boolean.class, email, password); + } + + @Override + public Optional findByEmail(final String email) { + final String sql = "SELECT member_id, email, password FROM MEMBER WHERE email=?"; + try { + return Optional.of(jdbcTemplate.queryForObject(sql, userEntityRowMapper, email).toDomain()); + } catch (final Exception e) { + return Optional.empty(); + } + } + + @Override + public List findAll() { + final String sql = "SELECT member_id, email, password FROM MEMBER"; + return jdbcTemplate.query(sql, userEntityRowMapper).stream() + .map(UserEntity::toDomain) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/cart/repository/user/UserRepository.java b/src/main/java/cart/repository/user/UserRepository.java new file mode 100644 index 0000000000..c0258e00ac --- /dev/null +++ b/src/main/java/cart/repository/user/UserRepository.java @@ -0,0 +1,16 @@ +package cart.repository.user; + +import cart.domain.user.User; +import java.util.List; +import java.util.Optional; + +public interface UserRepository { + + User save(User user); + + boolean existsByEmailAndPassword(String email, String password); + + Optional findByEmail(String email); + + List findAll(); +} diff --git a/src/main/java/cart/resolver/AuthInfo.java b/src/main/java/cart/resolver/AuthInfo.java new file mode 100644 index 0000000000..a5b99751f5 --- /dev/null +++ b/src/main/java/cart/resolver/AuthInfo.java @@ -0,0 +1,14 @@ +package cart.resolver; + +public class AuthInfo { + + private final String email; + + public AuthInfo(final String email) { + this.email = email; + } + + public String getEmail() { + return email; + } +} diff --git a/src/main/java/cart/resolver/Authentication.java b/src/main/java/cart/resolver/Authentication.java new file mode 100644 index 0000000000..3fbc9fbe4b --- /dev/null +++ b/src/main/java/cart/resolver/Authentication.java @@ -0,0 +1,13 @@ +package cart.resolver; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(PARAMETER) +public @interface Authentication { + +} diff --git a/src/main/java/cart/resolver/AuthenticationResolver.java b/src/main/java/cart/resolver/AuthenticationResolver.java new file mode 100644 index 0000000000..334ebc24c9 --- /dev/null +++ b/src/main/java/cart/resolver/AuthenticationResolver.java @@ -0,0 +1,40 @@ +package cart.resolver; + +import static org.apache.tomcat.util.codec.binary.Base64.decodeBase64; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + + +public class AuthenticationResolver implements HandlerMethodArgumentResolver { + + private static final String AUTHORIZATION = "Authorization"; + private static final String BASIC_TYPE = "Basic"; + private static final int EMAIL_INDEX = 0; + + @Override + public boolean supportsParameter(final MethodParameter parameter) { + if (parameter.hasParameterAnnotation(Authentication.class)) { + return parameter.getParameterType().isAssignableFrom(AuthInfo.class); + } + return false; + } + + @Override + public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, + final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) { + final String header = webRequest.getHeader(AUTHORIZATION); + + if (header == null || !header.toLowerCase().startsWith(BASIC_TYPE.toLowerCase())) { + throw new IllegalArgumentException(); + } + + final String authHeaderValue = header.substring(BASIC_TYPE.length()).trim(); + + final String[] credentials = new String(decodeBase64(authHeaderValue)).split(":"); + return new AuthInfo(credentials[EMAIL_INDEX]); + } +} diff --git a/src/main/java/cart/service/cart/CartCommandService.java b/src/main/java/cart/service/cart/CartCommandService.java new file mode 100644 index 0000000000..762117c9c6 --- /dev/null +++ b/src/main/java/cart/service/cart/CartCommandService.java @@ -0,0 +1,60 @@ +package cart.service.cart; + +import cart.domain.cart.Cart; +import cart.domain.product.Product; +import cart.domain.user.User; +import cart.event.user.UserRegisteredEvent; +import cart.exception.ProductNotFoundException; +import cart.exception.UserNotFoundException; +import cart.repository.cart.CartRepository; +import cart.service.product.ProductQueryService; +import cart.service.user.UserQueryService; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class CartCommandService { + + private final CartRepository cartRepository; + private final UserQueryService userQueryService; + private final ProductQueryService productQueryService; + + public CartCommandService(final CartRepository cartRepository, + final UserQueryService userQueryService, + final ProductQueryService productQueryService) { + this.cartRepository = cartRepository; + this.userQueryService = userQueryService; + this.productQueryService = productQueryService; + } + + @EventListener(UserRegisteredEvent.class) + public void createCart(final UserRegisteredEvent userRegisteredEvent) { + final Cart cart = new Cart(userRegisteredEvent.getUser()); + cartRepository.save(cart); + } + + public Cart addProduct(final Long productId, final String email) { + final Cart cart = userQueryService.findByEmail(email) + .map(User::getUserId) + .flatMap(cartRepository::findByUserId) + .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다.")); + + final Product product = productQueryService.findById(productId) + .orElseThrow(() -> new ProductNotFoundException("상품을 찾을 수 없습니다.")); + + cart.addProduct(product.getProductId()); + return cartRepository.save(cart); + } + + public void deleteProduct(final Long cartProductId, final String email) { + final Cart cart = userQueryService.findByEmail(email) + .map(User::getUserId) + .flatMap(cartRepository::findByUserId) + .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다.")); + + cart.deleteProduct(cartProductId); + cartRepository.save(cart); + } +} diff --git a/src/main/java/cart/service/cart/CartQueryService.java b/src/main/java/cart/service/cart/CartQueryService.java new file mode 100644 index 0000000000..30d366750b --- /dev/null +++ b/src/main/java/cart/service/cart/CartQueryService.java @@ -0,0 +1,29 @@ +package cart.service.cart; + +import cart.domain.cart.Cart; +import cart.domain.user.User; +import cart.exception.UserNotFoundException; +import cart.repository.cart.CartRepository; +import cart.service.user.UserQueryService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class CartQueryService { + + private final UserQueryService userQueryService; + private final CartRepository cartRepository; + + public CartQueryService(final UserQueryService userQueryService, final CartRepository cartRepository) { + this.userQueryService = userQueryService; + this.cartRepository = cartRepository; + } + + public Cart findByEmail(final String email) { + return userQueryService.findByEmail(email) + .map(User::getUserId) + .flatMap(cartRepository::findByUserId) + .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/cart/service/product/ProductCommandService.java b/src/main/java/cart/service/product/ProductCommandService.java new file mode 100644 index 0000000000..0f2a1cb551 --- /dev/null +++ b/src/main/java/cart/service/product/ProductCommandService.java @@ -0,0 +1,29 @@ +package cart.service.product; + +import cart.domain.product.Product; +import cart.repository.product.ProductRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class ProductCommandService { + + private final ProductRepository productRepository; + + public ProductCommandService(final ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public void delete(final long id) { + productRepository.deleteById(id); + } + + public Product create(final String name, final String image, final int price) { + return productRepository.save(new Product(name, image, price)); + } + + public Product update(final long productId, final String name, final String image, final int price) { + return productRepository.update(new Product(productId, name, image, price)); + } +} diff --git a/src/main/java/cart/service/product/ProductQueryService.java b/src/main/java/cart/service/product/ProductQueryService.java new file mode 100644 index 0000000000..bb661f53c2 --- /dev/null +++ b/src/main/java/cart/service/product/ProductQueryService.java @@ -0,0 +1,31 @@ +package cart.service.product; + +import cart.domain.product.Product; +import cart.repository.product.ProductRepository; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class ProductQueryService { + + private final ProductRepository productRepository; + + public ProductQueryService(final ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public List findAll() { + return productRepository.findAll(); + } + + public List findAllByIds(final List ids) { + return productRepository.findAllById(ids); + } + + public Optional findById(final Long id) { + return productRepository.findById(id); + } +} diff --git a/src/main/java/cart/service/user/UserCommandService.java b/src/main/java/cart/service/user/UserCommandService.java new file mode 100644 index 0000000000..39fc45269a --- /dev/null +++ b/src/main/java/cart/service/user/UserCommandService.java @@ -0,0 +1,29 @@ +package cart.service.user; + +import cart.domain.user.User; +import cart.event.user.UserRegisteredEvent; +import cart.repository.user.UserRepository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class UserCommandService { + + private final UserRepository userRepository; + private final ApplicationEventPublisher applicationEventPublisher; + + public UserCommandService(final UserRepository userRepository, + final ApplicationEventPublisher applicationEventPublisher) { + this.userRepository = userRepository; + this.applicationEventPublisher = applicationEventPublisher; + } + + + public User save(final String email, final String password) { + final User user = userRepository.save(new User(email, password)); + applicationEventPublisher.publishEvent(new UserRegisteredEvent(user)); + return user; + } +} diff --git a/src/main/java/cart/service/user/UserQueryService.java b/src/main/java/cart/service/user/UserQueryService.java new file mode 100644 index 0000000000..47f21c94e9 --- /dev/null +++ b/src/main/java/cart/service/user/UserQueryService.java @@ -0,0 +1,27 @@ +package cart.service.user; + +import cart.domain.user.User; +import cart.repository.user.UserRepository; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class UserQueryService { + + private final UserRepository userRepository; + + public UserQueryService(final UserRepository userRepository) { + this.userRepository = userRepository; + } + + public List findAll() { + return userRepository.findAll(); + } + + public Optional findByEmail(final String email) { + return userRepository.findByEmail(email); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000000..9e6d49b8e5 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000000..43c0e9465b --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS PRODUCT +( + product_id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, + image VARCHAR(2048) NOT NULL, + price INT NOT NULL, + PRIMARY KEY (product_id) +); + +CREATE TABLE IF NOT EXISTS MEMBER +( + member_id BIGINT NOT NULL AUTO_INCREMENT, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + UNIQUE (email), + PRIMARY KEY (member_id) +); + +CREATE TABLE IF NOT EXISTS CART +( + cart_id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + PRIMARY KEY (cart_id), + FOREIGN KEY (member_id) REFERENCES MEMBER (member_id) +); + +CREATE TABLE IF NOT EXISTS CART_PRODUCT +( + cart_product_id BIGINT NOT NULL AUTO_INCREMENT, + cart_id BIGINT NOT NULL, + product_id BIGINT NOT NULL, + PRIMARY KEY (cart_product_id), + FOREIGN KEY (cart_id) REFERENCES CART (cart_id), + FOREIGN KEY (product_id) REFERENCES PRODUCT (product_id) +); diff --git a/src/main/resources/static/css/admin.css b/src/main/resources/static/css/admin.css index c4204232b0..e2a570f36f 100644 --- a/src/main/resources/static/css/admin.css +++ b/src/main/resources/static/css/admin.css @@ -1,91 +1,100 @@ /* Table styles */ table { - width: 100%; - border-collapse: collapse; - margin-bottom: 20px; - background-color: #fff; - box-shadow: 0px 3px 3px rgb(0 0 0 / 10%); + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + background-color: #fff; + box-shadow: 0px 3px 3px rgb(0 0 0 / 10%); } + th, td { - padding: 10px; - border: 1px solid #333; - text-align: left; + padding: 10px; + border: 1px solid #333; + text-align: left; } + th { - font-weight: bold; + font-weight: bold; } /* Button styles */ .btn-group { - text-align: right; - margin-bottom: 10px; + text-align: right; + margin-bottom: 10px; } button { - background-color: #000000; - color: #ffffff; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 16px; - margin-right: 10px; + background-color: #000000; + color: #ffffff; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + margin-right: 10px; } + /* Image styles */ img { - max-width: 100px; + max-width: 100px; } + /* Modal styles */ .modal { - display: none; - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0, 0, 0, 0.4); + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); } + .modal-content { - background-color: #ffffff; - margin: 10% auto; - padding: 20px; - border: 1px solid #888; - width: 50%; - position: relative; + background-color: #ffffff; + margin: 10% auto; + padding: 20px; + border: 1px solid #888; + width: 50%; + position: relative; } + .close { - position: absolute; - right: 10px; - top: 5px; - color: #aaaaaa; - font-size: 28px; - font-weight: bold; - cursor: pointer; + position: absolute; + right: 10px; + top: 5px; + color: #aaaaaa; + font-size: 28px; + font-weight: bold; + cursor: pointer; } + /* Form styles */ label { - display: block; - margin-bottom: 10px; - font-weight: bold; + display: block; + margin-bottom: 10px; + font-weight: bold; } + input[type="text"], input[type="number"] { - padding: 10px; - border-radius: 4px; - border: 1px solid #cccccc; - width: 100%; - margin-bottom: 20px; - font-size: 16px; + padding: 10px; + border-radius: 4px; + border: 1px solid #cccccc; + width: 100%; + margin-bottom: 20px; + font-size: 16px; } + input[type="submit"] { - background-color: #000000; - color: #ffffff; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 16px; -} \ No newline at end of file + background-color: #000000; + color: #ffffff; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} diff --git a/src/main/resources/static/css/base.css b/src/main/resources/static/css/base.css index 7c1fef6dfe..d96b637156 100644 --- a/src/main/resources/static/css/base.css +++ b/src/main/resources/static/css/base.css @@ -1,76 +1,76 @@ /* 전체 스타일 */ * { - box-sizing: border-box; - font-family: 'Open Sans', sans-serif; - margin: 0; - padding: 0; - list-style: none; + box-sizing: border-box; + font-family: 'Open Sans', sans-serif; + margin: 0; + padding: 0; + list-style: none; } body { - font-size: 14px; - color: #555; - line-height: 1.4; - background-color: #f4f4f4; - margin: 0; - padding: 0; - min-width: 300px; + font-size: 14px; + color: #555; + line-height: 1.4; + background-color: #f4f4f4; + margin: 0; + padding: 0; + min-width: 300px; } /* 헤더 스타일 */ .gnb { - background-color: #fff; - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); - padding: 20px; + background-color: #fff; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); + padding: 20px; } .gnb .gnb-title { - color: #333; - font-weight: 600; + color: #333; + font-weight: 600; } .gnb .gnb-group { - display: flex; - justify-content: right; + display: flex; + justify-content: right; } .gnb .gnb-group li { - margin: 0 10px; + margin: 0 10px; } .gnb .gnb-group a { - color: #333; - font-weight: 600; - text-decoration: none; + color: #333; + font-weight: 600; + text-decoration: none; } .gnb .gnb-group a:hover { - color: #e84c3d; + color: #e84c3d; } .gnb .gnb-group .nav-admin { - position: relative; - margin-left: 15px; + position: relative; + margin-left: 15px; } .gnb .gnb-group .nav-admin::before { - content: '|'; - position: absolute; - left: -15px; + content: '|'; + position: absolute; + left: -15px; } .gnb .gnb-group .nav-admin a:hover { - color: #457b9d; + color: #457b9d; } .container { - margin: 0 auto; - padding: 50px 20px; + margin: 0 auto; + padding: 50px 20px; } h1 { - font-weight: 600; - font-size: 24px; - text-transform: uppercase; - margin: 0 0 20px; -} \ No newline at end of file + font-weight: 600; + font-size: 24px; + text-transform: uppercase; + margin: 0 0 20px; +} diff --git a/src/main/resources/static/css/cart.css b/src/main/resources/static/css/cart.css index a489652d5b..16e923b925 100644 --- a/src/main/resources/static/css/cart.css +++ b/src/main/resources/static/css/cart.css @@ -1,64 +1,64 @@ .container { - max-width: 800px; + max-width: 800px; } .cart-item { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - margin-bottom: 20px; - padding: 20px; - background-color: #fff; - box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease-in-out; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding: 20px; + background-color: #fff; + box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease-in-out; } .cart-item:hover { - box-shadow: 0px 6px 6px rgba(0, 0, 0, 0.2); - transform: translateY(-5px); + box-shadow: 0px 6px 6px rgba(0, 0, 0, 0.2); + transform: translateY(-5px); } .cart-item-info { - display: flex; - align-items: center; - margin-bottom: 10px; - width: 100%; + display: flex; + align-items: center; + margin-bottom: 10px; + width: 100%; } .cart-item-name { - font-weight: 600; - font-size: 18px; - margin: 0 20px 0 0; + font-weight: 600; + font-size: 18px; + margin: 0 20px 0 0; } .cart-item-price { - font-weight: 600; - font-size: 20px; - margin-right: 20px; + font-weight: 600; + font-size: 20px; + margin-right: 20px; } .cart-item img { - width: 80px; - height: 80px; - margin-right: 20px; - object-fit: cover; + width: 80px; + height: 80px; + margin-right: 20px; + object-fit: cover; } .cart-item-delete { - margin-left: auto; - background-color: #c0392b; - color: #fff; - font-weight: 600; - font-size: 14px; - text-transform: uppercase; - border: none; - padding: 10px 15px; - border-radius: 3px; - cursor: pointer; - transition: all 0.3s ease-in-out; + margin-left: auto; + background-color: #c0392b; + color: #fff; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + border: none; + padding: 10px 15px; + border-radius: 3px; + cursor: pointer; + transition: all 0.3s ease-in-out; } .cart-item-delete:hover { - background-color: #e74c3c; -} \ No newline at end of file + background-color: #e74c3c; +} diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index a87bcd718d..633ef2477a 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -1,88 +1,88 @@ /* 메인 스타일 */ .container { - display: flex; - justify-content: center; - text-align: left; - margin: 0; - padding: 50px 0; + display: flex; + justify-content: center; + text-align: left; + margin: 0; + padding: 50px 0; } .product-grid { - display: flex; - justify-content: center; - flex-wrap: wrap; - margin: 0 auto; - padding: 0; - text-align: center; + display: flex; + justify-content: center; + flex-wrap: wrap; + margin: 0 auto; + padding: 0; + text-align: center; } .product { - background-color: #fff; - border: 1px solid #e6e6e6; - border-radius: 0.5rem; - margin: 1rem; - overflow: hidden; - width: 300px; + background-color: #fff; + border: 1px solid #e6e6e6; + border-radius: 0.5rem; + margin: 1rem; + overflow: hidden; + width: 300px; } .product:hover { - box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.1); } .product a { - display: block; - color: #333; - text-decoration: none; + display: block; + color: #333; + text-decoration: none; } .product-image img { - display: block; - width: 100%; - height: 300px; - object-fit: cover; + display: block; + width: 100%; + height: 300px; + object-fit: cover; } .product-info { - padding: 10px; - text-align: left; - display: flex; - justify-content: space-between; - align-items: center; + padding: 10px; + text-align: left; + display: flex; + justify-content: space-between; + align-items: center; } .product-btn { - margin-left: auto; - background-color: #c0392b; - color: #fff; - font-weight: 600; - font-size: 14px; - text-transform: uppercase; - border: none; - padding: 10px 15px; - border-radius: 3px; - cursor: pointer; - transition: all 0.3s ease-in-out; + margin-left: auto; + background-color: #c0392b; + color: #fff; + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + border: none; + padding: 10px 15px; + border-radius: 3px; + cursor: pointer; + transition: all 0.3s ease-in-out; } .product-name { - font-size: 18px; - font-weight: bold; - margin-bottom: 10px; + font-size: 18px; + font-weight: bold; + margin-bottom: 10px; } .product-price { - color: #e84c3d; - font-size: 16px; - font-weight: bold; + color: #e84c3d; + font-size: 16px; + font-weight: bold; } @media screen and (max-width: 768px) { - .product-grid { - width: 100%; - margin: 0 auto; - } + .product-grid { + width: 100%; + margin: 0 auto; + } - .product { - width: 100%; - } -} \ No newline at end of file + .product { + width: 100%; + } +} diff --git a/src/main/resources/static/js/admin.js b/src/main/resources/static/js/admin.js index 72465e1e4e..d3f7f5419d 100644 --- a/src/main/resources/static/js/admin.js +++ b/src/main/resources/static/js/admin.js @@ -1,80 +1,83 @@ const modal = document.getElementById('modal'); const showAddModal = () => { - modal.dataset.formType = 'add'; - modal.style.display = 'block'; + modal.dataset.formType = 'add'; + modal.style.display = 'block'; }; const showEditModal = (product) => { - const elements = modal.getElementsByTagName('input'); - for (const element of elements) { - element.value = product[element.getAttribute('name')]; - } - modal.dataset.formType = 'edit'; - modal.dataset.productId = product.id; - modal.style.display = 'block'; + const elements = modal.getElementsByTagName('input'); + for (const element of elements) { + element.value = product[element.getAttribute('name')]; + } + modal.dataset.formType = 'edit'; + modal.dataset.productId = product.id; + modal.style.display = 'block'; }; const hideAddModal = () => { - modal.style.display = 'none'; - const elements = modal.getElementsByTagName('input'); - for (const element of elements) { - element.value = ''; - } + modal.style.display = 'none'; + const elements = modal.getElementsByTagName('input'); + for (const element of elements) { + element.value = ''; + } } const form = document.getElementById('form'); form.addEventListener('submit', (event) => { - event.preventDefault(); + event.preventDefault(); - const formData = new FormData(event.target); - let product = {}; - for (const entry of formData.entries()) { - const [key, value] = entry; - product[key] = value; - } + const formData = new FormData(event.target); + let product = {}; + for (const entry of formData.entries()) { + const [key, value] = entry; + product[key] = value; + } - if (modal.dataset.formType === 'edit') { - product['id'] = modal.dataset.productId; - updateProduct(product); - return; - } + if (modal.dataset.formType === 'edit') { + product['id'] = modal.dataset.productId; + updateProduct(product); + return; + } - createProduct(product); + createProduct(product); }); -// TODO: [1단계] 상품 관리 CRUD API에 맞게 변경 const createProduct = (product) => { - axios.request({ - url: '', - }).then((response) => { - window.location.reload(); - }).catch((error) => { - console.error(error); - }); + console.log("send request!!") + axios.request({ + url: '/products', + method: 'POST', + data: product + }).then((response) => { + window.location.reload(); + }).catch((error) => { + console.error(error); + }); }; -// TODO: [1단계] 상품 관리 CRUD API에 맞게 변경 const updateProduct = (product) => { - const { id } = product; + const {id} = product; - axios.request({ - url: '', - }).then((response) => { - window.location.reload(); - }).catch((error) => { - console.error(error); - }); + axios.request({ + url: `/products/${id}`, + method: 'PUT', + data: product + }).then((response) => { + window.location.reload(); + }).catch((error) => { + console.error(error); + }); }; -// TODO: [1단계] 상품 관리 CRUD API에 맞게 변경 const deleteProduct = (id) => { - axios.request({ - url: '', - }).then((response) => { - window.location.reload(); - }).catch((error) => { - console.error(error); - }); + axios.request({ + url: `/products/${id}`, + method: 'DELETE' + }).then((response) => { + window.location.reload(); + }).catch((error) => { + console.error(error); + }); }; diff --git a/src/main/resources/static/js/cart.js b/src/main/resources/static/js/cart.js index d0a961cc5d..ace5538394 100644 --- a/src/main/resources/static/js/cart.js +++ b/src/main/resources/static/js/cart.js @@ -1,41 +1,49 @@ const addCartItem = (productId) => { - const credentials = localStorage.getItem('credentials'); - if (!credentials) { - alert('사용자 정보가 없습니다.'); - window.location.href = '/settings'; - return; - } + const credentials = localStorage.getItem('credentials'); + if (!credentials) { + alert('사용자 정보가 없습니다.'); + window.location.href = '/settings'; + return; + } - // TODO: [2단계] 장바구니 CRUD API에 맞게 변경 - axios.request({ - url: '', - headers: { - 'Authorization': `Basic ${credentials}` - } - }).then((response) => { - alert('장바구니에 담았습니다.'); - }).catch((error) => { - console.error(error); - }); + // TODO: [2단계] 장바구니 CRUD API에 맞게 변경 + axios.request({ + url: '/carts/cartproduct', + method: 'POST', + data: { + productId + }, + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-type': 'application/json' + } + }).then((response) => { + alert('장바구니에 담았습니다.'); + }).catch((error) => { + console.error(error); + alert(error.response?.data?.message || "예상치 못한 예외가 발생했습니다"); + }); } const removeCartItem = (id) => { - const credentials = localStorage.getItem('credentials'); - if (!credentials) { - alert('사용자 정보가 없습니다.'); - window.location.href = '/settings'; - return; - } + const credentials = localStorage.getItem('credentials'); + if (!credentials) { + alert('사용자 정보가 없습니다.'); + window.location.href = '/settings'; + return; + } - // TODO: [2단계] 장바구니 CRUD API에 맞게 변경 - axios.request({ - url: '', - headers: { - 'Authorization': `Basic ${credentials}` - } - }).then((response) => { - window.location.reload(); - }).catch((error) => { - console.error(error); - }); + // TODO: [2단계] 장바구니 CRUD API에 맞게 변경 + axios.request({ + url: '/carts/cartproduct/' + id, + method: 'DELETE', + headers: { + 'Authorization': `Basic ${credentials}` + } + }).then((response) => { + window.location.reload(); + }).catch((error) => { + console.error(error); + alert('사용자 정보가 없습니다.'); + }); } diff --git a/src/main/resources/static/js/settings.js b/src/main/resources/static/js/settings.js index dd7dcae91a..310370dbb4 100644 --- a/src/main/resources/static/js/settings.js +++ b/src/main/resources/static/js/settings.js @@ -1,7 +1,7 @@ const selectMember = (member) => { - - const { email, password } = member; - const string = `${email}:${password}`; - localStorage.setItem('credentials', btoa(string)); - alert(`${email} 사용자로 설정 했습니다.`); + + const {email, password} = member; + const string = `${email}:${password}`; + localStorage.setItem('credentials', btoa(string)); + alert(`${email} 사용자로 설정 했습니다.`); } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index aac07aeb0f..bcae92122c 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -1,73 +1,71 @@ - - 관리자 페이지 - - - + + 관리자 페이지 + + +
- +
-
- -
- - - - - - - - - - - - - - - - - - -
ID이름가격이미지Actions
- - - - -
+
+ +
+ + + + + + + + + + + + + + + + + +
ID이름가격이미지Actions
+ + + + +
- - + - diff --git a/src/main/resources/templates/cart.html b/src/main/resources/templates/cart.html index c13e3cb9d2..379f72013c 100644 --- a/src/main/resources/templates/cart.html +++ b/src/main/resources/templates/cart.html @@ -1,68 +1,70 @@ - - 장바구니 - - - + + 장바구니 + + +
- +
-

장바구니

-
-
+

장바구니

+
+
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 35aa653f61..ab54f38abc 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,39 +1,41 @@ - - 상품목록 - - - + + 상품목록 + + +
- +
-
    - -
  • -
    - 상품 이미지 -
    -
    -
    -

    -

    -
    - -
    -
  • -
+
    +
  • +
    + 상품 이미지 +
    +
    +
    +

    +

    +
    + +
    +
  • +
diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html index 858151ad3f..1b42f46ab3 100644 --- a/src/main/resources/templates/settings.html +++ b/src/main/resources/templates/settings.html @@ -1,36 +1,39 @@ - - 설정 - - - + + 설정 + + +
- +
-

사용자 선택

-
- -
-
-
-
- -
-
+

사용자 선택

+
+ +
+
+
+
+ +
+
- + diff --git a/src/test/java/cart/JwpCartApplicationTests.java b/src/test/java/cart/JwpCartApplicationTests.java deleted file mode 100644 index e88e386d79..0000000000 --- a/src/test/java/cart/JwpCartApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package cart; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class JwpCartApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/cart/ProductIntegrationTest.java b/src/test/java/cart/ProductIntegrationTest.java index fc998a9d2c..639039f46b 100644 --- a/src/test/java/cart/ProductIntegrationTest.java +++ b/src/test/java/cart/ProductIntegrationTest.java @@ -1,30 +1,54 @@ package cart; +import static cart.dto.product.RequestFixture.NUNU_REQUEST; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.cart.Cart; +import cart.domain.product.Product; +import cart.domain.user.User; +import cart.service.cart.CartCommandService; +import cart.service.product.ProductCommandService; +import cart.service.user.UserCommandService; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; -import static io.restassured.RestAssured.given; -import static org.assertj.core.api.Assertions.assertThat; - +@SuppressWarnings({"SpellCheckingInspection", "NonAsciiCharacters"}) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ProductIntegrationTest { +@Sql("/truncate.sql") +class ProductIntegrationTest { - @LocalServerPort - private int port; + private static final Product 상품 = new Product("email", "password", 100); + + @Autowired + private UserCommandService userCommandService; + + @Autowired + private ProductCommandService productCommandService; + + @Autowired + private CartCommandService cartCommandService; @BeforeEach - void setUp() { + void setUp(@LocalServerPort final int port) { RestAssured.port = port; } @Test - public void getProducts() { - var result = given() + void getProducts() { + final User 새_유저 = 회원가입(); + final Product 등록된_상품 = 상품_등록(상품); + 상품_카트에_등록_후_등록된_상품_id(새_유저, 등록된_상품); + + final var result = given() .contentType(MediaType.APPLICATION_JSON_VALUE) .when() .get("/products") @@ -34,4 +58,99 @@ public void getProducts() { assertThat(result.statusCode()).isEqualTo(HttpStatus.OK.value()); } + @Test + void updateProduct() { + //given + final User 새_유저 = 회원가입(); + final Product 등록된_상품 = 상품_등록(상품); + final long id = 상품_카트에_등록_후_등록된_상품_id(새_유저, 등록된_상품); + + //when + final var result = given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(NUNU_REQUEST) + .when() + .put("/products/" + id) + .then() + .extract(); + + //then + assertAll( + () -> assertThat(result.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(result.body().jsonPath().getLong("productId")).isEqualTo(1), + () -> assertThat(result.body().jsonPath().getString("name")).isEqualTo("누누"), + () -> assertThat(result.body().jsonPath().getString("image")).isEqualTo("naver.com"), + () -> assertThat(result.body().jsonPath().getInt("price")).isEqualTo(1) + ); + } + + @Test + void createProduct() { + //when + final var result = given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(NUNU_REQUEST) + .when() + .post("/products") + .then() + .extract(); + + //then + assertAll( + () -> assertThat(result.statusCode()).isEqualTo(HttpStatus.CREATED.value()), + () -> assertThat(result.body().jsonPath().getLong("productId")).isPositive(), + () -> assertThat(result.body().jsonPath().getString("name")).isEqualTo("누누"), + () -> assertThat(result.body().jsonPath().getString("image")).isEqualTo("naver.com"), + () -> assertThat(result.body().jsonPath().getInt("price")).isEqualTo(1) + ); + } + + @Test + void 카트에_없는_상품은_제거할_수_있음() { + //given + final Product 등록된_상품 = 상품_등록(상품); + + //when + final var result = given() + .when() + .delete("/products/" + 등록된_상품.getProductId().getValue()) + .then() + .extract(); + + //then + assertThat(result.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Test + void 카트에_추가된_상품을_제거하면_예외() { + //given + final User 새_유저 = 회원가입(); + final Product 등록된_상품 = 상품_등록(상품); + final long 등록된_상품_id = 상품_카트에_등록_후_등록된_상품_id(새_유저, 등록된_상품); + + //when + final var result = given() + .when() + .delete("/products/" + 등록된_상품_id) + .then() + .extract(); + + //then + assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + private User 회원가입() { + return userCommandService.save("asdf", "1234"); + } + + private long 상품_카트에_등록_후_등록된_상품_id(final User 가입자, final Product 등록_희망_상품) { + final Cart cart = cartCommandService.addProduct(등록_희망_상품.getProductId().getValue(), 가입자.getEmail().getValue()); + return cart.getCartProducts().getCartProducts().get(0).getCartProductId().getValue(); + } + + private Product 상품_등록(final Product 새로운_상품) { + return productCommandService.create(새로운_상품.getProductName(), + 새로운_상품.getProductImage().getValue(), + 새로운_상품.getProductPrice().getValue()); + } } diff --git a/src/test/java/cart/auth/filter/AuthenticationFilterTest.java b/src/test/java/cart/auth/filter/AuthenticationFilterTest.java new file mode 100644 index 0000000000..47efb7beab --- /dev/null +++ b/src/test/java/cart/auth/filter/AuthenticationFilterTest.java @@ -0,0 +1,48 @@ +package cart.auth.filter; + + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import cart.controller.AbstractControllerTest; +import cart.filter.AuthenticationFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.Base64Utils; +import org.springframework.web.context.WebApplicationContext; + +@SuppressWarnings("NonAsciiCharacters") +class AuthenticationFilterTest extends AbstractControllerTest { + + @Autowired + private WebApplicationContext webApplicationContext; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .addFilter(new AuthenticationFilter(userRepository)) + .build(); + } + + @Test + void 인증_헤더_없이_요청을_날리면_예외가_발생한다() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isUnauthorized()); + } + + @Test + void 인증_헤더_있이_요청시_성공한다() throws Exception { + final String email = "test@naver.com"; + final String password = "1234"; + final String basicAuthHeader = "Basic " + Base64Utils.encodeToString((email + ":" + password).getBytes()); + given(userRepository.existsByEmailAndPassword(email, password)).willReturn(true); + + mockMvc.perform(get("/") + .header("Authorization", basicAuthHeader)) + + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/cart/controller/AbstractControllerTest.java b/src/test/java/cart/controller/AbstractControllerTest.java new file mode 100644 index 0000000000..faf553722d --- /dev/null +++ b/src/test/java/cart/controller/AbstractControllerTest.java @@ -0,0 +1,34 @@ +package cart.controller; + +import cart.repository.user.UserRepository; +import cart.service.cart.CartCommandService; +import cart.service.cart.CartQueryService; +import cart.service.product.ProductCommandService; +import cart.service.product.ProductQueryService; +import cart.service.user.UserQueryService; +import com.fasterxml.jackson.databind.ObjectMapper; +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; + +@WebMvcTest +public abstract class AbstractControllerTest { + + @MockBean + protected ProductQueryService productQueryService; + @MockBean + protected ProductCommandService productCommandService; + @MockBean + protected UserQueryService userQueryService; + @MockBean + protected CartCommandService cartCommandService; + @MockBean + protected CartQueryService cartQueryService; + @Autowired + protected ObjectMapper objectMapper; + @Autowired + protected MockMvc mockMvc; + @MockBean + protected UserRepository userRepository; +} diff --git a/src/test/java/cart/controller/product/ProductApiControllerTest.java b/src/test/java/cart/controller/product/ProductApiControllerTest.java new file mode 100644 index 0000000000..6d49df40f0 --- /dev/null +++ b/src/test/java/cart/controller/product/ProductApiControllerTest.java @@ -0,0 +1,205 @@ +package cart.controller.product; + +import static cart.domain.product.ProductFixture.NUNU_ID_PRODUCT; +import static cart.domain.product.ProductFixture.ODO_ID_PRODUCT; +import static cart.dto.product.ResponseFixture.NUNU_RESPONSE; +import static cart.dto.product.ResponseFixture.ODO_RESPONSE; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +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.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import cart.controller.AbstractControllerTest; +import cart.domain.product.Product; +import cart.dto.product.ProductRequest; +import cart.dto.product.ProductResponse; +import cart.dto.product.RequestFixture; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +class ProductApiControllerTest extends AbstractControllerTest { + + @Test + void 상품_생성_테스트() throws Exception { + // given + given(productCommandService.create(anyString(), anyString(), anyInt())).willReturn(NUNU_ID_PRODUCT); + final ProductRequest productRequest = RequestFixture.NUNU_REQUEST; + final String request = objectMapper.writeValueAsString(productRequest); + final String result = objectMapper.writeValueAsString(NUNU_RESPONSE); + + // when + mockMvc.perform(post("/products") + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + // then + .andExpect(status().isCreated()) + .andExpect(content().json(result)); + } + + @Test + void 상품_삭제_테스트() throws Exception { + mockMvc.perform(delete("/products/1")) + + .andExpect(status().isNoContent()); + } + + @Test + void 상품_업데이트_테스트() throws Exception { + // given + given(productCommandService.update(anyLong(), anyString(), anyString(), anyInt())).willReturn(NUNU_ID_PRODUCT); + final ProductRequest productRequest = RequestFixture.NUNU_REQUEST; + final String request = objectMapper.writeValueAsString(productRequest); + final String result = objectMapper.writeValueAsString(NUNU_RESPONSE); + final int id = 1; + + // when + mockMvc.perform(put("/products/" + id) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + // then + .andExpect(status().isOk()) + .andExpect(content().json(result)); + } + + @Test + void 상품_조회_테스트() throws Exception { + final List given = List.of(NUNU_ID_PRODUCT, ODO_ID_PRODUCT); + given(productQueryService.findAll()).willReturn(given); + final List productResponses = List.of(NUNU_RESPONSE, ODO_RESPONSE); + final String result = objectMapper.writeValueAsString(productResponses); + + mockMvc.perform(get("/products")) + + .andExpect(status().isOk()) + .andExpect(content().json(result)); + } + + @Test + void PUT_시에_상품_이름_길이_초과_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("a".repeat(256), "naver.com", 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(put("/products/1") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void PUT_시에_상품_이름_공백_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("", "naver.com", 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(put("/products/1") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void PUT_시에_이미지_url_길이_초과_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("누누", "a".repeat(2049), 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(put("/products/1") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void PUT_시에_이미지_url_공백_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("누누", "", 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(put("/products/1") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void PUT_시에_상품_가격_음수_예외_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("누누", "naver.com", -1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(put("/products/1") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + + @Test + void POST_시에_상품_이름_길이_초과_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("a".repeat(256), "naver.com", 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(post("/products") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void POST_시에_상품_이름_공백_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("", "naver.com", 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(post("/products") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void POST_시에_이미지_url_길이_초과_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("누누", "a".repeat(2049), 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(post("/products") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void POST_시에_이미지_url_공백_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("누누", "", 1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(post("/products") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } + + @Test + void POST_시에_상품_가격_음수_예외_테스트() throws Exception { + final ProductRequest productRequest = new ProductRequest("누누", "naver.com", -1); + final String request = objectMapper.writeValueAsString(productRequest); + + mockMvc.perform(post("/products") + .contentType("application/json") + .content(request)) + + .andExpect(status().isBadRequest()); + } +} diff --git a/src/test/java/cart/controller/product/ViewControllerTest.java b/src/test/java/cart/controller/product/ViewControllerTest.java new file mode 100644 index 0000000000..1ae8784114 --- /dev/null +++ b/src/test/java/cart/controller/product/ViewControllerTest.java @@ -0,0 +1,55 @@ +package cart.controller.product; + +import static cart.domain.product.ProductFixture.NUNU_ID_PRODUCT; +import static cart.domain.product.ProductFixture.ODO_ID_PRODUCT; +import static cart.dto.product.ResponseFixture.NUNU_RESPONSE; +import static cart.dto.product.ResponseFixture.ODO_RESPONSE; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; +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.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import cart.controller.AbstractControllerTest; +import cart.domain.product.Product; +import cart.dto.product.ProductResponse; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +@SuppressWarnings({"NonAsciiCharacters"}) +class ViewControllerTest extends AbstractControllerTest { + + @Test + void 상품_조회_테스트() throws Exception { + //given + final List products = List.of(NUNU_ID_PRODUCT, ODO_ID_PRODUCT); + given(productQueryService.findAll()).willReturn(products); + final List expected = List.of(NUNU_RESPONSE, ODO_RESPONSE); + + //when + mockMvc.perform(get("/")) + + //then + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) + .andExpect(model().attribute("products", equalTo(expected))); + } + + @Test + void 어드민_페이지_조회_테스트() throws Exception { + //given + final List products = List.of(NUNU_ID_PRODUCT, ODO_ID_PRODUCT); + given(productQueryService.findAll()).willReturn(products); + final List expected = List.of(NUNU_RESPONSE, ODO_RESPONSE); + + //when + mockMvc.perform(get("/admin")) + + //then + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) + .andExpect(model().attribute("products", equalTo(expected))); + } +} diff --git a/src/test/java/cart/domain/cart/CartProductsTest.java b/src/test/java/cart/domain/cart/CartProductsTest.java new file mode 100644 index 0000000000..c11ad76735 --- /dev/null +++ b/src/test/java/cart/domain/cart/CartProductsTest.java @@ -0,0 +1,24 @@ +package cart.domain.cart; + +import static cart.domain.product.ProductFixture.NUNU_ID_PRODUCT; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import cart.domain.product.Product; +import cart.exception.AlreadyAddedProductException; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +class CartProductsTest { + + private final Product product = NUNU_ID_PRODUCT; + + @Test + void 같은_상품이_여러번_추가되면_예외() { + final CartProducts cartProducts = new CartProducts(); + cartProducts.add(product.getProductId()); + + assertThatThrownBy(() -> cartProducts.add(product.getProductId())) + .isInstanceOf(AlreadyAddedProductException.class) + .hasMessage("이미 장바구니에 담긴 상품입니다."); + } +} diff --git a/src/test/java/cart/domain/product/ImageTest.java b/src/test/java/cart/domain/product/ImageTest.java new file mode 100644 index 0000000000..845cc396bc --- /dev/null +++ b/src/test/java/cart/domain/product/ImageTest.java @@ -0,0 +1,34 @@ +package cart.domain.product; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import cart.domain.product.Image; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +class ImageTest { + + @Test + void 이미지url은_null_일수없습니다() { + assertThatThrownBy(() -> new Image(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지 url은 null일 수 없습니다."); + } + + @Test + void 이미지url은_2048자_이하여야_합니다() { + final String input = "a".repeat(2049); + + assertThatThrownBy(() -> new Image(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지 url은 2048자 이하만 가능합니다."); + } + + @Test + void 이미지url은_공백일수없습니다() { + assertThatThrownBy(() -> new Image("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지 url은 공백일 수 없습니다."); + } + +} diff --git a/src/test/java/cart/domain/product/PriceTest.java b/src/test/java/cart/domain/product/PriceTest.java new file mode 100644 index 0000000000..9346fc43b5 --- /dev/null +++ b/src/test/java/cart/domain/product/PriceTest.java @@ -0,0 +1,18 @@ +package cart.domain.product; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import cart.domain.product.Price; +import org.junit.jupiter.api.Test; + + +@SuppressWarnings("NonAsciiCharacters") +class PriceTest { + + @Test + void 이름은_null_일수없습니다() { + assertThatThrownBy(() -> new Price(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("가격은 0원 이상이어야 합니다."); + } +} diff --git a/src/test/java/cart/domain/product/ProductFixture.java b/src/test/java/cart/domain/product/ProductFixture.java new file mode 100644 index 0000000000..084df61b62 --- /dev/null +++ b/src/test/java/cart/domain/product/ProductFixture.java @@ -0,0 +1,16 @@ +package cart.domain.product; + + +import cart.domain.product.Product; + +@SuppressWarnings("SpellCheckingInspection") +public class ProductFixture { + + public static final Product NUNU_ID_PRODUCT = new Product(1L, "누누", "naver.com", 1); + public static final Product ODO_ID_PRODUCT = new Product(2L, "오도", "naver.com", 1); + public static final Product ODO_PRODUCT = new Product("오도", "naver.com", 1); + + + private ProductFixture() { + } +} diff --git a/src/test/java/cart/domain/product/ProductTest.java b/src/test/java/cart/domain/product/ProductTest.java new file mode 100644 index 0000000000..58f9ecf466 --- /dev/null +++ b/src/test/java/cart/domain/product/ProductTest.java @@ -0,0 +1,38 @@ +package cart.domain.product; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import cart.domain.product.Product; +import org.junit.jupiter.api.Test; + + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +class ProductTest { + + @Test + void 이름은_null_일수없습니다() { + assertThatThrownBy(() -> createProductWithName(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 null일 수 없습니다."); + } + + @Test + void 이름은_255자_이하여야_합니다() { + final String input = "a".repeat(256); + + assertThatThrownBy(() -> createProductWithName(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 255자 이하만 가능합니다."); + } + + @Test + void 이름은_공백일수없습니다() { + assertThatThrownBy(() -> createProductWithName("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이름은 공백일 수 없습니다."); + } + + private Product createProductWithName(final String name) { + return new Product(name, "url", 1); + } +} diff --git a/src/test/java/cart/domain/user/UserFixture.java b/src/test/java/cart/domain/user/UserFixture.java new file mode 100644 index 0000000000..cfcd283cc1 --- /dev/null +++ b/src/test/java/cart/domain/user/UserFixture.java @@ -0,0 +1,9 @@ +package cart.domain.user; + +public class UserFixture { + + public static final User NUNU_USER = new User("nunu@naver.com", "1234"); + + private UserFixture() { + } +} diff --git a/src/test/java/cart/dto/product/RequestFixture.java b/src/test/java/cart/dto/product/RequestFixture.java new file mode 100644 index 0000000000..304c4e5105 --- /dev/null +++ b/src/test/java/cart/dto/product/RequestFixture.java @@ -0,0 +1,11 @@ +package cart.dto.product; + + +@SuppressWarnings("SpellCheckingInspection") +public class RequestFixture { + + public static final ProductRequest NUNU_REQUEST = new ProductRequest("누누", "naver.com", 1); + + private RequestFixture() { + } +} diff --git a/src/test/java/cart/dto/product/ResponseFixture.java b/src/test/java/cart/dto/product/ResponseFixture.java new file mode 100644 index 0000000000..6a857e7259 --- /dev/null +++ b/src/test/java/cart/dto/product/ResponseFixture.java @@ -0,0 +1,11 @@ +package cart.dto.product; + +@SuppressWarnings("SpellCheckingInspection") +public class ResponseFixture { + + public static final ProductResponse NUNU_RESPONSE = new ProductResponse(1, "누누", "naver.com", 1); + public static final ProductResponse ODO_RESPONSE = new ProductResponse(2, "오도", "naver.com", 1); + + private ResponseFixture() { + } +} diff --git a/src/test/java/cart/entity/product/ProductEntityFixture.java b/src/test/java/cart/entity/product/ProductEntityFixture.java new file mode 100644 index 0000000000..b3b374f7b9 --- /dev/null +++ b/src/test/java/cart/entity/product/ProductEntityFixture.java @@ -0,0 +1,12 @@ +package cart.entity.product; + +import cart.entiy.product.ProductEntity; + +@SuppressWarnings("SpellCheckingInspection") +public class ProductEntityFixture { + + public static final ProductEntity ODO_ENTITY = new ProductEntity("오도", "naver.com", 1); + + private ProductEntityFixture() { + } +} diff --git a/src/test/java/cart/repository/cart/H2CartRepositoryTest.java b/src/test/java/cart/repository/cart/H2CartRepositoryTest.java new file mode 100644 index 0000000000..59de9ca9c3 --- /dev/null +++ b/src/test/java/cart/repository/cart/H2CartRepositoryTest.java @@ -0,0 +1,114 @@ +package cart.repository.cart; + +import static cart.domain.product.ProductFixture.ODO_PRODUCT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.cart.Cart; +import cart.domain.product.Product; +import cart.domain.user.User; +import cart.repository.product.H2ProductRepository; +import cart.repository.product.ProductRepository; +import cart.repository.user.H2UserRepository; +import cart.repository.user.UserRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SuppressWarnings("NonAsciiCharacters") +@JdbcTest +class H2CartRepositoryTest { + + private Product product = ODO_PRODUCT; + private User user = new User("asdf", "1234"); + private CartRepository cartRepository; + private UserRepository userRepository; + private ProductRepository productRepository; + private long userId; + private long productId; + + @Autowired + private void setUp(final JdbcTemplate jdbcTemplate) { + userRepository = new H2UserRepository(jdbcTemplate); + productRepository = new H2ProductRepository(jdbcTemplate); + cartRepository = new H2CartRepository(jdbcTemplate); + } + + @BeforeEach + void setUp() { + userId = userRepository.save(user).getUserId().getValue(); + productId = productRepository.save(ODO_PRODUCT).getProductId().getValue(); + user = new User(userId, user); + product = new Product(productId, product); + } + + @Test + void 카트_저장_테스트() { + final Cart cart = new Cart(user.getUserId()); + + final Cart result = cartRepository.save(cart); + + final Optional findResult = cartRepository.findByUserId(user.getUserId()); + + assertAll( + () -> assertThat(findResult).isPresent(), + () -> assertThat(findResult).contains(result), + () -> assertThat(findResult.get().getCartProducts().getCartProducts()).isEmpty(), + () -> assertThat(findResult.get().getUserId()).isEqualTo(user.getUserId()), + () -> assertThat(findResult.get().getCartId()).isEqualTo(result.getCartId()) + ); + } + + @Test + void 카트_저장_테스트_카트_상품_추가() { + final Cart cart = new Cart(user.getUserId()); + cart.addProduct(product.getProductId()); + + final Cart result = cartRepository.save(cart); + + final Optional findResult = cartRepository.findByUserId(user.getUserId()); + + assertAll( + () -> assertThat(findResult).isPresent(), + () -> assertThat(findResult).contains(result), + () -> assertThat(findResult.get().getCartProducts().getCartProducts()).hasSize(1), + () -> assertThat(findResult.get().getUserId()).isEqualTo(user.getUserId()), + () -> assertThat(findResult.get().getCartId()).isEqualTo(result.getCartId()), + () -> assertThat(findResult.get().getCartProducts().getCartProducts().get(0).getCartProductId()) + .isEqualTo(result.getCartProducts().getCartProducts().get(0).getCartProductId()) + ); + } + + @Nested + class NotSaveTest { + + private Cart cart; + + @BeforeEach + void setUp() { + final Cart newCart = new Cart(user.getUserId()); + newCart.addProduct(product.getProductId()); + + cart = cartRepository.save(newCart); + } + + @Test + void 조회_테스트() { + final Optional findResult = cartRepository.findByUserId(user.getUserId()); + + assertAll( + () -> assertThat(findResult).isPresent(), + () -> assertThat(findResult).contains(cart), + () -> assertThat(findResult.get().getCartProducts().getCartProducts()).hasSize(1), + () -> assertThat(findResult.get().getUserId()).isEqualTo(user.getUserId()), + () -> assertThat(findResult.get().getCartId()).isEqualTo(cart.getCartId()), + () -> assertThat(findResult.get().getCartProducts().getCartProducts().get(0).getCartProductId()) + .isEqualTo(cart.getCartProducts().getCartProducts().get(0).getCartProductId()) + ); + } + } +} diff --git a/src/test/java/cart/repository/cart/StubCartRepository.java b/src/test/java/cart/repository/cart/StubCartRepository.java new file mode 100644 index 0000000000..7e7a4e56cb --- /dev/null +++ b/src/test/java/cart/repository/cart/StubCartRepository.java @@ -0,0 +1,43 @@ +package cart.repository.cart; + +import cart.domain.cart.Cart; +import cart.domain.cart.CartId; +import cart.domain.cart.CartProduct; +import cart.domain.cart.CartProductId; +import cart.domain.user.UserId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class StubCartRepository implements CartRepository { + + private final Map userIdToCart = new HashMap<>(); + private long maxId = 1; + private long maxCartProductId = 1; + + @Override + public Cart save(final Cart cart) { + final UserId userId = cart.getUserId(); + final CartId cartId = new CartId(maxId); + final var CartProducts = cart.getCartProducts().getCartProducts(); + final ArrayList cartProducts = new ArrayList<>(); + for (final CartProduct cartProduct : CartProducts) { + cartProducts.add( + new CartProduct(new CartProductId(maxCartProductId), cartProduct.getProductId())); + maxCartProductId++; + } + maxId++; + final Cart savedCart = new Cart(cartId, userId, cartProducts); + userIdToCart.put(userId, savedCart); + return savedCart; + } + + @Override + public Optional findByUserId(final UserId userId) { + if (userIdToCart.containsKey(userId)) { + return Optional.of(userIdToCart.get(userId)); + } + return Optional.empty(); + } +} diff --git a/src/test/java/cart/repository/cart/dao/CartDaoTest.java b/src/test/java/cart/repository/cart/dao/CartDaoTest.java new file mode 100644 index 0000000000..abf69fe63a --- /dev/null +++ b/src/test/java/cart/repository/cart/dao/CartDaoTest.java @@ -0,0 +1,67 @@ +package cart.repository.cart.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.user.User; +import cart.domain.user.UserId; +import cart.entiy.cart.CartEntity; +import cart.repository.user.H2UserRepository; +import cart.repository.user.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SuppressWarnings("NonAsciiCharacters") +@JdbcTest +class CartDaoTest { + + private CartDao cartDao; + private UserRepository userRepository; + private long userId; + + @Autowired + private void setUp(final JdbcTemplate jdbcTemplate) { + cartDao = new CartDao(jdbcTemplate); + userRepository = new H2UserRepository(jdbcTemplate); + } + + @BeforeEach + void setUp() { + userId = userRepository.save(new User("asdf", "1234")).getUserId().getValue(); + } + + @Test + void 저장_테스트() { + final CartEntity result = cartDao.save(new CartEntity(null, userId)); + + assertAll( + () -> assertThat(result.getCartId().getValue()).isPositive(), + () -> assertThat(result.getUserId().getValue()).isEqualTo(userId) + ); + } + + @Nested + class NotSaveTest { + + private long cartId; + + @BeforeEach + void setUp() { + cartId = cartDao.save(new CartEntity(null, userId)).getCartId().getValue(); + } + + @Test + void 조회_테스트() { + final CartEntity result = cartDao.findByUserId(new UserId(userId)); + + assertAll( + () -> assertThat(result.getCartId().getValue()).isEqualTo(cartId), + () -> assertThat(result.getUserId().getValue()).isEqualTo(userId) + ); + } + } +} diff --git a/src/test/java/cart/repository/cart/dao/CartProductDaoTest.java b/src/test/java/cart/repository/cart/dao/CartProductDaoTest.java new file mode 100644 index 0000000000..e803523055 --- /dev/null +++ b/src/test/java/cart/repository/cart/dao/CartProductDaoTest.java @@ -0,0 +1,94 @@ +package cart.repository.cart.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.cart.CartId; +import cart.domain.product.ProductFixture; +import cart.domain.user.User; +import cart.entiy.cart.CartEntity; +import cart.entiy.cart.CartProductEntity; +import cart.repository.product.H2ProductRepository; +import cart.repository.product.ProductRepository; +import cart.repository.user.H2UserRepository; +import cart.repository.user.UserRepository; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SuppressWarnings("NonAsciiCharacters") +@JdbcTest +class CartProductDaoTest { + + private CartProductDao cartProductDao; + private CartDao cartDao; + private UserRepository userRepository; + private ProductRepository productRepository; + private long userId; + private long cartId; + private long productId; + + + @Autowired + private void setUp(final JdbcTemplate jdbcTemplate) { + cartProductDao = new CartProductDao(jdbcTemplate); + userRepository = new H2UserRepository(jdbcTemplate); + productRepository = new H2ProductRepository(jdbcTemplate); + cartDao = new CartDao(jdbcTemplate); + } + + @BeforeEach + void setUp() { + userId = userRepository.save(new User("asdf", "1234")).getUserId().getValue(); + cartId = cartDao.save(new CartEntity(null, userId)).getCartId().getValue(); + productId = productRepository.save(ProductFixture.ODO_PRODUCT).getProductId().getValue(); + } + + @Test + void 저장_테스트() { + final CartProductEntity cartProductEntity = new CartProductEntity(null, cartId, productId); + cartProductDao.insertAll(List.of(cartProductEntity)); + + final List result = cartProductDao.findAllByCartId(new CartId(cartId)); + + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).getCartEntityId().getValue()).isEqualTo(cartId), + () -> assertThat(result.get(0).getProductEntityId().getValue()).isEqualTo(productId) + ); + } + + @Nested + class NotSaveTest { + + @BeforeEach + void setUp() { + cartProductDao.insertAll(List.of(new CartProductEntity(null, cartId, productId))); + } + + @Test + void 조회_테스트() { + final List result = cartProductDao.findAllByCartId(new CartId(cartId)); + + assertAll( + () -> assertThat(result).hasSize(1), + () -> assertThat(result.get(0).getCartProductEntityId().getValue()).isPositive(), + () -> assertThat(result.get(0).getCartEntityId().getValue()).isEqualTo(cartId), + () -> assertThat(result.get(0).getProductEntityId().getValue()).isEqualTo(productId) + ); + } + + @Test + void 제거_테스트() { + cartProductDao.deleteAllByCartId(new CartId(cartId)); + + final List result = cartProductDao.findAllByCartId(new CartId(cartId)); + + assertThat(result).isEmpty(); + } + } +} diff --git a/src/test/java/cart/repository/product/H2ProductRepositoryTest.java b/src/test/java/cart/repository/product/H2ProductRepositoryTest.java new file mode 100644 index 0000000000..f2eec68dc8 --- /dev/null +++ b/src/test/java/cart/repository/product/H2ProductRepositoryTest.java @@ -0,0 +1,85 @@ +package cart.repository.product; + +import static cart.domain.product.ProductFixture.ODO_PRODUCT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.product.Product; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SuppressWarnings("SpellCheckingInspection") +@JdbcTest +class H2ProductRepositoryTest { + + private H2ProductRepository h2ProductRepository; + + @Autowired + private void setUp(final JdbcTemplate jdbcTemplate) { + h2ProductRepository = new H2ProductRepository(jdbcTemplate); + } + + @Test + void save() { + final Product result = h2ProductRepository.save(ODO_PRODUCT); + + assertThat(result.getProductId().getValue()).isPositive(); + } + + @Nested + class NotSaveTest { + + private long productId; + + @BeforeEach + void setUp() { + productId = h2ProductRepository.save(ODO_PRODUCT).getProductId().getValue(); + } + + @Test + void update() { + final Product product = new Product(productId, "누누", "newUrl", 3); + + final Product result = h2ProductRepository.update(product); + + final Product expectResult = new Product(productId, product); + assertAll( + () -> assertThat(result.getProductId().getValue()).isEqualTo(productId), + () -> assertThat(result).usingRecursiveComparison().isEqualTo(expectResult) + ); + } + + @Test + void findAll() { + final List result = h2ProductRepository.findAll(); + + assertThat(result).hasSize(1); + } + + @Test + void findById() { + final Optional result = h2ProductRepository.findById(productId); + + final Product expectResult = new Product(productId, result.get()); + assertAll( + () -> assertThat(result).isPresent(), + () -> assertThat(result.get()).usingRecursiveComparison().isEqualTo(expectResult) + ); + } + + @Test + void deleteById() { + h2ProductRepository.deleteById(productId); + + final Optional result = h2ProductRepository.findById(productId); + + assertThat(result).isEmpty(); + } + } +} diff --git a/src/test/java/cart/repository/product/StubProductRepository.java b/src/test/java/cart/repository/product/StubProductRepository.java new file mode 100644 index 0000000000..9d605abcfa --- /dev/null +++ b/src/test/java/cart/repository/product/StubProductRepository.java @@ -0,0 +1,65 @@ +package cart.repository.product; + +import cart.domain.product.Product; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +public class StubProductRepository implements ProductRepository { + + private final Map productMap = new HashMap<>(); + private final AtomicLong maxId = new AtomicLong(); + + @Override + public Product save(final Product product) { + final long currentId = maxId.incrementAndGet(); + + final Product saved = new Product(currentId, product); + productMap.put(currentId, saved); + return saved; + } + + @Override + public Product update(final Product product) { + final Long id = product.getProductId().getValue(); + if (id == null || productMap.get(id) == null) { + return product; + } + productMap.put(id, product); + return product; + } + + @Override + public List findAll() { + return new ArrayList<>(productMap.values()); + } + + @Override + public Optional findById(final Long id) { + final Product product = productMap.get(id); + if (product == null) { + return Optional.empty(); + } + return Optional.of(product); + } + + @Override + public void deleteById(final Long id) { + productMap.remove(id); + } + + @Override + public List findAllById(final List ids) { + final List products = new ArrayList<>(); + for (final Long id : ids) { + final Product product = productMap.get(id); + if (product != null) { + products.add(product); + } + } + return products; + } +} diff --git a/src/test/java/cart/repository/user/H2UserRepositoryTest.java b/src/test/java/cart/repository/user/H2UserRepositoryTest.java new file mode 100644 index 0000000000..db3de938d5 --- /dev/null +++ b/src/test/java/cart/repository/user/H2UserRepositoryTest.java @@ -0,0 +1,67 @@ +package cart.repository.user; + +import static cart.domain.user.UserFixture.NUNU_USER; +import static org.assertj.core.api.Assertions.assertThat; + +import cart.domain.user.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +@SuppressWarnings("NonAsciiCharacters") +@JdbcTest +class H2UserRepositoryTest { + + private H2UserRepository h2UserRepository; + + @Autowired + private void setUp(final JdbcTemplate jdbcTemplate) { + h2UserRepository = new H2UserRepository(jdbcTemplate); + } + + @Test + void 저장_테스트() { + final User result = h2UserRepository.save(NUNU_USER); + + assertThat(result.getUserId().getValue()).isPositive(); + } + + @Nested + class NotSaveTest { + + private long userId; + + @BeforeEach + void setUp() { + userId = h2UserRepository.save(NUNU_USER).getUserId().getValue(); + } + + @Test + void 존재_확인_테스트() { + final boolean result = h2UserRepository.existsByEmailAndPassword( + NUNU_USER.getEmail().getValue(), + NUNU_USER.getPassword().getValue()); + + assertThat(result).isTrue(); + } + + @Test + void 없는거_확인_테스트() { + final boolean result = h2UserRepository.existsByEmailAndPassword( + NUNU_USER.getEmail().getValue() + "1", + NUNU_USER.getPassword().getValue()); + + assertThat(result).isFalse(); + } + + @Test + void 이메일로_조회_테스트() { + final User result = h2UserRepository.findByEmail(NUNU_USER.getEmail().getValue()).get(); + + assertThat(result).usingRecursiveComparison().isEqualTo(new User(userId, NUNU_USER)); + } + } +} diff --git a/src/test/java/cart/repository/user/StubUserRepository.java b/src/test/java/cart/repository/user/StubUserRepository.java new file mode 100644 index 0000000000..dcac535a1f --- /dev/null +++ b/src/test/java/cart/repository/user/StubUserRepository.java @@ -0,0 +1,44 @@ +package cart.repository.user; + +import cart.domain.user.User; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class StubUserRepository implements UserRepository { + + private final List users = new ArrayList<>(); + private final long maxUserId = 1; + + @Override + public User save(final User user) { + final User newUser = new User(maxUserId, user.getEmail().getValue(), user.getPassword().getValue()); + users.add(newUser); + return newUser; + } + + @Override + public boolean existsByEmailAndPassword(final String email, final String password) { + for (final User user : users) { + if (user.getEmail().getValue().equals(email) && user.getPassword().getValue().equals(password)) { + return true; + } + } + return false; + } + + @Override + public Optional findByEmail(final String email) { + for (final User user : users) { + if (user.getEmail().getValue().equals(email)) { + return Optional.of(user); + } + } + return Optional.empty(); + } + + @Override + public List findAll() { + return users; + } +} diff --git a/src/test/java/cart/service/cart/CartCommandServiceEventTest.java b/src/test/java/cart/service/cart/CartCommandServiceEventTest.java new file mode 100644 index 0000000000..5f35eaf788 --- /dev/null +++ b/src/test/java/cart/service/cart/CartCommandServiceEventTest.java @@ -0,0 +1,47 @@ +package cart.service.cart; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.cart.Cart; +import cart.domain.user.User; +import cart.event.user.UserRegisteredEvent; +import cart.repository.cart.CartRepository; +import cart.repository.user.UserRepository; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Transactional; + +@SuppressWarnings("NonAsciiCharacters") +@SpringBootTest +@Transactional +class CartCommandServiceEventTest { + + @Autowired + private ApplicationEventPublisher applicationEventPublisher; + + @Autowired + private UserRepository userRepository; + + @Autowired + private CartRepository cartRepository; + + @Test + void 이벤트_테스트() { + //given + final User user = userRepository.save(new User("asdf", "1234")); + + //when + applicationEventPublisher.publishEvent(new UserRegisteredEvent(user)); + + //then + final Optional expectResult = cartRepository.findByUserId(user.getUserId()); + assertAll( + () -> assertThat(expectResult).isPresent(), + () -> assertThat(expectResult.get().getCartId().getValue()).isPositive() + ); + } +} diff --git a/src/test/java/cart/service/cart/CartCommandServiceTest.java b/src/test/java/cart/service/cart/CartCommandServiceTest.java new file mode 100644 index 0000000000..e7ed2eccf2 --- /dev/null +++ b/src/test/java/cart/service/cart/CartCommandServiceTest.java @@ -0,0 +1,80 @@ +package cart.service.cart; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.cart.Cart; +import cart.domain.product.Product; +import cart.domain.user.User; +import cart.event.user.UserRegisteredEvent; +import cart.repository.cart.CartRepository; +import cart.repository.cart.StubCartRepository; +import cart.repository.product.ProductRepository; +import cart.repository.product.StubProductRepository; +import cart.repository.user.StubUserRepository; +import cart.repository.user.UserRepository; +import cart.service.product.ProductQueryService; +import cart.service.user.UserQueryService; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +class CartCommandServiceTest { + + private CartRepository cartRepository; + private UserQueryService userQueryService; + private UserRepository userRepository; + private CartCommandService cartCommandService; + private ProductQueryService productQueryService; + private ProductRepository productRepository; + + @BeforeEach + void setUp() { + cartRepository = new StubCartRepository(); + userRepository = new StubUserRepository(); + userQueryService = new UserQueryService(userRepository); + productRepository = new StubProductRepository(); + productQueryService = new ProductQueryService(productRepository); + cartCommandService = new CartCommandService(cartRepository, userQueryService, productQueryService); + } + + @Test + void 카트_추가_테스트() { + // given + final User savedUser = userRepository.save(new User("asdf", "1234")); + final Cart cart = new Cart(savedUser.getUserId()); + cartRepository.save(cart); + + // when + cartCommandService.createCart(new UserRegisteredEvent(savedUser)); + + // then + final Optional findResult = cartRepository.findByUserId(savedUser.getUserId()); + assertAll( + () -> assertThat(findResult).isPresent(), + () -> assertThat(findResult.get().getCartId().getValue()).isPositive() + ); + } + + @Test + void 장바구니_추가_테스트() { + // given + final User savedUser = userRepository.save(new User("asdf", "1234")); + final Product savedProduct = productRepository.save(new Product("name", "image", 5)); + final Cart cart = new Cart(savedUser.getUserId()); + cartRepository.save(cart); + + // when + final Cart result = cartCommandService.addProduct(savedProduct.getProductId().getValue(), + savedUser.getEmail().getValue()); + + // then + assertAll( + () -> assertThat(result.getCartId().getValue()).isPositive(), + () -> assertThat(result.getCartProducts().getCartProducts()).hasSize(1), + () -> assertThat( + result.getCartProducts().getCartProducts().get(0).getCartProductId().getValue()).isPositive() + ); + } +} diff --git a/src/test/java/cart/service/product/ProductCommandServiceTest.java b/src/test/java/cart/service/product/ProductCommandServiceTest.java new file mode 100644 index 0000000000..e12b8c3291 --- /dev/null +++ b/src/test/java/cart/service/product/ProductCommandServiceTest.java @@ -0,0 +1,71 @@ +package cart.service.product; + +import static cart.domain.product.ProductFixture.ODO_PRODUCT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.domain.product.Product; +import cart.repository.product.StubProductRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters", "SpellCheckingInspection"}) +class ProductCommandServiceTest { + + private StubProductRepository stubProductRepository; + private ProductCommandService productCommandService; + + @BeforeEach + void setUp() { + stubProductRepository = new StubProductRepository(); + productCommandService = new ProductCommandService(stubProductRepository); + } + + @Test + void 생성_테스트() { + final Product result = productCommandService.create("오도", "naver.com", 1); + + final Optional product = stubProductRepository.findById(1L); + assertAll( + () -> assertThat(result.getProductId().getValue()).isPositive(), + () -> assertThat(product).isPresent() + ); + } + + @Test + void 제거_테스트() { + //given + final Product product = stubProductRepository.save(ODO_PRODUCT); + final long productId = product.getProductId().getValue(); + + //when + productCommandService.delete(productId); + + //then + final Optional result = stubProductRepository.findById(productId); + assertThat(result).isEmpty(); + } + + @Test + void 업데이트_테스트() { + //given + final Product product = stubProductRepository.save(ODO_PRODUCT); + + //when + final Product result = productCommandService.update( + product.getProductId().getValue(), + ODO_PRODUCT.getProductName(), + ODO_PRODUCT.getProductImage().getValue(), + ODO_PRODUCT.getProductPrice().getValue()); + + //then + final Product expect = new Product(product.getProductId().getValue(), product); + final Optional updatedProduct = stubProductRepository.findById(product.getProductId().getValue()); + assertAll( + () -> assertThat(result).usingRecursiveComparison().isEqualTo(expect), + () -> assertThat(updatedProduct).isPresent(), + () -> assertThat(updatedProduct.get()).usingRecursiveComparison().isEqualTo(expect) + ); + } +} diff --git a/src/test/java/cart/service/product/ProductQueryServiceTest.java b/src/test/java/cart/service/product/ProductQueryServiceTest.java new file mode 100644 index 0000000000..b81be232b3 --- /dev/null +++ b/src/test/java/cart/service/product/ProductQueryServiceTest.java @@ -0,0 +1,32 @@ +package cart.service.product; + +import static cart.domain.product.ProductFixture.ODO_PRODUCT; +import static org.assertj.core.api.Assertions.assertThat; + +import cart.domain.product.Product; +import cart.repository.product.StubProductRepository; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings({"NonAsciiCharacters"}) +class ProductQueryServiceTest { + + private StubProductRepository stubProductRepository; + private ProductQueryService productSearchService; + + @BeforeEach + void setUp() { + stubProductRepository = new StubProductRepository(); + productSearchService = new ProductQueryService(stubProductRepository); + } + + @Test + void 조회_테스트() { + stubProductRepository.save(ODO_PRODUCT); + + final List result = productSearchService.findAll(); + + assertThat(result).hasSize(1); + } +} diff --git a/src/test/java/cart/service/user/UserCommandServiceTest.java b/src/test/java/cart/service/user/UserCommandServiceTest.java new file mode 100644 index 0000000000..38d30d56bc --- /dev/null +++ b/src/test/java/cart/service/user/UserCommandServiceTest.java @@ -0,0 +1,46 @@ +package cart.service.user; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import cart.repository.user.StubUserRepository; +import cart.repository.user.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +@SuppressWarnings("NonAsciiCharacters") +@ExtendWith(MockitoExtension.class) +class UserCommandServiceTest { + + private UserCommandService userCommandService; + private UserRepository userRepository; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + @BeforeEach + void setUp() { + userRepository = new StubUserRepository(); + userCommandService = new UserCommandService(userRepository, applicationEventPublisher); + } + + @Test + void 저장_테스트() { + // given + final String email = "asdf"; + final String password = "1234"; + + // when + userCommandService.save(email, password); + + // then + assertAll( + () -> assertThat(userRepository.existsByEmailAndPassword(email, password)).isTrue(), + () -> assertThat(userRepository.findByEmail(email)).isPresent() + ); + } +} diff --git a/src/test/java/cart/service/user/UserQueryServiceTest.java b/src/test/java/cart/service/user/UserQueryServiceTest.java new file mode 100644 index 0000000000..12ee0e44ee --- /dev/null +++ b/src/test/java/cart/service/user/UserQueryServiceTest.java @@ -0,0 +1,50 @@ +package cart.service.user; + +import static cart.domain.user.UserFixture.NUNU_USER; +import static org.assertj.core.api.Assertions.assertThat; + +import cart.domain.user.User; +import cart.repository.user.StubUserRepository; +import cart.repository.user.UserRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +class UserQueryServiceTest { + + private UserQueryService userQueryService; + private UserRepository userRepository; + + @BeforeEach + void setUp() { + userRepository = new StubUserRepository(); + userQueryService = new UserQueryService(userRepository); + } + + @Test + void 전체_조회_테스트() { + userRepository.save(NUNU_USER); + + final List result = userQueryService.findAll(); + + assertThat(result).hasSize(1); + } + + @Test + void 이메일로_조회_테스트() { + userRepository.save(NUNU_USER); + + final Optional result = userQueryService.findByEmail(NUNU_USER.getEmail().getValue()); + + assertThat(result).isPresent(); + } + + @Test + void 없는_이메일_조회_테스트() { + final Optional result = userQueryService.findByEmail(NUNU_USER.getEmail().getValue()); + + assertThat(result).isEmpty(); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000000..ecf8bb4402 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,10 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL + driver-class-name: org.h2.Driver + sql: + init: + continue-on-error: true diff --git a/src/test/resources/truncate.sql b/src/test/resources/truncate.sql new file mode 100644 index 0000000000..13ebd5f6b0 --- /dev/null +++ b/src/test/resources/truncate.sql @@ -0,0 +1,10 @@ +SELECT H2VERSION() +FROM DUAL; +SET +FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE CART_PRODUCT; +TRUNCATE TABLE CART; +TRUNCATE TABLE PRODUCT; +TRUNCATE TABLE MEMBER; +SET +FOREIGN_KEY_CHECKS = 1;