diff --git a/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuStockUpdateException.java b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuStockUpdateException.java new file mode 100644 index 00000000..d62ec3dd --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/InvalidMenuStockUpdateException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.menu.exception; + +import camp.woowak.lab.common.exception.ConflictException; + +public class InvalidMenuStockUpdateException extends ConflictException { + public InvalidMenuStockUpdateException(String message) { + super(MenuErrorCode.INVALID_UPDATE_MENU_STOCK, 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 b82d2e1f..0f2417fb 100644 --- a/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java +++ b/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java @@ -21,7 +21,13 @@ public enum MenuErrorCode implements ErrorCode { MENU_OWNER_NOT_MATCH(HttpStatus.FORBIDDEN, "m_10", "메뉴는 가게의 점주만 수정할 수 있습니다."), - NOT_ENOUGH_STOCK(HttpStatus.BAD_REQUEST, "M4", "재고가 부족합니다."); + NOT_ENOUGH_STOCK(HttpStatus.BAD_REQUEST, "M4", "재고가 부족합니다."), + + INVALID_UPDATE_MENU_STOCK(HttpStatus.CONFLICT, "m_11", "메뉴의 재고를 변경할 수 없습니다."), + + NOT_EQUALS_OWNER(HttpStatus.BAD_REQUEST, "m_12", "매장의 점주와 일치하지 않습니다."), + NOT_UPDATABLE_TIME(HttpStatus.CONFLICT, "m_13", "메뉴를 변경할 수 없는 시간입니다."), + ; private final int status; private final String errorCode; diff --git a/src/main/java/camp/woowak/lab/menu/exception/NotEqualsOwnerException.java b/src/main/java/camp/woowak/lab/menu/exception/NotEqualsOwnerException.java new file mode 100644 index 00000000..9429b129 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/NotEqualsOwnerException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.menu.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class NotEqualsOwnerException extends BadRequestException { + public NotEqualsOwnerException(String message) { + super(MenuErrorCode.NOT_EQUALS_OWNER, message); + } +} diff --git a/src/main/java/camp/woowak/lab/menu/exception/NotUpdatableTimeException.java b/src/main/java/camp/woowak/lab/menu/exception/NotUpdatableTimeException.java new file mode 100644 index 00000000..8af402b3 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/NotUpdatableTimeException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.menu.exception; + +import camp.woowak.lab.common.exception.ConflictException; + +public class NotUpdatableTimeException extends ConflictException { + public NotUpdatableTimeException(String message) { + super(MenuErrorCode.NOT_UPDATABLE_TIME, message); + } +} diff --git a/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java b/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java index 7ee862bd..2b410830 100644 --- a/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java +++ b/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -20,4 +21,18 @@ public interface MenuRepository extends JpaRepository { List findAllByIdForUpdate(List ids); List findByStoreId(Long storeId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT m FROM Menu m WHERE m.id = :id") + Optional findByIdForUpdate(Long id); + + /** + * + * 메뉴의 재고를 변경합니다. + * TODO: [논의] @Transactional을 Respository 단에 안둬도되는가? + * Repository 에서 직접 접근할 때 사용자가 실수해서 @Transactional 을 빼먹을 수도 있다. + */ + @Modifying + @Query("UPDATE Menu m SET m.stockCount = :stock WHERE m.id = :id") + int updateStock(@Param("id") Long id, @Param("stock") int stock); } diff --git a/src/main/java/camp/woowak/lab/menu/service/UpdateMenuStockService.java b/src/main/java/camp/woowak/lab/menu/service/UpdateMenuStockService.java new file mode 100644 index 00000000..e741e1f1 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/service/UpdateMenuStockService.java @@ -0,0 +1,56 @@ +package camp.woowak.lab.menu.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.cart.exception.MenuNotFoundException; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.exception.InvalidMenuStockUpdateException; +import camp.woowak.lab.menu.exception.NotEqualsOwnerException; +import camp.woowak.lab.menu.exception.NotUpdatableTimeException; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.menu.service.command.UpdateMenuStockCommand; + +@Service +public class UpdateMenuStockService { + private final MenuRepository menuRepository; + + public UpdateMenuStockService(MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + /** + * + * @throws MenuNotFoundException 메뉴를 찾을 수 없는 경우 발생한다. + * @throws NotEqualsOwnerException 메뉴를 소유한 가게의 주인이 아닌 경우 발생한다. + * @throws NotUpdatableTimeException 가게가 열려있지 않은 경우 발생한다. + * @throws InvalidMenuStockUpdateException 메뉴의 재고를 변경할 수 없는 경우 발생한다. + */ + @Transactional + public Long updateMenuStock(UpdateMenuStockCommand cmd) { + // 수량을 변경하려는 메뉴를 조회한다. + Menu targetMenu = findMenuByIdForUpdateOrThrow(cmd.menuId()); + + // 메뉴를 소유한 가게를 조회한다. + if (!targetMenu.getStore().isOwnedBy(cmd.vendorId())) { + throw new NotEqualsOwnerException("메뉴를 소유한 가게의 주인이 아닙니다."); + } + + // 가게가 열려있는지 확인한다. + if (targetMenu.getStore().isOpen()) { + throw new NotUpdatableTimeException("가게가 열려 있습니다."); + } + + // 메뉴의 재고를 변경한다. + int modifiedRow = menuRepository.updateStock(cmd.menuId(), cmd.stock()); + if (modifiedRow != 1) { // 변경된 메뉴의 개수가 1이 아닌 경우 예외를 발생시킨다. + throw new InvalidMenuStockUpdateException("변경의 영향을 받은 메뉴의 개수가 1이 아닙니다."); + } + + return targetMenu.getId(); + } + + private Menu findMenuByIdForUpdateOrThrow(Long menuId) { + return menuRepository.findByIdForUpdate(menuId).orElseThrow(() -> new MenuNotFoundException("메뉴를 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/camp/woowak/lab/menu/service/command/UpdateMenuStockCommand.java b/src/main/java/camp/woowak/lab/menu/service/command/UpdateMenuStockCommand.java new file mode 100644 index 00000000..dc2a9bed --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/service/command/UpdateMenuStockCommand.java @@ -0,0 +1,6 @@ +package camp.woowak.lab.menu.service.command; + +import java.util.UUID; + +public record UpdateMenuStockCommand(Long menuId, int stock, UUID vendorId) { +} diff --git a/src/main/java/camp/woowak/lab/web/api/menu/MenuApiController.java b/src/main/java/camp/woowak/lab/web/api/menu/MenuApiController.java new file mode 100644 index 00000000..9188f56e --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/menu/MenuApiController.java @@ -0,0 +1,39 @@ +package camp.woowak.lab.web.api.menu; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import camp.woowak.lab.menu.service.UpdateMenuStockService; +import camp.woowak.lab.menu.service.command.UpdateMenuStockCommand; +import camp.woowak.lab.web.authentication.LoginVendor; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import camp.woowak.lab.web.dto.request.menu.UpdateMenuStockRequest; +import camp.woowak.lab.web.dto.response.menu.UpdateMenuStockResponse; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +public class MenuApiController { + private final UpdateMenuStockService updateMenuStockService; + + public MenuApiController(UpdateMenuStockService updateMenuStockService) { + this.updateMenuStockService = updateMenuStockService; + } + + @ResponseStatus(HttpStatus.OK) + @PatchMapping("/menus/stock") + public UpdateMenuStockResponse updateMenuStock(@AuthenticationPrincipal LoginVendor vendor, + @Valid @RequestBody UpdateMenuStockRequest request) { + UpdateMenuStockCommand cmd = new UpdateMenuStockCommand(request.menuId(), request.stock(), vendor.getId()); + + Long updatedId = updateMenuStockService.updateMenuStock(cmd); + + log.info("메뉴 재고 업데이트 완료: menuId={}, newStock={}, vendorId={}", updatedId, request.stock(), vendor.getId()); + + return new UpdateMenuStockResponse(updatedId); + } +} diff --git a/src/main/java/camp/woowak/lab/web/api/menu/MenuExceptionHandler.java b/src/main/java/camp/woowak/lab/web/api/menu/MenuExceptionHandler.java new file mode 100644 index 00000000..da8763b5 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/menu/MenuExceptionHandler.java @@ -0,0 +1,40 @@ +package camp.woowak.lab.web.api.menu; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import camp.woowak.lab.common.advice.DomainExceptionHandler; +import camp.woowak.lab.common.exception.HttpStatusException; +import camp.woowak.lab.menu.exception.InvalidMenuStockUpdateException; +import camp.woowak.lab.menu.exception.NotEqualsOwnerException; +import camp.woowak.lab.menu.exception.NotUpdatableTimeException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@DomainExceptionHandler(basePackageClasses = MenuApiController.class) +public class MenuExceptionHandler { + + @ExceptionHandler(value = NotUpdatableTimeException.class) + public ProblemDetail handleInvalidMenuStockUpdateException(NotUpdatableTimeException e) { + log.warn("Conflict", e); + return getProblemDetail(HttpStatus.CONFLICT, e); + } + + @ExceptionHandler(value = NotEqualsOwnerException.class) + public ProblemDetail handleNotEqualsOwnerException(NotEqualsOwnerException e) { + log.warn("Bad Request", e); + return getProblemDetail(HttpStatus.BAD_REQUEST, e); + } + + @ExceptionHandler(value = InvalidMenuStockUpdateException.class) + public ProblemDetail handleInvalidMenuStockUpdateException(InvalidMenuStockUpdateException e) { + log.warn("Conflict", e); + return getProblemDetail(HttpStatus.CONFLICT, e); + } + + private ProblemDetail getProblemDetail(HttpStatus status, HttpStatusException e) { + return ProblemDetail.forStatusAndDetail(status, e.errorCode().getMessage()); + } + +} diff --git a/src/main/java/camp/woowak/lab/web/dto/request/menu/UpdateMenuStockRequest.java b/src/main/java/camp/woowak/lab/web/dto/request/menu/UpdateMenuStockRequest.java new file mode 100644 index 00000000..41865ea0 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/request/menu/UpdateMenuStockRequest.java @@ -0,0 +1,11 @@ +package camp.woowak.lab.web.dto.request.menu; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +public record UpdateMenuStockRequest( + @NotNull(message = "메뉴 ID는 필수입니다.") + Long menuId, + @Min(value = 0, message = "재고는 0 이상이어야 합니다.") + int stock) { +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/menu/UpdateMenuStockResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/menu/UpdateMenuStockResponse.java new file mode 100644 index 00000000..19287ad9 --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/menu/UpdateMenuStockResponse.java @@ -0,0 +1,4 @@ +package camp.woowak.lab.web.dto.response.menu; + +public record UpdateMenuStockResponse(Long menuId) { +} diff --git a/src/test/java/camp/woowak/lab/menu/service/UpdateMenuStockServiceTest.java b/src/test/java/camp/woowak/lab/menu/service/UpdateMenuStockServiceTest.java new file mode 100644 index 00000000..cd5de0ad --- /dev/null +++ b/src/test/java/camp/woowak/lab/menu/service/UpdateMenuStockServiceTest.java @@ -0,0 +1,162 @@ +package camp.woowak.lab.menu.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import camp.woowak.lab.cart.exception.MenuNotFoundException; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.exception.InvalidMenuStockUpdateException; +import camp.woowak.lab.menu.exception.NotEqualsOwnerException; +import camp.woowak.lab.menu.exception.NotUpdatableTimeException; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.menu.service.command.UpdateMenuStockCommand; +import camp.woowak.lab.store.domain.Store; + +@ExtendWith(MockitoExtension.class) +class UpdateMenuStockServiceTest { + @InjectMocks + private UpdateMenuStockService updateMenuStockService; + + @Mock + private MenuRepository menuRepository; + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 성공") + void testUpdateMenuStock() { + // given + Long menuId = 1L; + int stock = 10; + UUID vendorId = UUID.randomUUID(); + Menu fakeMenu = Mockito.mock(Menu.class); + Store fakeStore = Mockito.mock(Store.class); + + given(fakeMenu.getStore()).willReturn(fakeStore); + given(fakeStore.isOwnedBy(any(UUID.class))).willReturn(true); + given(menuRepository.findByIdForUpdate(anyLong())).willReturn(Optional.of(fakeMenu)); + given(fakeStore.isOpen()).willReturn(false); + given(menuRepository.updateStock(anyLong(), anyInt())).willReturn(1); + + UpdateMenuStockCommand cmd = new UpdateMenuStockCommand(menuId, stock, vendorId); + + // when + updateMenuStockService.updateMenuStock(cmd); + + // then + verify(menuRepository, times(1)).findByIdForUpdate(anyLong()); + verify(fakeMenu, times(2)).getStore(); + verify(fakeStore, times(1)).isOwnedBy(any(UUID.class)); + verify(fakeStore, times(1)).isOpen(); + verify(menuRepository, times(1)).updateStock(anyLong(), anyInt()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 메뉴를 찾을 수 없는 경우") + void testUpdateMenuStockNotFound() { + // given + Long menuId = 1L; + int stock = 10; + UUID vendorId = UUID.randomUUID(); + + given(menuRepository.findByIdForUpdate(anyLong())).willReturn(Optional.empty()); + + UpdateMenuStockCommand cmd = new UpdateMenuStockCommand(menuId, stock, vendorId); + + // when + // then + assertThrows(MenuNotFoundException.class, () -> updateMenuStockService.updateMenuStock(cmd)); + verify(menuRepository, times(1)).findByIdForUpdate(anyLong()); + verify(menuRepository, never()).updateStock(anyLong(), anyInt()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 메뉴를 소유한 가게의 주인이 아닌 경우") + void testUpdateMenuStockNotEqualsOwner() { + // given + Long menuId = 1L; + int stock = 10; + UUID vendorId = UUID.randomUUID(); + Menu fakeMenu = Mockito.mock(Menu.class); + Store fakeStore = Mockito.mock(Store.class); + + given(fakeMenu.getStore()).willReturn(fakeStore); + given(fakeStore.isOwnedBy(any(UUID.class))).willReturn(false); + given(menuRepository.findByIdForUpdate(anyLong())).willReturn(Optional.of(fakeMenu)); + + UpdateMenuStockCommand cmd = new UpdateMenuStockCommand(menuId, stock, vendorId); + + // when + // then + assertThrows(NotEqualsOwnerException.class, () -> updateMenuStockService.updateMenuStock(cmd)); + verify(menuRepository, times(1)).findByIdForUpdate(anyLong()); + verify(fakeMenu, times(1)).getStore(); + verify(fakeStore, times(1)).isOwnedBy(any(UUID.class)); + verify(menuRepository, never()).updateStock(anyLong(), anyInt()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 매장이 열려있는 경우") + void testUpdateMenuStockStoreNotOpen() { + // given + Long menuId = 1L; + int stock = 10; + UUID vendorId = UUID.randomUUID(); + Menu fakeMenu = Mockito.mock(Menu.class); + Store fakeStore = Mockito.mock(Store.class); + + given(fakeMenu.getStore()).willReturn(fakeStore); + given(fakeStore.isOwnedBy(any(UUID.class))).willReturn(true); + given(fakeStore.isOpen()).willReturn(true); + given(menuRepository.findByIdForUpdate(anyLong())).willReturn(Optional.of(fakeMenu)); + + UpdateMenuStockCommand cmd = new UpdateMenuStockCommand(menuId, stock, vendorId); + + // when + // then + assertThrows(NotUpdatableTimeException.class, () -> updateMenuStockService.updateMenuStock(cmd)); + verify(menuRepository, times(1)).findByIdForUpdate(anyLong()); + verify(fakeMenu, times(2)).getStore(); + verify(fakeStore, times(1)).isOwnedBy(any(UUID.class)); + verify(fakeStore, times(1)).isOpen(); + verify(menuRepository, never()).updateStock(anyLong(), anyInt()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 메뉴의 재고를 변경할 수 없는 경우") + void testUpdateMenuStockInvalid() { + // given + Long menuId = 1L; + int stock = 10; + UUID vendorId = UUID.randomUUID(); + Menu fakeMenu = Mockito.mock(Menu.class); + Store fakeStore = Mockito.mock(Store.class); + + given(fakeMenu.getStore()).willReturn(fakeStore); + given(fakeStore.isOwnedBy(any(UUID.class))).willReturn(true); + given(fakeStore.isOpen()).willReturn(false); + given(menuRepository.findByIdForUpdate(anyLong())).willReturn(Optional.of(fakeMenu)); + given(menuRepository.updateStock(anyLong(), anyInt())).willReturn(0); + + UpdateMenuStockCommand cmd = new UpdateMenuStockCommand(menuId, stock, vendorId); + + // when + // then + assertThrows(InvalidMenuStockUpdateException.class, () -> updateMenuStockService.updateMenuStock(cmd)); + verify(menuRepository, times(1)).findByIdForUpdate(anyLong()); + verify(fakeMenu, times(2)).getStore(); + verify(fakeStore, times(1)).isOwnedBy(any(UUID.class)); + verify(fakeStore, times(1)).isOpen(); + + verify(menuRepository, times(1)).updateStock(anyLong(), anyInt()); + } +} diff --git a/src/test/java/camp/woowak/lab/web/api/menu/MenuApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/menu/MenuApiControllerTest.java new file mode 100644 index 00000000..830acd59 --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/menu/MenuApiControllerTest.java @@ -0,0 +1,157 @@ +package camp.woowak.lab.web.api.menu; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import camp.woowak.lab.menu.exception.NotEqualsOwnerException; +import camp.woowak.lab.menu.exception.NotUpdatableTimeException; +import camp.woowak.lab.menu.service.UpdateMenuStockService; +import camp.woowak.lab.menu.service.command.UpdateMenuStockCommand; +import camp.woowak.lab.web.authentication.LoginVendor; +import camp.woowak.lab.web.dto.request.menu.UpdateMenuStockRequest; +import camp.woowak.lab.web.resolver.session.SessionConst; + +@WebMvcTest(MenuApiController.class) +@MockBean(JpaMetamodelMappingContext.class) +class MenuApiControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UpdateMenuStockService updateMenuStockService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 성공") + void testUpdateMenuStock() throws Exception { + // given + Long menuId = 1L; + int stock = 10; + MockHttpSession session = new MockHttpSession(); + session.setAttribute(SessionConst.SESSION_VENDOR_KEY, new LoginVendor(UUID.randomUUID())); + UpdateMenuStockRequest request = new UpdateMenuStockRequest(menuId, stock); + + given(updateMenuStockService.updateMenuStock(any(UpdateMenuStockCommand.class))).willReturn(menuId); + + // when + mockMvc.perform(patch("/menus/stock") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .session(session)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.menuId").value(menuId)); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 실패(stock이 0 미만)") + void testUpdateMenuStockFailWithStockUnderZero() throws Exception { + // given + Long menuId = 1L; + int stock = -1; + MockHttpSession session = new MockHttpSession(); + session.setAttribute(SessionConst.SESSION_VENDOR_KEY, new LoginVendor(UUID.randomUUID())); + UpdateMenuStockRequest request = new UpdateMenuStockRequest(menuId, stock); + + // when + mockMvc.perform(patch("/menus/stock") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .session(session)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 실패(메뉴id null)") + void testUpdateMenuStockFailWithMenuIdNull() throws Exception { + // given + Long menuId = null; + int stock = 10; + MockHttpSession session = new MockHttpSession(); + session.setAttribute(SessionConst.SESSION_VENDOR_KEY, new LoginVendor(UUID.randomUUID())); + UpdateMenuStockRequest request = new UpdateMenuStockRequest(menuId, stock); + + // when + mockMvc.perform(patch("/menus/stock") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .session(session)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 실패(점주 불일치)") + void testUpdateMenuStockFail() throws Exception { + // given + Long menuId = 1L; + int stock = 10; + MockHttpSession session = new MockHttpSession(); + session.setAttribute(SessionConst.SESSION_VENDOR_KEY, new LoginVendor(UUID.randomUUID())); + UpdateMenuStockRequest request = new UpdateMenuStockRequest(menuId, stock); + + given(updateMenuStockService.updateMenuStock(any(UpdateMenuStockCommand.class))).willThrow( + new NotEqualsOwnerException("매장 주인이 아닙니다.")); + + // when + mockMvc.perform(patch("/menus/stock") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .session(session)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 실패(인증되지 않음)") + void testUpdateMenuStockFailWithUnauthorized() throws Exception { + // given + Long menuId = 1L; + int stock = 10; + UpdateMenuStockRequest request = new UpdateMenuStockRequest(menuId, stock); + + // when + mockMvc.perform(patch("/menus/stock") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("메뉴 재고 업데이트 테스트 - 실패(매장이 열려있음)") + void testUpdateMenuStockFailWithStoreNotOpen() throws Exception { + // given + Long menuId = 1L; + int stock = 10; + MockHttpSession session = new MockHttpSession(); + session.setAttribute(SessionConst.SESSION_VENDOR_KEY, new LoginVendor(UUID.randomUUID())); + UpdateMenuStockRequest request = new UpdateMenuStockRequest(menuId, stock); + + given(updateMenuStockService.updateMenuStock(any(UpdateMenuStockCommand.class))).willThrow( + new NotUpdatableTimeException("매장이 열려있습니다.")); + + // when + mockMvc.perform(patch("/menus/stock") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .session(session)) + .andExpect(status().isConflict()); + } + +}