diff --git a/app/src/main/java/com/codesoom/assignment/common/BaseException.java b/app/src/main/java/com/codesoom/assignment/common/BaseException.java new file mode 100644 index 000000000..8ed672ed0 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/common/BaseException.java @@ -0,0 +1,31 @@ +package com.codesoom.assignment.common; + +import com.codesoom.assignment.common.dto.ErrorResponse; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseException extends RuntimeException { + public final List errors = new ArrayList<>(); + + public BaseException() { + } + + public BaseException(String message) { + super(message); + } + + public abstract int getStatusCode(); + + public void addValidation(String source, String type, String message) { + errors.add(new ErrorValidation(source, type, message)); + } + + public ErrorResponse toErrorResponse() { + return new ErrorResponse( + String.valueOf(getStatusCode()), + getMessage(), + errors + ); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/common/ErrorValidation.java b/app/src/main/java/com/codesoom/assignment/common/ErrorValidation.java new file mode 100644 index 000000000..b0aacd24a --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/common/ErrorValidation.java @@ -0,0 +1,18 @@ +package com.codesoom.assignment.common; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ErrorValidation { + private final String source; + private final String type; + private final String message; + + @Builder + public ErrorValidation(String source, String type, String message) { + this.source = source; + this.type = type; + this.message = message; + } +} diff --git a/app/src/main/java/com/codesoom/assignment/common/dto/ErrorResponse.java b/app/src/main/java/com/codesoom/assignment/common/dto/ErrorResponse.java new file mode 100644 index 000000000..28084aef9 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/common/dto/ErrorResponse.java @@ -0,0 +1,24 @@ +package com.codesoom.assignment.common.dto; + +import com.codesoom.assignment.common.ErrorValidation; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Setter +@ToString +public class ErrorResponse { + private String code; + private String message; + private List errors; + + public ErrorResponse(String code, String message, List errors) { + this.code = code; + this.message = message; + this.errors = errors != null ? errors : List.of(); + } + +} diff --git a/app/src/main/java/com/codesoom/assignment/product/application/InvalidProductRequest.java b/app/src/main/java/com/codesoom/assignment/product/application/InvalidProductRequest.java new file mode 100644 index 000000000..162c0c9f8 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/application/InvalidProductRequest.java @@ -0,0 +1,20 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.common.BaseException; + +public class InvalidProductRequest extends BaseException { + public static final String MESSAGE = "유효하지 않은 상품 정보입니다"; + + public InvalidProductRequest() { + super(MESSAGE); + } + + @Override + public int getStatusCode() { + return 400; + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/application/ProductCreator.java b/app/src/main/java/com/codesoom/assignment/product/application/ProductCreator.java new file mode 100644 index 000000000..99917df8e --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/application/ProductCreator.java @@ -0,0 +1,19 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import org.springframework.stereotype.Service; + +@Service +public class ProductCreator { + private final ProductRepository productRepository; + + public ProductCreator(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public Product createProduct(ProductRequest product) { + return productRepository.save(product.toProductEntity()); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/application/ProductDeleter.java b/app/src/main/java/com/codesoom/assignment/product/application/ProductDeleter.java new file mode 100644 index 000000000..d81986f72 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/application/ProductDeleter.java @@ -0,0 +1,22 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import org.springframework.stereotype.Service; + +@Service +public class ProductDeleter { + + private final ProductRepository productRepository; + private final ProductReader productReader; + + public ProductDeleter(ProductRepository productRepository, ProductReader productReader) { + this.productRepository = productRepository; + this.productReader = productReader; + } + + public void deleteProduct(Long id) { + Product product = productReader.getProduct(id); + productRepository.delete(product); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/application/ProductNotFoundException.java b/app/src/main/java/com/codesoom/assignment/product/application/ProductNotFoundException.java new file mode 100644 index 000000000..926602306 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/application/ProductNotFoundException.java @@ -0,0 +1,16 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.common.BaseException; + +public class ProductNotFoundException extends BaseException { + public static final String MESSAGE = "해당하는 상품이 존재하지 않습니다"; + + public ProductNotFoundException() { + super(MESSAGE); + } + + @Override + public int getStatusCode() { + return 404; + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/application/ProductReader.java b/app/src/main/java/com/codesoom/assignment/product/application/ProductReader.java new file mode 100644 index 000000000..7e8c900ce --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/application/ProductReader.java @@ -0,0 +1,27 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Service +public class ProductReader { + + private final ProductRepository productRepository; + + public ProductReader(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public List getProductList() { + return productRepository.findAll(); + } + + public Product getProduct(Long id) { + return productRepository.findById(id).orElseThrow(ProductNotFoundException::new); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/application/ProductUpdater.java b/app/src/main/java/com/codesoom/assignment/product/application/ProductUpdater.java new file mode 100644 index 000000000..42c09ca13 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/application/ProductUpdater.java @@ -0,0 +1,24 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; + +@Service +public class ProductUpdater { + private final ProductRepository productRepository; + + public ProductUpdater(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + @Transactional + public Product updateProduct(Long id, ProductRequest productRequest) { + Product product = productRepository.findById(id).orElseThrow(ProductNotFoundException::new); + product.update(productRequest.toProductEntity()); + return product; + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/domain/Product.java b/app/src/main/java/com/codesoom/assignment/product/domain/Product.java new file mode 100644 index 000000000..40eb902da --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/domain/Product.java @@ -0,0 +1,41 @@ +package com.codesoom.assignment.product.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +@Getter +@ToString +public class Product { + @Id + @GeneratedValue + private Long id; + private String name; + private String maker; + private Integer price; + private String imageUrl; + + public Product() { + } + + @Builder + public Product(Long id, String name, String maker, Integer price, String imageUrl) { + this.id = id; + this.name = name; + this.maker = maker; + this.price = price; + this.imageUrl = imageUrl; + } + + public void update(Product source) { + this.name = source.name; + this.maker = source.maker; + this.price = source.price; + this.imageUrl = source.imageUrl; + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/domain/dto/ProductRequest.java b/app/src/main/java/com/codesoom/assignment/product/domain/dto/ProductRequest.java new file mode 100644 index 000000000..21aef48cc --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/domain/dto/ProductRequest.java @@ -0,0 +1,49 @@ +package com.codesoom.assignment.product.domain.dto; + +import com.codesoom.assignment.product.application.InvalidProductRequest; +import com.codesoom.assignment.product.domain.Product; +import lombok.Getter; +import lombok.ToString; +import org.springframework.util.StringUtils; + +@ToString +@Getter +public class ProductRequest { + private final String name; + private final String maker; + private final Integer price; + private final String imageUrl; + + public ProductRequest(String name, String maker, Integer price, String imageUrl) { + this.name = name; + this.maker = maker; + this.price = price; + this.imageUrl = imageUrl; + } + + public Product toProductEntity() { + return Product.builder() + .name(this.name) + .maker(this.maker) + .price(this.price) + .imageUrl(this.imageUrl).build(); + } + + public void validate() { + InvalidProductRequest invalidProductRequest = new InvalidProductRequest(); + + if (StringUtils.isEmpty(this.name)) { + invalidProductRequest.addValidation("name", "name is empty", "상품명은 필수입니다."); + } + if (StringUtils.isEmpty(this.maker)) { + invalidProductRequest.addValidation("maker", "maker is empty", "제조사은 필수입니다."); + } + if (this.price == null || this.price < 0) { + invalidProductRequest.addValidation("price", "price is empty", "가격은 필수입니다."); + } + + if (invalidProductRequest.hasErrors()) { + throw invalidProductRequest; + } + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/domain/dto/ProductResponse.java b/app/src/main/java/com/codesoom/assignment/product/domain/dto/ProductResponse.java new file mode 100644 index 000000000..0eefb2e03 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/domain/dto/ProductResponse.java @@ -0,0 +1,33 @@ +package com.codesoom.assignment.product.domain.dto; + +import com.codesoom.assignment.product.domain.Product; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class ProductResponse { + private final Long id; + private final String name; + private final String maker; + private final Integer price; + private final String imageUrl; + + public ProductResponse(Product product) { + this.id = product.getId(); + this.name = product.getName(); + this.maker = product.getMaker(); + this.price = product.getPrice(); + this.imageUrl = product.getImageUrl(); + } + + public static List listOf(Iterable list) { + ArrayList productResponseArrayList = new ArrayList<>(); + for (Product product : list) { + productResponseArrayList.add(new ProductResponse(product)); + } + return productResponseArrayList; + } + +} diff --git a/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductAdvice.java b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductAdvice.java new file mode 100644 index 000000000..bfb957953 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductAdvice.java @@ -0,0 +1,30 @@ +package com.codesoom.assignment.product.infra.api.web.v1; + +import com.codesoom.assignment.common.dto.ErrorResponse; +import com.codesoom.assignment.product.application.InvalidProductRequest; +import com.codesoom.assignment.product.application.ProductNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class ProductAdvice { + + @ExceptionHandler(ProductNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + @ResponseBody + public ErrorResponse productNotFoundExceptionHandler(ProductNotFoundException e) { + + return e.toErrorResponse(); + } + + @ExceptionHandler(InvalidProductRequest.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ErrorResponse invalidProductExceptionHandler(InvalidProductRequest e) { + return e.toErrorResponse(); + } + +} diff --git a/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductCreateController.java b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductCreateController.java new file mode 100644 index 000000000..a201e2c79 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductCreateController.java @@ -0,0 +1,22 @@ +package com.codesoom.assignment.product.infra.api.web.v1; + +import com.codesoom.assignment.product.application.ProductCreator; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import com.codesoom.assignment.product.domain.dto.ProductResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@CrossOrigin +@RequiredArgsConstructor +public class ProductCreateController { + private final ProductCreator productCreator; + + @PostMapping("/products") + @ResponseStatus(HttpStatus.CREATED) + public ProductResponse createProduct(@RequestBody ProductRequest productRequest) { + productRequest.validate(); + return new ProductResponse(productCreator.createProduct(productRequest)); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductDeleteController.java b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductDeleteController.java new file mode 100644 index 000000000..8d07f1761 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductDeleteController.java @@ -0,0 +1,22 @@ +package com.codesoom.assignment.product.infra.api.web.v1; + +import com.codesoom.assignment.product.application.ProductDeleter; +import com.codesoom.assignment.product.application.ProductReader; +import com.codesoom.assignment.product.application.ProductUpdater; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@CrossOrigin +@RequiredArgsConstructor +public class ProductDeleteController { + + private final ProductDeleter productDeleter; + + @DeleteMapping("/products/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteProduct(@PathVariable Long id) { + productDeleter.deleteProduct(id); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductReadController.java b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductReadController.java new file mode 100644 index 000000000..c6ebfbf7c --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductReadController.java @@ -0,0 +1,32 @@ +package com.codesoom.assignment.product.infra.api.web.v1; + +import com.codesoom.assignment.product.application.ProductReader; +import com.codesoom.assignment.product.application.ProductUpdater; +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@CrossOrigin +@RequiredArgsConstructor +public class ProductReadController { + private final ProductReader productReader; + + + @GetMapping("/products") + public List getProductList() { + return ProductResponse.listOf(productReader.getProductList()); + } + + @GetMapping("/products/{id}") + public ProductResponse getProduct(@PathVariable Long id) { + Product product = productReader.getProduct(id); + return new ProductResponse(product); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductUpdateController.java b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductUpdateController.java new file mode 100644 index 000000000..736bcda84 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/infra/api/web/v1/ProductUpdateController.java @@ -0,0 +1,21 @@ +package com.codesoom.assignment.product.infra.api.web.v1; + +import com.codesoom.assignment.product.application.ProductUpdater; +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import com.codesoom.assignment.product.domain.dto.ProductResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@CrossOrigin +@RequiredArgsConstructor +public class ProductUpdateController { + private final ProductUpdater productUpdater; + + @PatchMapping("/products/{id}") + public ProductResponse updateProduct(@PathVariable Long id, @RequestBody ProductRequest productRequest) { + Product product = productUpdater.updateProduct(id, productRequest); + return new ProductResponse(product); + } +} diff --git a/app/src/main/java/com/codesoom/assignment/product/infra/persistence/ProductRepository.java b/app/src/main/java/com/codesoom/assignment/product/infra/persistence/ProductRepository.java new file mode 100644 index 000000000..6872daf90 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/product/infra/persistence/ProductRepository.java @@ -0,0 +1,9 @@ +package com.codesoom.assignment.product.infra.persistence; + +import com.codesoom.assignment.product.domain.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; + + +public interface ProductRepository extends JpaRepository { +} diff --git a/app/src/test/java/com/codesoom/assignment/product/application/JpaTest.java b/app/src/test/java/com/codesoom/assignment/product/application/JpaTest.java new file mode 100644 index 000000000..86e9905d3 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/product/application/JpaTest.java @@ -0,0 +1,18 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@DataJpaTest +public class JpaTest { + @Autowired + ProductRepository productRepository; + + public ProductRepository getProductRepository() { + return productRepository; + } +} diff --git a/app/src/test/java/com/codesoom/assignment/product/application/ProductCreatorTest.java b/app/src/test/java/com/codesoom/assignment/product/application/ProductCreatorTest.java new file mode 100644 index 000000000..52ecf5dd9 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/product/application/ProductCreatorTest.java @@ -0,0 +1,43 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.*; + +@SuppressWarnings({"InnerClassMayBeStatic", "NonAsciiCharacters"}) +@DisplayName("ProductCreator 클래스") +public class ProductCreatorTest extends JpaTest{ + private ProductRequest createProductRequest() { + return new ProductRequest("테스트 상품", "테스트 제조사", 1000, "테스트 이미지 URL"); + } + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class createProduct_메서드는 { + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 상품요청정보가_주어지면 { + private ProductRequest PRODUCT_REQUEST; + @BeforeEach + void setUp() { + PRODUCT_REQUEST = createProductRequest(); + } + @Test + @DisplayName("해당상품정보를_저장후_해당상품정보를_리턴한다") + void 해당상품정보를_저장후_해당상품정보를_리턴한다() { + ProductCreator productCreator = new ProductCreator(productRepository); + Product product = productCreator.createProduct(PRODUCT_REQUEST); + Assertions.assertThat(product.getName()).isEqualTo(PRODUCT_REQUEST.getName()); + Assertions.assertThat(product.getMaker()).isEqualTo(PRODUCT_REQUEST.getMaker()); + Assertions.assertThat(product.getPrice()).isEqualTo(PRODUCT_REQUEST.getPrice()); + Assertions.assertThat(product.getImageUrl()).isEqualTo(PRODUCT_REQUEST.getImageUrl()); + } + + } + + + + + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/product/application/ProductDeleterTest.java b/app/src/test/java/com/codesoom/assignment/product/application/ProductDeleterTest.java new file mode 100644 index 000000000..ccb82b2c2 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/product/application/ProductDeleterTest.java @@ -0,0 +1,58 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.*; + +import java.util.Optional; + +@SuppressWarnings({"InnerClassMayBeStatic", "NonAsciiCharacters"}) +@DisplayName("ProductDeleter 클래스") +public class ProductDeleterTest extends JpaTest { + private final String PRODUCT_NAME = "testProduct"; + private final String PRODUCT_IMAGE_URL = "testImage"; + private final int PRODUCT_PRICE = 1000; + private final String PRODUCT_MAKER = "testMaker"; + + private Product createProduct() { + Product testProduct = Product.builder() + .name(PRODUCT_NAME) + .imageUrl(PRODUCT_IMAGE_URL) + .price(PRODUCT_PRICE) + .maker(PRODUCT_MAKER) + .build(); + return testProduct; + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class deleteProduct_메서드는 { + ProductDeleter productDeleter = new ProductDeleter(productRepository, new ProductReader(productRepository)); + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 해당하는_상품이_없다면 { + @Test + @DisplayName("ProductNotFoundException을_반환한다") + void ProductNotFoundException을_반환한다() { + Assertions.assertThatThrownBy(() -> productDeleter.deleteProduct(1L)) + .isInstanceOf(ProductNotFoundException.class); + } + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 해당하는_상품이_있다면 { + @Test + @DisplayName("해당하는_상품을_삭제한다") + void 해당하는_상품을_삭제한다() { + Product product = createProduct(); + productRepository.save(product); + + productDeleter.deleteProduct(product.getId()); + + Assertions.assertThat(productRepository.findById(product.getId())).isEmpty(); + } + } + } +} diff --git a/app/src/test/java/com/codesoom/assignment/product/application/ProductReaderTest.java b/app/src/test/java/com/codesoom/assignment/product/application/ProductReaderTest.java new file mode 100644 index 000000000..3b25b958c --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/product/application/ProductReaderTest.java @@ -0,0 +1,116 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.*; + +@SuppressWarnings({"InnerClassMayBeStatic", "NonAsciiCharacters"}) +@DisplayName("ProductReader 클래스") +public class ProductReaderTest extends JpaTest { + + private final String PRODUCT_NAME = "testProduct"; + private final String PRODUCT_IMAGE_URL = "testImage"; + private final int PRODUCT_PRICE = 1000; + private final String PRODUCT_MAKER = "testMaker"; + + private Product createProduct() { + Product testProduct = Product.builder() + .name(PRODUCT_NAME) + .imageUrl(PRODUCT_IMAGE_URL) + .price(PRODUCT_PRICE) + .maker(PRODUCT_MAKER) + .build(); + return testProduct; + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class getProduct_메서드는 { + ProductReader productReader = new ProductReader(productRepository); + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 해당하는_상품이_없다면 { + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + @Test + @DisplayName("ProductNotFoundException을_반환한다") + void ProductNotFoundException을_반환한다() { + Assertions.assertThatThrownBy(() -> productReader.getProduct(1L)) + .isInstanceOf(ProductNotFoundException.class); + } + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 해당하는_상품이_있다면 { + + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + + @Test + @DisplayName("해당하는_상품을_리턴한다") + void 해당하는_상품을_리턴한다() { + Product testProduct = createProduct(); + productRepository.save(testProduct); + + Assertions.assertThat(productReader.getProduct(testProduct.getId()).getName()).isEqualTo(PRODUCT_NAME); + Assertions.assertThat(productReader.getProduct(testProduct.getId()).getImageUrl()).isEqualTo(PRODUCT_IMAGE_URL); + Assertions.assertThat(productReader.getProduct(testProduct.getId()).getPrice()).isEqualTo(PRODUCT_PRICE); + Assertions.assertThat(productReader.getProduct(testProduct.getId()).getMaker()).isEqualTo(PRODUCT_MAKER); + } + } + + + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class getProductList_메서드는 { + ProductReader productReader = new ProductReader(productRepository); + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 저장된_상품이_없다면 { + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + @Test + @DisplayName("빈 리스트를_리턴한다") + void 빈_리스트를_리턴한다() { + Assertions.assertThat(productReader.getProductList()).isEmpty(); + } + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 저장된_상품이_있다면 { + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + @Test + @DisplayName("저장된_상품리스트를_리턴한다") + void 저장된_상품리스트를_리턴한다() { + Product testProduct = createProduct(); + productRepository.save(testProduct); + + Assertions.assertThat(productReader.getProductList()).isNotEmpty(); + Product product = productReader.getProductList().get(0); + Assertions.assertThat(product.getName()).isEqualTo(PRODUCT_NAME); + Assertions.assertThat(product.getImageUrl()).isEqualTo(PRODUCT_IMAGE_URL); + Assertions.assertThat(product.getPrice()).isEqualTo(PRODUCT_PRICE); + Assertions.assertThat(product.getMaker()).isEqualTo(PRODUCT_MAKER); + } + } + } +} diff --git a/app/src/test/java/com/codesoom/assignment/product/application/ProductUpdaterTest.java b/app/src/test/java/com/codesoom/assignment/product/application/ProductUpdaterTest.java new file mode 100644 index 000000000..010ecd3d5 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/product/application/ProductUpdaterTest.java @@ -0,0 +1,73 @@ +package com.codesoom.assignment.product.application; + +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.*; + +@SuppressWarnings({"InnerClassMayBeStatic", "NonAsciiCharacters"}) +@DisplayName("ProductUpdate 클래스") +public class ProductUpdaterTest extends JpaTest{ + private final String PRODUCT_NAME = "testProduct"; + private final String PRODUCT_IMAGE_URL = "testImage"; + private final int PRODUCT_PRICE = 1000; + private final String PRODUCT_MAKER = "testMaker"; + + private Product createProduct() { + Product testProduct = Product.builder() + .name(PRODUCT_NAME) + .imageUrl(PRODUCT_IMAGE_URL) + .price(PRODUCT_PRICE) + .maker(PRODUCT_MAKER) + .build(); + return testProduct; + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class updateProduct_메서드는 { + ProductUpdater productUpdater = new ProductUpdater(productRepository); + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 수정할_상품이_없다면 { + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + @Test + @DisplayName("ProductNotFoundException을_반환한다") + void ProductNotFoundException을_반환한다() { + Assertions.assertThatThrownBy(() -> productUpdater.updateProduct(1L, null)) + .isInstanceOf(ProductNotFoundException.class); + } + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class 수정할_상품이_있다면 { + private Product product; + @BeforeEach + void setUp() { + productRepository.deleteAll(); + product = createProduct(); + productRepository.save(product); + + } + + @Test + @DisplayName("해당_상품정보를_수정한_후_해당_상품을_반환한다") + void 해당_상품정보를_수정한_후_해당_상품을_반환한다() { + ProductRequest productRequest = new ProductRequest("updateProduct", "updateMaker", 2000, "updateImage"); + Product updatedProduct = productUpdater.updateProduct(product.getId(), productRequest); + + Assertions.assertThat(updatedProduct.getName()).isEqualTo(productRequest.getName()); + Assertions.assertThat(updatedProduct.getMaker()).isEqualTo(productRequest.getMaker()); + Assertions.assertThat(updatedProduct.getPrice()).isEqualTo(productRequest.getPrice()); + Assertions.assertThat(updatedProduct.getImageUrl()).isEqualTo(productRequest.getImageUrl()); + } + } + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/product/infra/api/ProductControllerTest.java b/app/src/test/java/com/codesoom/assignment/product/infra/api/ProductControllerTest.java new file mode 100644 index 000000000..2f388b9b2 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/product/infra/api/ProductControllerTest.java @@ -0,0 +1,237 @@ +package com.codesoom.assignment.product.infra.api; + +import com.codesoom.assignment.product.application.InvalidProductRequest; +import com.codesoom.assignment.product.application.ProductNotFoundException; +import com.codesoom.assignment.product.domain.Product; +import com.codesoom.assignment.product.domain.dto.ProductRequest; +import com.codesoom.assignment.product.infra.persistence.ProductRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class ProductControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ProductRepository productRepository; + + @Autowired + ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + productRepository.deleteAll(); + } + + @Test + @DisplayName("상품을 등록 후 생성된 상품정보를 반환한다.") + void createProduct() throws Exception { + // given + ProductRequest productRequest = new ProductRequest("catToy", "CatMaker", 1200, "test/img.jpg"); + String jsonString = objectMapper.writeValueAsString(productRequest); + + // expected + mockMvc.perform(post("/products") + .contentType(APPLICATION_JSON) + .content(jsonString)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("name").value("catToy")) + .andExpect(jsonPath("maker").value("CatMaker")) + .andExpect(jsonPath("price").value(1200)) + .andExpect(jsonPath("imageUrl").value("test/img.jpg")) + .andDo(print()); + } + + @Test + @DisplayName("상품 리스트 요청 시 상품리스트 반환한다.") + void getProducts() throws Exception { + // given + Product product = Product.builder() + .name("catToy1") + .price(2000) + .maker("maker1") + .imageUrl("test/img1.jpg") + .build(); + + productRepository.save(product); + + // expected + mockMvc.perform(get("/products")) + .andExpect(status().isOk()) + .andExpect(jsonPath("[0].name").value("catToy1")) + .andExpect(jsonPath("[0].maker").value("maker1")) + .andExpect(jsonPath("[0].price").value(2000)) + .andExpect(jsonPath("[0].imageUrl").value("test/img1.jpg")) + .andDo(print()); + } + + @Test + @DisplayName("단일 상품 조회 시 해당 상품정보를 반환한다.") + void getProduct() throws Exception { + // given + Product product = Product.builder() + .name("catToy1") + .price(2000) + .maker("maker1") + .imageUrl("test/img1.jpg") + .build(); + + Product savedProduct = productRepository.save(product); + + // expected + mockMvc.perform(get("/products/" + savedProduct.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("name").value("catToy1")) + .andExpect(jsonPath("maker").value("maker1")) + .andExpect(jsonPath("price").value(2000)) + .andExpect(jsonPath("imageUrl").value("test/img1.jpg")) + .andDo(print()); + } + + @Test + @DisplayName("단일 상품 조회 시 없는 경우 ProductNotFound 예외 발생") + void getProductNotFound() throws Exception { + + // expected + mockMvc.perform(get("/products/" + 1L)) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof ProductNotFoundException)) + .andExpect(jsonPath("message").value(ProductNotFoundException.MESSAGE)) + .andDo(print()); + } + + @Test + @DisplayName("상품 수정 요청시 해당 상품정보를 수정한다.") + void updateProduct() throws Exception { + // given + Product product = Product.builder() + .name("catToy1") + .price(2000) + .maker("maker1") + .imageUrl("test/img1.jpg") + .build(); + Product savedProduct = productRepository.save(product); + ProductRequest productRequest = new ProductRequest("update", "update", 3000, "test/update.jpg"); + + // expected + mockMvc.perform(patch("/products/" + savedProduct.getId()) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(productRequest)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("name").value("update")) + .andExpect(jsonPath("maker").value("update")) + .andExpect(jsonPath("price").value(3000)) + .andExpect(jsonPath("imageUrl").value("test/update.jpg")) + .andDo(print()); + } + + @Test + @DisplayName("상품 수정 요청시 없는 경우 ProductNotFound 예외 발생") + void updateProductNotFound() throws Exception { + // given + ProductRequest productRequest = new ProductRequest("update", "update", 3000, "test/update.jpg"); + + // expected + mockMvc.perform(patch("/products/" + 100L) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(productRequest)) + ) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof ProductNotFoundException)) + .andExpect(jsonPath("message").value(ProductNotFoundException.MESSAGE)) + .andDo(print()); + } + + @Test + @DisplayName("상품 삭제 요청 시 해당 상품을 삭제한다.") + void deleteProducts() throws Exception { + // given + Product product = Product.builder() + .name("deleteTarget") + .price(2000) + .maker("deleteMaker") + .imageUrl("test/delete.jpg") + .build(); + productRepository.save(product); + + // expected + mockMvc.perform(delete("/products/" + product.getId())) + .andExpect(status().isNoContent()) + .andDo(print()); + + } + + @Test + @DisplayName("상품 삭제 요청 시 없는 경우 ProductNotFound 예외 발생") + void deleteProductsNotFound() throws Exception { + // expected + mockMvc.perform(delete("/products/" + 100L)) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof ProductNotFoundException)) + .andExpect(jsonPath("message").value(ProductNotFoundException.MESSAGE)) + .andDo(print()); + } + + @Test + @DisplayName("상품 생성시 모든 항목의 값이 없는 요청인 경우 에러를 반환한다.") + void createProductInvalidRequest() throws Exception { + // given + ProductRequest productRequest = new ProductRequest("", "", 0, ""); + + // expected + mockMvc.perform(post("/products") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(productRequest))) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof InvalidProductRequest)) + .andExpect(jsonPath("message").value(InvalidProductRequest.MESSAGE)) + .andExpect(jsonPath("errors[0].source").value("name")) + .andExpect(jsonPath("errors[0].type").value("name is empty")) + .andExpect(jsonPath("errors[1].source").value("maker")) + .andExpect(jsonPath("errors[1].type").value("maker is empty")) + .andDo(print()); + } + + @ParameterizedTest + @DisplayName("상품 생성시 올바르지 않은 요청 케이스별 테스트 요청인 경우 에러응답을 반환한다.") + @MethodSource("provideInvalidProductRequests") + void createProductInvalidRequestCase(ProductRequest productRequest) throws Exception { + mockMvc.perform(post("/products") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(productRequest))) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof InvalidProductRequest)) + .andExpect(jsonPath("message").value(InvalidProductRequest.MESSAGE)) + .andDo(print()); + } + + private static Stream provideInvalidProductRequests() { + return Stream.of( + Arguments.of(new ProductRequest("", "testMaker", 1000, "testUrl")), + Arguments.of(new ProductRequest("testName", "", 1000, "testUrl")), + Arguments.of(new ProductRequest("testName", "testMaker", -10, "testUrl")) + ); + } +}