From 48bcf4b37cd5e279ab34a708db7c8083fa6d69d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=98=84=EC=9A=B1?= <43038815+Hyeon-Uk@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:49:03 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EA=B0=80=EA=B2=8C=EC=9D=98=20?= =?UTF-8?q?=EC=9D=8C=EC=8B=9D=20=EC=83=81=ED=92=88=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] update할 음식의 가격이 0 또는 음수일 경우 던져질 Exception 구현 * [feat] Menu 도메인 엔티티에서 update할 price를 받으면 자신의 price를 업데이트 하는 메서드 구현 - 음수, 0 이라면 InvalidMenuPriceUpdateException 던짐 - Dynamic Update를 이용해 수정된 가격만 업데이트하는 쿼리를 보내기 위해 사용 * [test] Menu 도메인 엔티티에서 update할 price를 받으면 자신의 price를 업데이트 하는 메서드 테스트 - 음수, 혹은 양수면 InvalidMenuPriceUpdateException 발생 테스트 - 양수라면 업데이트 된 가격으로 업데이트 됐는지 확인 * [feat] Menu의 가격을 Update하기 위한 Command 구현 - vendorId : 업데이트를 시도하는 vendor의 id - menuId : 업데이트 할 menu의 id - updatePrice : 업데이트 할 가격 * [feat] Menu의 가격을 Update할 수 있는 MenuPriceUpdateService 구현 - 존재하는 메뉴만 업데이트 할 수 있다. - 자신의 가게만 업데이트 할 수 있다. - 가격은 양수만 가능하다. * [feat] 더미용 Menu를 생성하기 위한 createMenu 메서드 구현 - 테스트용 Menu를 매번 생성하면 중복코드가 많이 생기기 때문에 더미 객체 생성용 메서드 생성 * [test] Menu의 가격을 Update할 수 있는 MenuPriceUpdateService 테스트 구현 - 존재하는 메뉴만 업데이트 할 수 있다. - 자신의 가게만 업데이트 할 수 있다. - 가격은 양수만 가능하다. * [fix] 메뉴의 주인이 아닌 점주가 수정 요청을 했을 때, 400을 던지는 로직에서 403을 던지도록 수정 * [feat] 클라이언트에게 주고 받을 DTO 생성 - 업데이트 할 price를 받고 업데이트 된 price를 준다. * [feat] 업데이트 할 엔드포인트 구현 및 exception handler에 추가 * [test] StoreApiController에 추가한 update price관련 로직 테스트 추가 - 업데이트 가격이 0일경우 400 - 업데이트 가격이 음수일경우 400 - 벤더가 아니면 401 - 내 메뉴를 수정하는것이 아니면 403 * [fix] MenuPriceUpdateService 에서 자신의 가게의 메뉴를 수정할 때 던져지는 Exception을 MenuOwnerNotMatchException 로 수정 --- .../camp/woowak/lab/menu/domain/Menu.java | 13 ++ .../InvalidMenuPriceUpdateException.java | 9 ++ .../lab/menu/exception/MenuErrorCode.java | 3 + .../exception/MenuOwnerNotMatchException.java | 9 ++ .../menu/service/MenuPriceUpdateService.java | 48 ++++++ .../command/MenuPriceUpdateCommand.java | 15 ++ .../lab/web/api/store/StoreApiController.java | 18 +++ .../web/api/store/StoreExceptionHandler.java | 10 ++ .../request/store/MenuPriceUpdateRequest.java | 11 ++ .../store/MenuPriceUpdateResponse.java | 6 + .../camp/woowak/lab/fixture/MenuFixture.java | 6 + .../camp/woowak/lab/menu/domain/MenuTest.java | 53 +++++++ .../service/MenuPriceUpdateServiceTest.java | 113 ++++++++++++++ .../web/api/store/StoreApiControllerTest.java | 141 ++++++++++++++++++ 14 files changed, 455 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/menu/exception/InvalidMenuPriceUpdateException.java create mode 100644 src/main/java/camp/woowak/lab/menu/exception/MenuOwnerNotMatchException.java create mode 100644 src/main/java/camp/woowak/lab/menu/service/MenuPriceUpdateService.java create mode 100644 src/main/java/camp/woowak/lab/menu/service/command/MenuPriceUpdateCommand.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/request/store/MenuPriceUpdateRequest.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/response/store/MenuPriceUpdateResponse.java create mode 100644 src/test/java/camp/woowak/lab/menu/service/MenuPriceUpdateServiceTest.java diff --git a/src/main/java/camp/woowak/lab/menu/domain/Menu.java b/src/main/java/camp/woowak/lab/menu/domain/Menu.java index 170b6825..fa0d10b4 100644 --- a/src/main/java/camp/woowak/lab/menu/domain/Menu.java +++ b/src/main/java/camp/woowak/lab/menu/domain/Menu.java @@ -1,5 +1,8 @@ package camp.woowak.lab.menu.domain; +import org.hibernate.annotations.DynamicUpdate; + +import camp.woowak.lab.menu.exception.InvalidMenuPriceUpdateException; import camp.woowak.lab.menu.exception.NotEnoughStockException; import camp.woowak.lab.store.domain.Store; import jakarta.persistence.Column; @@ -17,6 +20,7 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@DynamicUpdate public class Menu { @Id @@ -65,4 +69,13 @@ public void decrementStockCount(int amount) { } stockCount -= amount; } + + public long updatePrice(long uPrice) { + if (uPrice <= 0) { + throw new InvalidMenuPriceUpdateException("메뉴의 가격은 0원보다 커야합니다. 입력값 : " + uPrice); + } + this.price = uPrice; + + return this.price; + } } diff --git a/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuPriceUpdateException.java b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuPriceUpdateException.java new file mode 100644 index 00000000..46d1f843 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuPriceUpdateException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.menu.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class InvalidMenuPriceUpdateException extends BadRequestException { + public InvalidMenuPriceUpdateException(String message) { + super(MenuErrorCode.INVALID_PRICE, message); + } +} diff --git a/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java b/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java index 484885be..b82d2e1f 100644 --- a/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java +++ b/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java @@ -18,6 +18,9 @@ public enum MenuErrorCode implements ErrorCode { NOT_FOUND_MENU(HttpStatus.BAD_REQUEST, "m_8", "메뉴를 찾을 수 없습니다."), NOT_FOUND_MENU_CATEGORY(HttpStatus.BAD_REQUEST, "m_9", "메뉴 카테고리를 찾을 수 없습니다."), + + MENU_OWNER_NOT_MATCH(HttpStatus.FORBIDDEN, "m_10", "메뉴는 가게의 점주만 수정할 수 있습니다."), + NOT_ENOUGH_STOCK(HttpStatus.BAD_REQUEST, "M4", "재고가 부족합니다."); private final int status; diff --git a/src/main/java/camp/woowak/lab/menu/exception/MenuOwnerNotMatchException.java b/src/main/java/camp/woowak/lab/menu/exception/MenuOwnerNotMatchException.java new file mode 100644 index 00000000..8551045f --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/MenuOwnerNotMatchException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.menu.exception; + +import camp.woowak.lab.common.exception.ForbiddenException; + +public class MenuOwnerNotMatchException extends ForbiddenException { + public MenuOwnerNotMatchException(String message) { + super(MenuErrorCode.MENU_OWNER_NOT_MATCH, message); + } +} diff --git a/src/main/java/camp/woowak/lab/menu/service/MenuPriceUpdateService.java b/src/main/java/camp/woowak/lab/menu/service/MenuPriceUpdateService.java new file mode 100644 index 00000000..f746aae2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/service/MenuPriceUpdateService.java @@ -0,0 +1,48 @@ +package camp.woowak.lab.menu.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.exception.MenuOwnerNotMatchException; +import camp.woowak.lab.menu.exception.UnauthorizedMenuCategoryCreationException; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.menu.service.command.MenuPriceUpdateCommand; +import camp.woowak.lab.order.exception.NotFoundMenuException; +import camp.woowak.lab.store.domain.Store; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class MenuPriceUpdateService { + private final MenuRepository menuRepository; + + public MenuPriceUpdateService(MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + /** + * @throws camp.woowak.lab.menu.exception.InvalidMenuPriceUpdateException 업데이트 하려는 가격이 0 또는 음수인 경우 + * @throws UnauthorizedMenuCategoryCreationException 자신의 가게의 메뉴가 아닌 메뉴를 업데이트 하려는 경우 + * @throws NotFoundMenuException 존재하지 않는 메뉴의 가격을 업데이트 하려는 경우 + */ + @Transactional + public long updateMenuPrice(MenuPriceUpdateCommand cmd) { + Menu menu = menuRepository.findByIdWithStore(cmd.menuId()) + .orElseThrow(() -> { + log.info("등록되지 않은 메뉴 {}의 가격 수정을 시도했습니다.", cmd.menuId()); + throw new NotFoundMenuException("등록되지 않은 Menu의 가격 수정을 시도했습니다."); + }); + + Store store = menu.getStore(); + if (!store.isOwnedBy(cmd.vendorId())) { + log.info("권한없는 사용자 {}가 점포 {}의 메뉴 가격 수정을 시도했습니다.", cmd.vendorId(), store.getId()); + throw new MenuOwnerNotMatchException("권한없는 사용자가 메뉴 가격 수정을 시도했습니다."); + } + + long updatedPrice = menu.updatePrice(cmd.updatePrice()); + log.info("Store({}) 의 메뉴({}) 가격을 ({})로 수정했습니다.", store.getId(), menu.getId(), cmd.updatePrice()); + + return updatedPrice; + } +} diff --git a/src/main/java/camp/woowak/lab/menu/service/command/MenuPriceUpdateCommand.java b/src/main/java/camp/woowak/lab/menu/service/command/MenuPriceUpdateCommand.java new file mode 100644 index 00000000..30ac6af1 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/service/command/MenuPriceUpdateCommand.java @@ -0,0 +1,15 @@ +package camp.woowak.lab.menu.service.command; + +import java.util.UUID; + +/** + * @param vendorId 업데이트를 시도하는 vendor의 id + * @param menuId 업데이트 할 menu의 id + * @param updatePrice 업데이트 할 가격 + */ +public record MenuPriceUpdateCommand( + UUID vendorId, + Long menuId, + Long updatePrice +) { +} diff --git a/src/main/java/camp/woowak/lab/web/api/store/StoreApiController.java b/src/main/java/camp/woowak/lab/web/api/store/StoreApiController.java index 57aec81f..075ff8cd 100644 --- a/src/main/java/camp/woowak/lab/web/api/store/StoreApiController.java +++ b/src/main/java/camp/woowak/lab/web/api/store/StoreApiController.java @@ -3,6 +3,7 @@ import java.util.List; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -10,7 +11,9 @@ import org.springframework.web.bind.annotation.RestController; import camp.woowak.lab.menu.service.MenuCategoryRegistrationService; +import camp.woowak.lab.menu.service.MenuPriceUpdateService; import camp.woowak.lab.menu.service.command.MenuCategoryRegistrationCommand; +import camp.woowak.lab.menu.service.command.MenuPriceUpdateCommand; import camp.woowak.lab.store.service.StoreMenuRegistrationService; import camp.woowak.lab.store.service.StoreRegistrationService; import camp.woowak.lab.store.service.command.StoreMenuRegistrationCommand; @@ -19,10 +22,12 @@ import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; import camp.woowak.lab.web.dao.store.StoreDao; import camp.woowak.lab.web.dto.request.store.MenuCategoryRegistrationRequest; +import camp.woowak.lab.web.dto.request.store.MenuPriceUpdateRequest; import camp.woowak.lab.web.dto.request.store.StoreInfoListRequest; import camp.woowak.lab.web.dto.request.store.StoreMenuRegistrationRequest; import camp.woowak.lab.web.dto.request.store.StoreRegistrationRequest; import camp.woowak.lab.web.dto.response.store.MenuCategoryRegistrationResponse; +import camp.woowak.lab.web.dto.response.store.MenuPriceUpdateResponse; import camp.woowak.lab.web.dto.response.store.StoreInfoListResponse; import camp.woowak.lab.web.dto.response.store.StoreMenuRegistrationResponse; import camp.woowak.lab.web.dto.response.store.StoreRegistrationResponse; @@ -36,6 +41,7 @@ public class StoreApiController { private final StoreRegistrationService storeRegistrationService; private final StoreMenuRegistrationService storeMenuRegistrationService; private final MenuCategoryRegistrationService menuCategoryRegistrationService; + private final MenuPriceUpdateService menuPriceUpdateService; private final StoreDao storeDao; @GetMapping("/stores") @@ -84,6 +90,17 @@ public StoreMenuRegistrationResponse storeMenuRegistration(final @Authentication return new StoreMenuRegistrationResponse(menuIds); } + @PatchMapping("/stores/menus/{menuId}/price") + public MenuPriceUpdateResponse menuPriceUpdate(final @AuthenticationPrincipal LoginVendor loginVendor, + final @PathVariable Long menuId, + final @Valid @RequestBody MenuPriceUpdateRequest request + ) { + MenuPriceUpdateCommand command = new MenuPriceUpdateCommand(loginVendor.getId(), menuId, request.price()); + + long updatedPrice = menuPriceUpdateService.updateMenuPrice(command); + return new MenuPriceUpdateResponse(updatedPrice); + } + @PostMapping("/stores/{storeId}/category") public MenuCategoryRegistrationResponse storeCategoryRegistration(@AuthenticationPrincipal LoginVendor loginVendor, @PathVariable Long storeId, @@ -93,4 +110,5 @@ public MenuCategoryRegistrationResponse storeCategoryRegistration(@Authenticatio Long registeredId = menuCategoryRegistrationService.register(command); return new MenuCategoryRegistrationResponse(registeredId); } + } diff --git a/src/main/java/camp/woowak/lab/web/api/store/StoreExceptionHandler.java b/src/main/java/camp/woowak/lab/web/api/store/StoreExceptionHandler.java index 5477bc48..0699b771 100644 --- a/src/main/java/camp/woowak/lab/web/api/store/StoreExceptionHandler.java +++ b/src/main/java/camp/woowak/lab/web/api/store/StoreExceptionHandler.java @@ -10,6 +10,7 @@ import camp.woowak.lab.common.exception.HttpStatusException; import camp.woowak.lab.menu.exception.InvalidMenuCategoryCreationException; import camp.woowak.lab.menu.exception.InvalidMenuCreationException; +import camp.woowak.lab.menu.exception.MenuOwnerNotMatchException; import camp.woowak.lab.menu.exception.NotFoundMenuCategoryException; import camp.woowak.lab.store.exception.InvalidStoreCreationException; import camp.woowak.lab.store.exception.NotEqualsOwnerException; @@ -39,6 +40,15 @@ public ResponseEntity handleException(NotEqualsOwnerException exc return ResponseEntity.status(badRequest).body(problemDetail); } + @ExceptionHandler(MenuOwnerNotMatchException.class) + public ProblemDetail handleException(MenuOwnerNotMatchException exception) { + log.warn("Forbidden",exception); + HttpStatus forbidden = HttpStatus.FORBIDDEN; + ProblemDetail problemDetail = getProblemDetail(exception, forbidden); + + return problemDetail; + } + @ExceptionHandler(NotFoundStoreCategoryException.class) public ResponseEntity handleException(NotFoundStoreCategoryException exception) { log.warn("Not Found", exception); diff --git a/src/main/java/camp/woowak/lab/web/dto/request/store/MenuPriceUpdateRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/store/MenuPriceUpdateRequest.java new file mode 100644 index 00000000..bc8da439 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/store/MenuPriceUpdateRequest.java @@ -0,0 +1,11 @@ +package camp.woowak.lab.web.dto.request.store; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +public record MenuPriceUpdateRequest( + @Min(value = 1, message = "price값은 음수 혹은 0이 될 수 없습니다.") + @NotNull(message = "price값은 필수 입니다.") + Long price +) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/store/MenuPriceUpdateResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/store/MenuPriceUpdateResponse.java new file mode 100644 index 00000000..3630f508 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/store/MenuPriceUpdateResponse.java @@ -0,0 +1,6 @@ +package camp.woowak.lab.web.dto.response.store; + +public record MenuPriceUpdateResponse( + long updatedPrice +) { +} diff --git a/src/test/java/camp/woowak/lab/fixture/MenuFixture.java b/src/test/java/camp/woowak/lab/fixture/MenuFixture.java index e41d7002..8f19c33a 100644 --- a/src/test/java/camp/woowak/lab/fixture/MenuFixture.java +++ b/src/test/java/camp/woowak/lab/fixture/MenuFixture.java @@ -3,7 +3,9 @@ import java.time.LocalDateTime; import java.util.UUID; +import camp.woowak.lab.menu.TestMenu; import camp.woowak.lab.menu.TestMenuCategory; +import camp.woowak.lab.menu.domain.Menu; import camp.woowak.lab.menu.domain.MenuCategory; import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.domain.TestPayAccount; @@ -57,4 +59,8 @@ default MenuCategory createMenuCategory(Long id, Store store, String name) { default StoreCategory createStoreCategory() { return new StoreCategory("양식"); } + + default Menu createMenu(Long id, Store store, MenuCategory menuCategory, String name, long price) { + return new TestMenu(id, store, menuCategory, name, price); + } } diff --git a/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java b/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java index e34dff84..4e8f33c2 100644 --- a/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java +++ b/src/test/java/camp/woowak/lab/menu/domain/MenuTest.java @@ -9,7 +9,9 @@ import org.junit.jupiter.api.Test; import camp.woowak.lab.common.exception.ErrorCode; +import camp.woowak.lab.common.exception.HttpStatusException; import camp.woowak.lab.menu.exception.InvalidMenuCreationException; +import camp.woowak.lab.menu.exception.InvalidMenuPriceUpdateException; import camp.woowak.lab.menu.exception.MenuErrorCode; import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.domain.TestPayAccount; @@ -291,6 +293,57 @@ void isBlank() { } + @Nested + @DisplayName("메뉴 가격 업데이트는") + class UpdatePriceTest { + private long menuPrice = 10000; + private Menu menu = new Menu(storeFixture, menuCategoryFixture, "메뉴1", menuPrice, 10L, "imageUrl"); + + @Nested + @DisplayName("업데이트 하려는 가격이") + class UpdatePrice { + @Test + @DisplayName("[Exception] 음수이면 InvalidMenuPriceUpdateException이 발생한다.") + void negativeUpdatePrice() { + //given + long updatePrice = -1; + + //when & then + HttpStatusException throwable = (HttpStatusException)catchThrowable( + () -> menu.updatePrice(updatePrice)); + assertThat(throwable).isInstanceOf(InvalidMenuPriceUpdateException.class); + assertThat(throwable.errorCode()).isEqualTo(MenuErrorCode.INVALID_PRICE); + } + + @Test + @DisplayName("[Exception] 0이면 InvalidMenuPriceUpdateException이 발생한다.") + void zeroUpdatePrice() { + //given + long updatePrice = 0; + + //when & then + HttpStatusException throwable = (HttpStatusException)catchThrowable( + () -> menu.updatePrice(updatePrice)); + assertThat(throwable).isInstanceOf(InvalidMenuPriceUpdateException.class); + assertThat(throwable.errorCode()).isEqualTo(MenuErrorCode.INVALID_PRICE); + } + + @Test + @DisplayName("[success] 양수면 가격이 update되고 update된 가격이 return된다.") + void positiveUpdatePrice() { + //given + long updatePrice = 9000; + + //when + long updatedPrice = menu.updatePrice(updatePrice); + + //then + assertThat(updatedPrice).isEqualTo(updatePrice); + assertThat(menu.getPrice()).isEqualTo(updatePrice); + } + } + } + private void assertExceptionAndErrorCode(Throwable thrown, ErrorCode expected) { assertThat(thrown).isInstanceOf(InvalidMenuCreationException.class); InvalidMenuCreationException exception = (InvalidMenuCreationException)thrown; diff --git a/src/test/java/camp/woowak/lab/menu/service/MenuPriceUpdateServiceTest.java b/src/test/java/camp/woowak/lab/menu/service/MenuPriceUpdateServiceTest.java new file mode 100644 index 00000000..422226ab --- /dev/null +++ b/src/test/java/camp/woowak/lab/menu/service/MenuPriceUpdateServiceTest.java @@ -0,0 +1,113 @@ +package camp.woowak.lab.menu.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import camp.woowak.lab.fixture.MenuFixture; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.exception.InvalidMenuPriceUpdateException; +import camp.woowak.lab.menu.exception.MenuOwnerNotMatchException; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.menu.service.command.MenuPriceUpdateCommand; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.vendor.domain.Vendor; + +@Nested +@DisplayName("MenuPriceUpdateService 클래스") +@ExtendWith(MockitoExtension.class) +class MenuPriceUpdateServiceTest implements MenuFixture { + @InjectMocks + private MenuPriceUpdateService menuPriceUpdateService; + + @Mock + private MenuRepository menuRepository; + + private Vendor vendor; + private Vendor otherVendor; + private Store store; + private Menu menu; + private int menuPrice = 10000; + + @BeforeEach + void setUpDummies() { + vendor = createVendor(UUID.randomUUID()); + otherVendor = createVendor(UUID.randomUUID()); + store = createStore(vendor); + menu = createMenu(1L, store, createMenuCategory(1L, store, "카테고리1"), "메뉴1", menuPrice); + } + + @Nested + @DisplayName("메뉴가격을 업데이트 할 때") + class UpdateMenu { + @Test + @DisplayName("[성공] 해당 메뉴의 price가 정상적으로 업데이트 된다.") + void success() { + //given + long updatePrice = menuPrice + 1000; + MenuPriceUpdateCommand command = new MenuPriceUpdateCommand(vendor.getId(), menu.getId(), updatePrice); + + when(menuRepository.findByIdWithStore(menu.getId())).thenReturn(Optional.of(menu)); + + //when + long updatedMenuPrice = menuPriceUpdateService.updateMenuPrice(command); + + //then + assertThat(updatedMenuPrice).isEqualTo(updatePrice); + assertThat(menu.getPrice()).isEqualTo(updatePrice); + } + + @Test + @DisplayName("[Exception] 다른 가게 사장님이 다른 가게의 메뉴 가격을 수정하려는 경우 UnauthorizedMenuCategoryCreationException 를 던진다.") + void otherVendorUpdateOtherStorePriceTest() { + //given + long updatePrice = menuPrice + 1000; + MenuPriceUpdateCommand command = new MenuPriceUpdateCommand(otherVendor.getId(), menu.getId(), updatePrice); + + when(menuRepository.findByIdWithStore(menu.getId())).thenReturn(Optional.of(menu)); + + //when & then + assertThatThrownBy(() -> menuPriceUpdateService.updateMenuPrice(command)) + .isExactlyInstanceOf(MenuOwnerNotMatchException.class); + } + + @Test + @DisplayName("[Exception] 업데이트 하려는 가격이 0인경우 InvalidMenuPriceUpdateException을 던진다.") + void invalidNegativePrice() { + //given + long updatePrice = -1; + MenuPriceUpdateCommand command = new MenuPriceUpdateCommand(vendor.getId(), menu.getId(), updatePrice); + + when(menuRepository.findByIdWithStore(menu.getId())).thenReturn(Optional.of(menu)); + + //when & then + assertThatThrownBy(() -> menuPriceUpdateService.updateMenuPrice(command)) + .isExactlyInstanceOf(InvalidMenuPriceUpdateException.class); + } + + @Test + @DisplayName("[Exception] 업데이트 하려는 가격이 0인경우 InvalidMenuPriceUpdateException을 던진다.") + void invalidZeroPrice() { + //given + long updatePrice = 0; + MenuPriceUpdateCommand command = new MenuPriceUpdateCommand(vendor.getId(), menu.getId(), updatePrice); + + when(menuRepository.findByIdWithStore(menu.getId())).thenReturn(Optional.of(menu)); + + //when & then + assertThatThrownBy(() -> menuPriceUpdateService.updateMenuPrice(command)) + .isExactlyInstanceOf(InvalidMenuPriceUpdateException.class); + } + } +} \ No newline at end of file diff --git a/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerTest.java index d5f45b01..fcfd2204 100644 --- a/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerTest.java +++ b/src/test/java/camp/woowak/lab/web/api/store/StoreApiControllerTest.java @@ -26,9 +26,12 @@ import camp.woowak.lab.common.exception.UnauthorizedException; import camp.woowak.lab.infra.date.DateTimeProvider; +import camp.woowak.lab.menu.exception.MenuOwnerNotMatchException; import camp.woowak.lab.menu.exception.UnauthorizedMenuCategoryCreationException; import camp.woowak.lab.menu.service.MenuCategoryRegistrationService; +import camp.woowak.lab.menu.service.MenuPriceUpdateService; import camp.woowak.lab.menu.service.command.MenuCategoryRegistrationCommand; +import camp.woowak.lab.menu.service.command.MenuPriceUpdateCommand; import camp.woowak.lab.payaccount.domain.PayAccount; import camp.woowak.lab.payaccount.domain.TestPayAccount; import camp.woowak.lab.store.exception.NotFoundStoreCategoryException; @@ -43,6 +46,7 @@ import camp.woowak.lab.web.authentication.PasswordEncoder; import camp.woowak.lab.web.dao.store.StoreDao; import camp.woowak.lab.web.dto.request.store.MenuCategoryRegistrationRequest; +import camp.woowak.lab.web.dto.request.store.MenuPriceUpdateRequest; import camp.woowak.lab.web.dto.request.store.StoreRegistrationRequest; import camp.woowak.lab.web.resolver.session.SessionConst; import camp.woowak.lab.web.resolver.session.SessionVendorArgumentResolver; @@ -72,6 +76,9 @@ class StoreApiControllerTest { @MockBean private SessionVendorArgumentResolver sessionVendorArgumentResolver; + @MockBean + private MenuPriceUpdateService menuPriceUpdateService; + DateTimeProvider fixedStartTime = () -> LocalDateTime.of(2024, 8, 24, 1, 0, 0); DateTimeProvider fixedEndTime = () -> LocalDateTime.of(2024, 8, 24, 5, 0, 0); @@ -251,6 +258,140 @@ void failWith403() throws Exception { } } + @Nested + @DisplayName("메뉴 가격 수정 : PUT /stores/menus/{menuId}/price") + class MenuPriceUpdate { + @Test + @DisplayName("[Success] 200 OK") + void success() throws Exception { + //given + LoginVendor loginVendor = new LoginVendor(UUID.randomUUID()); + Long updatePrice = 10000L; + MenuPriceUpdateRequest request = new MenuPriceUpdateRequest(updatePrice); + + when(sessionVendorArgumentResolver.supportsParameter(any())) + .thenReturn(true); + when(sessionVendorArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(loginVendor); + when(menuPriceUpdateService.updateMenuPrice(any(MenuPriceUpdateCommand.class))) + .thenReturn(updatePrice); + + //when & then + mockMvc.perform(patch("/stores/menus/" + new Random().nextLong() + "/price") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .sessionAttr(SessionConst.SESSION_VENDOR_KEY, loginVendor)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.updatedPrice").value(updatePrice)); + } + + @Test + @DisplayName("[Exception] 400 BadRequest with null price") + void badRequestWithNullPrice() throws Exception { + //given + LoginVendor loginVendor = new LoginVendor(UUID.randomUUID()); + Long updatePrice = null; + MenuPriceUpdateRequest request = new MenuPriceUpdateRequest(updatePrice); + + when(sessionVendorArgumentResolver.supportsParameter(any())) + .thenReturn(true); + when(sessionVendorArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(loginVendor); + + //when & then + mockMvc.perform(patch("/stores/menus/" + new Random().nextLong() + "/price") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .sessionAttr(SessionConst.SESSION_VENDOR_KEY, loginVendor)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("[Exception] 400 BadRequest with zero price") + void badRequestWithZeroPrice() throws Exception { + //given + LoginVendor loginVendor = new LoginVendor(UUID.randomUUID()); + Long updatePrice = 0L; + MenuPriceUpdateRequest request = new MenuPriceUpdateRequest(updatePrice); + + when(sessionVendorArgumentResolver.supportsParameter(any())) + .thenReturn(true); + when(sessionVendorArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(loginVendor); + + //when & then + mockMvc.perform(patch("/stores/menus/" + new Random().nextLong() + "/price") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .sessionAttr(SessionConst.SESSION_VENDOR_KEY, loginVendor)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("[Exception] 400 BadRequest with negative price") + void badRequestWithNegativePrice() throws Exception { + //given + LoginVendor loginVendor = new LoginVendor(UUID.randomUUID()); + Long updatePrice = -1000L; + MenuPriceUpdateRequest request = new MenuPriceUpdateRequest(updatePrice); + + when(sessionVendorArgumentResolver.supportsParameter(any())) + .thenReturn(true); + when(sessionVendorArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(loginVendor); + + //when & then + mockMvc.perform(patch("/stores/menus/" + new Random().nextLong() + "/price") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .sessionAttr(SessionConst.SESSION_VENDOR_KEY, loginVendor)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("[Exception] 401 Unauthorized") + void unAuthorizedException() throws Exception { + //given + Long updatePrice = -1000L; + MenuPriceUpdateRequest request = new MenuPriceUpdateRequest(updatePrice); + + when(sessionVendorArgumentResolver.supportsParameter(any())) + .thenReturn(true); + when(sessionVendorArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenThrow( + new UnauthorizedException(AuthenticationErrorCode.UNAUTHORIZED, "Vendor가 세션에 저장되어 있지 않습니다.")); + + //when & then + mockMvc.perform(patch("/stores/menus/" + new Random().nextLong() + "/price") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("[Exception] 403 Forbidden") + void forbidden() throws Exception { + //given + LoginVendor loginVendor = new LoginVendor(UUID.randomUUID()); + Long updatePrice = 1000L; + MenuPriceUpdateRequest request = new MenuPriceUpdateRequest(updatePrice); + + when(sessionVendorArgumentResolver.supportsParameter(any())) + .thenReturn(true); + when(sessionVendorArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(loginVendor); + when(menuPriceUpdateService.updateMenuPrice(any(MenuPriceUpdateCommand.class))) + .thenThrow(new MenuOwnerNotMatchException("메뉴 주인이 아닙니다.")); + + //when & then + mockMvc.perform(patch("/stores/menus/" + new Random().nextLong() + "/price") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .sessionAttr(SessionConst.SESSION_VENDOR_KEY, loginVendor)) + .andExpect(status().isForbidden()); + } + } + private Vendor createVendor() { return new Vendor("vendorName", "vendorEmail@example.com", "vendorPassword", "010-0000-0000", payAccount, passwordEncoder);