diff --git a/src/main/java/site/campingon/campingon/bookmark/repository/BookmarkRepository.java b/src/main/java/site/campingon/campingon/bookmark/repository/BookmarkRepository.java index a02de820..b821eb4c 100644 --- a/src/main/java/site/campingon/campingon/bookmark/repository/BookmarkRepository.java +++ b/src/main/java/site/campingon/campingon/bookmark/repository/BookmarkRepository.java @@ -2,8 +2,6 @@ import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import site.campingon.campingon.bookmark.entity.Bookmark; @@ -13,11 +11,5 @@ public interface BookmarkRepository extends JpaRepository { boolean existsByCampIdAndUserId(Long campId, Long userId); - @Query("SELECT b FROM Bookmark b " + - "JOIN FETCH b.camp c " + - "JOIN FETCH c.campAddr " + - "JOIN FETCH c.campInfo " + - "JOIN FETCH b.user " + - "WHERE b.camp.id = :campId AND b.user.id = :userId") - Optional findByCampIdAndUserId(@Param("campId") Long campId, @Param("userId") Long userId); + Optional findByCampIdAndUserId(Long campId, Long userId); } \ No newline at end of file diff --git a/src/main/java/site/campingon/campingon/camp/entity/Camp.java b/src/main/java/site/campingon/campingon/camp/entity/Camp.java index 8787635c..a71efb50 100644 --- a/src/main/java/site/campingon/campingon/camp/entity/Camp.java +++ b/src/main/java/site/campingon/campingon/camp/entity/Camp.java @@ -2,8 +2,6 @@ import jakarta.persistence.*; import lombok.*; -import org.hibernate.annotations.LazyToOne; -import org.hibernate.annotations.LazyToOneOption; import site.campingon.campingon.bookmark.entity.Bookmark; import site.campingon.campingon.common.public_data.dto.GoCampingParsedResponseDto; @@ -67,10 +65,10 @@ public class Camp{ @Builder.Default private List induty=new ArrayList<>(); // 업종 - @OneToOne(mappedBy = "camp", cascade = CascadeType.ALL, orphanRemoval = true,fetch = FetchType.LAZY) + @OneToOne(mappedBy = "camp", cascade = CascadeType.ALL, orphanRemoval = true) private CampAddr campAddr; - @OneToOne(mappedBy = "camp", cascade = CascadeType.ALL, orphanRemoval = true,fetch = FetchType.LAZY) + @OneToOne(mappedBy = "camp", cascade = CascadeType.ALL, orphanRemoval = true) private CampInfo campInfo; @OneToMany(mappedBy = "camp", cascade = CascadeType.ALL, orphanRemoval = true) diff --git a/src/main/java/site/campingon/campingon/camp/repository/CampInfoRepository.java b/src/main/java/site/campingon/campingon/camp/repository/CampInfoRepository.java index 0fa4bd1d..14f979c1 100644 --- a/src/main/java/site/campingon/campingon/camp/repository/CampInfoRepository.java +++ b/src/main/java/site/campingon/campingon/camp/repository/CampInfoRepository.java @@ -1,8 +1,6 @@ package site.campingon.campingon.camp.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import site.campingon.campingon.camp.entity.CampInfo; @@ -10,9 +8,5 @@ @Repository public interface CampInfoRepository extends JpaRepository { - @Query("SELECT ci FROM CampInfo ci " + - "JOIN FETCH ci.camp c " + - "JOIN FETCH c.campAddr " + - "WHERE c.id = :campId") - Optional findByCampId(@Param("campId") Long id); + Optional findByCampId(Long id); } \ No newline at end of file diff --git a/src/main/java/site/campingon/campingon/camp/repository/CampRepository.java b/src/main/java/site/campingon/campingon/camp/repository/CampRepository.java index b14c600f..b4d62be5 100644 --- a/src/main/java/site/campingon/campingon/camp/repository/CampRepository.java +++ b/src/main/java/site/campingon/campingon/camp/repository/CampRepository.java @@ -10,39 +10,21 @@ import site.campingon.campingon.camp.entity.Camp; import java.util.List; -import java.util.Optional; @Repository public interface CampRepository extends JpaRepository { // 캠핑장 정보의 추천수의 내림차순으로 캠핑장 정렬 - @Query(value = """ + @Query(""" SELECT c FROM Camp c - JOIN FETCH c.campInfo ci - JOIN FETCH c.campAddr - ORDER BY ci.recommendCnt DESC, c.thumbImage DESC - """, countQuery = "SELECT COUNT(c) FROM Camp c" - ) + JOIN CampInfo ci ON c.id = ci.camp.id + ORDER BY ci.recommendCnt DESC + """) Page findPopularCamps(Pageable pageable); - @Query(""" - SELECT c FROM Camp c - JOIN FETCH c.campInfo - JOIN FETCH c.campAddr - WHERE c.id= :campId - """) - Optional findById(@Param("campId") Long campId); - // 사용자의 isMarked 된 캠핑장 목록 페이지 - @Query(value = """ - SELECT c FROM Camp c - JOIN FETCH c.campInfo - JOIN FETCH c.campAddr - JOIN c.bookmarks b - WHERE b.user.id = :userId - ORDER BY b.createdAt DESC - """,countQuery = "SELECT COUNT(c) FROM Camp c JOIN c.bookmarks b WHERE b.user.id= :userId") - Page findByBookmarksUserId(@Param("userId") Long userId, Pageable pageable); + @Query("SELECT c FROM Camp c JOIN c.bookmarks b WHERE b.user.id = :userId ORDER BY b.createdAt DESC") + Page findByBookmarks_User_Id(@Param("userId") Long userId, Pageable pageable); //쿼리 최적화 where in 조건 @Modifying diff --git a/src/main/java/site/campingon/campingon/camp/repository/CampSiteRepository.java b/src/main/java/site/campingon/campingon/camp/repository/CampSiteRepository.java index d3d6b079..d22544c4 100644 --- a/src/main/java/site/campingon/campingon/camp/repository/CampSiteRepository.java +++ b/src/main/java/site/campingon/campingon/camp/repository/CampSiteRepository.java @@ -21,11 +21,8 @@ public interface CampSiteRepository extends JpaRepository { """) List findByCampId(@Param("campId") Long campId); - @Query(value = """ + @Query(""" SELECT cs FROM CampSite cs - JOIN FETCH cs.camp c - JOIN FETCH c.campInfo - JOIN FETCH c.campAddr WHERE cs.id = :siteId AND cs.camp.id = :campId """) diff --git a/src/main/java/site/campingon/campingon/camp/service/CampService.java b/src/main/java/site/campingon/campingon/camp/service/CampService.java index f3d09b31..517bbbb8 100644 --- a/src/main/java/site/campingon/campingon/camp/service/CampService.java +++ b/src/main/java/site/campingon/campingon/camp/service/CampService.java @@ -12,8 +12,10 @@ import site.campingon.campingon.camp.mapper.CampMapper; import site.campingon.campingon.camp.repository.CampRepository; import site.campingon.campingon.bookmark.repository.BookmarkRepository; +import site.campingon.campingon.camp.repository.mongodb.MongoSearchClient; import site.campingon.campingon.common.exception.ErrorCode; import site.campingon.campingon.common.exception.GlobalException; +import site.campingon.campingon.user.repository.UserKeywordRepository; import java.util.Collections; import java.util.List; @@ -60,7 +62,7 @@ public CampDetailResponseDto getCampDetail(Long campId) { // 사용자의 찜한 캠핑장 목록 조회 public Page getBookmarkedCamps(Long userId, Pageable pageable) { - Page bookmarkedCamps = campRepository.findByBookmarksUserId(userId, pageable); + Page bookmarkedCamps = campRepository.findByBookmarks_User_Id(userId, pageable); List campDtos = bookmarkedCamps.getContent().stream() .map(camp -> { diff --git a/src/test/java/site/campingon/campingon/camp/service/CampServiceTest.java b/src/test/java/site/campingon/campingon/camp/service/CampServiceTest.java index 1621956b..fddb9db0 100644 --- a/src/test/java/site/campingon/campingon/camp/service/CampServiceTest.java +++ b/src/test/java/site/campingon/campingon/camp/service/CampServiceTest.java @@ -295,7 +295,7 @@ void getBookmarkedCamps_success() { List camps = Arrays.asList(mockCamp); Page campPage = new PageImpl<>(camps, pageable, camps.size()); - when(campRepository.findByBookmarksUserId(userId, pageable)) + when(campRepository.findByBookmarks_User_Id(userId, pageable)) .thenReturn(campPage); when(campMapper.toCampListDto(any(Camp.class))).thenReturn(mockCampListDto); @@ -307,7 +307,7 @@ void getBookmarkedCamps_success() { assertEquals(1, result.getTotalElements()); assertTrue(result.getContent().get(0).isMarked()); - verify(campRepository).findByBookmarksUserId(userId, pageable); + verify(campRepository).findByBookmarks_User_Id(userId, pageable); verify(campMapper).toCampListDto(any(Camp.class)); } @@ -319,7 +319,7 @@ void getBookmarkedCamps_noBookmarks_returnsEmptyList() { Pageable pageable = PageRequest.of(0, 3); Page emptyPage = new PageImpl<>(Collections.emptyList(), pageable, 0); - when(campRepository.findByBookmarksUserId(userId, pageable)) + when(campRepository.findByBookmarks_User_Id(userId, pageable)) .thenReturn(emptyPage); // when @@ -328,7 +328,7 @@ void getBookmarkedCamps_noBookmarks_returnsEmptyList() { // then assertTrue(result.isEmpty()); assertEquals(0, result.getTotalElements()); - verify(campRepository).findByBookmarksUserId(userId, pageable); + verify(campRepository).findByBookmarks_User_Id(userId, pageable); } // 캠핑장 관리자 테스트 CRUD diff --git a/src/test/java/site/campingon/campingon/common/public_data/service/GoCampingServiceTest.java b/src/test/java/site/campingon/campingon/common/public_data/service/GoCampingServiceTest.java index bba86168..d31adf54 100644 --- a/src/test/java/site/campingon/campingon/common/public_data/service/GoCampingServiceTest.java +++ b/src/test/java/site/campingon/campingon/common/public_data/service/GoCampingServiceTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.web.client.RestTemplate; import site.campingon.campingon.camp.repository.*; import site.campingon.campingon.common.public_data.GoCampingPath; @@ -51,7 +50,7 @@ class GoCampingServiceTest { @Mock private CampIndutyRepository campIndutyRepository; - @Mock + @Spy private RestTemplate restTemplate; @@ -67,8 +66,6 @@ void testGetAndConvertToGoCampingDataDto_Success() throws Exception { String numOfRows = "1"; String pageNo = "1"; - URI mockUri = new URI("http://example.com"); // 반환할 실제 URI 객체 생성 - // Item 객체 생성 GoCampingDataDto.Item item = GoCampingDataDto.Item.builder() .contentId(123L).facltNm("Sample Camping Site") @@ -101,10 +98,10 @@ void testGetAndConvertToGoCampingDataDto_Success() throws Exception { ) .build(); System.out.println("goCampingDataDto :" + goCampingDataDto); - when(goCampingProviderService.createUri(any(GoCampingPath.class),anyString(), anyString(), anyString(), anyString())).thenReturn(mockUri); - doReturn(goCampingDataDto).when(restTemplate).getForObject(eq(mockUri), eq(GoCampingDataDto.class)); + doReturn(goCampingDataDto).when(restTemplate).getForObject(any(URI.class), eq(GoCampingDataDto.class)); + //when GoCampingDataDto result = goCampingService.fetchCampData(GoCampingPath.BASED_LIST, "numOfRows", numOfRows, "pageNo", pageNo); // then diff --git a/src/test/java/site/campingon/campingon/review/service/ReviewServiceTest.java b/src/test/java/site/campingon/campingon/review/service/ReviewServiceTest.java index b890c0c0..e5ab2835 100644 --- a/src/test/java/site/campingon/campingon/review/service/ReviewServiceTest.java +++ b/src/test/java/site/campingon/campingon/review/service/ReviewServiceTest.java @@ -125,7 +125,7 @@ void createReview_Success() throws IOException { ReviewResponseDto.builder() .reviewId(savedReview.getId()) .content(savedReview.getContent()) - .recommended(savedReview.isRecommend()) + .isRecommend(savedReview.isRecommend()) .build() ); @@ -136,34 +136,34 @@ void createReview_Success() throws IOException { assertNotNull(responseDto); assertEquals(savedReview.getId(), responseDto.getReviewId()); assertEquals(requestDto.getContent(), responseDto.getContent()); - assertEquals(requestDto.isRecommended(), responseDto.isRecommended()); + assertEquals(requestDto.isRecommend(), responseDto.isRecommend()); verify(reviewRepository, times(1)).save(mockReview); verify(reviewImageRepository, times(1)).saveAll(reviewImages); verify(s3BucketService, times(1)).upload(requestDto.getS3Images(), "reviews/1"); } -// @Test -// @DisplayName("리뷰 수정 성공") -// void updateReview_Success() throws IOException { -// // Given -// ReviewUpdateRequestDto requestDto = createMockUpdateRequest(); -// Review existingReview = createSavedReview(mockReview); -// List oldReviewImages = List.of( -// ReviewImage.builder().imageUrl("old_url1").build(), -// ReviewImage.builder().imageUrl("old_url2").build() -// ); -// -// when(reviewRepository.findById(1L)).thenReturn(Optional.of(existingReview)); -// when(reviewImageRepository.findByReview(existingReview)).thenReturn(oldReviewImages); -// -// // When -// reviewService.updateReview(1L, 1L, requestDto); -// -// // Then -// verify(reviewImageRepository).deleteAll(oldReviewImages); -// verify(s3BucketService).upload(requestDto.getS3Images(), "reviews/1"); -// } + @Test + @DisplayName("리뷰 수정 성공") + void updateReview_Success() throws IOException { + // Given + ReviewUpdateRequestDto requestDto = createMockUpdateRequest(); + Review existingReview = createSavedReview(mockReview); + List oldReviewImages = List.of( + ReviewImage.builder().imageUrl("old_url1").build(), + ReviewImage.builder().imageUrl("old_url2").build() + ); + + when(reviewRepository.findById(1L)).thenReturn(Optional.of(existingReview)); + when(reviewImageRepository.findByReview(existingReview)).thenReturn(oldReviewImages); + + // When + reviewService.updateReview(1L, 1L, requestDto); + + // Then + verify(reviewImageRepository).deleteAll(oldReviewImages); + verify(s3BucketService).upload(requestDto.getS3Images(), "reviews/1"); + } // Helper Methods private void mockCampAndReservationFindById() { @@ -174,7 +174,7 @@ private void mockCampAndReservationFindById() { private ReviewCreateRequestDto createMockReviewRequest() { return ReviewCreateRequestDto.builder() .content("Great camping experience!") - .recommended(true) + .isRecommend(true) .s3Images(List.of( new MockMultipartFile("image1", "image1.jpg", "image/jpeg", "dummy image content".getBytes()), new MockMultipartFile("image2", "image2.jpg", "image/jpeg", "dummy image content".getBytes()) @@ -210,42 +210,42 @@ private List createMockReviewImages(Review review, List url ); } -// @Test -// @DisplayName("캠핑장의 리뷰 조회 성공") -// void getReviewsByCampId_Success() { -// // Given -// Long campId = 1L; // 캠프 ID -// Camp mockCamp = Camp.builder().id(campId).campName("Mock Camp").build(); -// -// List mockReviews = List.of( -// Review.builder().id(1L).content("Great camping experience!").camp(mockCamp).isRecommend(true).build(), -// Review.builder().id(2L).content("Could be better.").camp(mockCamp).isRecommend(false).build() -// ); -// -// List expectedResponseDtos = List.of( -// ReviewResponseDto.builder().reviewId(1L).content("Great camping experience!").isRecommend(true).build(), -// ReviewResponseDto.builder().reviewId(2L).content("Could be better.").isRecommend(false).build() -// ); -// -// // Mock 설정 -// when(campRepository.findById(campId)).thenReturn(Optional.of(mockCamp)); -// when(reviewRepository.findByCampId(campId)).thenReturn(mockReviews); -// when(reviewMapper.toResponseDtoList(mockReviews)).thenReturn(expectedResponseDtos); -// -// // When -// List responseDtos = reviewService.getReviewsByCampId(campId); -// -// // Then -// assertNotNull(responseDtos); -// assertEquals(2, responseDtos.size()); -// assertEquals("Great camping experience!", responseDtos.get(0).getContent()); -// assertEquals("Could be better.", responseDtos.get(1).getContent()); -// -// // Mock 호출 검증 -// verify(campRepository, times(1)).findById(campId); -// verify(reviewRepository, times(1)).findByCampId(campId); -// verify(reviewMapper, times(1)).toResponseDtoList(mockReviews); -// } + @Test + @DisplayName("캠핑장의 리뷰 조회 성공") + void getReviewsByCampId_Success() { + // Given + Long campId = 1L; // 캠프 ID + Camp mockCamp = Camp.builder().id(campId).campName("Mock Camp").build(); + + List mockReviews = List.of( + Review.builder().id(1L).content("Great camping experience!").camp(mockCamp).isRecommend(true).build(), + Review.builder().id(2L).content("Could be better.").camp(mockCamp).isRecommend(false).build() + ); + + List expectedResponseDtos = List.of( + ReviewResponseDto.builder().reviewId(1L).content("Great camping experience!").isRecommend(true).build(), + ReviewResponseDto.builder().reviewId(2L).content("Could be better.").isRecommend(false).build() + ); + + // Mock 설정 + when(campRepository.findById(campId)).thenReturn(Optional.of(mockCamp)); + when(reviewRepository.findByCampId(campId)).thenReturn(mockReviews); + when(reviewMapper.toResponseDtoList(mockReviews)).thenReturn(expectedResponseDtos); + + // When + List responseDtos = reviewService.getReviewsByCampId(campId); + + // Then + assertNotNull(responseDtos); + assertEquals(2, responseDtos.size()); + assertEquals("Great camping experience!", responseDtos.get(0).getContent()); + assertEquals("Could be better.", responseDtos.get(1).getContent()); + + // Mock 호출 검증 + verify(campRepository, times(1)).findById(campId); + verify(reviewRepository, times(1)).findByCampId(campId); + verify(reviewMapper, times(1)).toResponseDtoList(mockReviews); + } // @Test @@ -280,117 +280,117 @@ private List createMockReviewImages(Review review, List url // } -// @Test -// @DisplayName("리뷰 삭제 성공") -// void deleteReview_Success() { -// // Given -// Long reviewId = 1L; -// Review existingReview = createSavedReview(mockReview); -// List reviewImages = List.of( -// ReviewImage.builder().imageUrl("url1").build(), -// ReviewImage.builder().imageUrl("url2").build() -// ); -// -// when(reviewRepository.findById(reviewId)).thenReturn(Optional.of(existingReview)); -// when(reviewImageRepository.findByReview(existingReview)).thenReturn(reviewImages); -// -// // When -// reviewService.deleteReview(reviewId); -// -// // Then -// verify(reviewImageRepository, times(1)).findByReview(existingReview); -// verify(reviewImageRepository, times(1)).deleteAll(reviewImages); -// verify(s3BucketService, times(1)).remove(reviewImages.get(0).getImageUrl()); -// verify(s3BucketService, times(1)).remove(reviewImages.get(1).getImageUrl()); -// verify(reviewRepository, times(1)).delete(existingReview); -// } -// -// @Test -// @DisplayName("리뷰 추천 상태 변경 - 권한 있는 유저일 때 성공적으로 추천 상태 변경") -// void toggleRecommend_ShouldToggleRecommendStatus_WhenUserIsAuthorized() { -// // Given -// Long reviewId = 1L; -// Long userId = 2L; -// -// // Mocking Review 객체 생성 -// Review review = Review.builder() -// .id(reviewId) -// .isRecommend(false) -// .reservation(Reservation.builder() -// .user(User.builder().id(userId).build()) -// .build()) -// .build(); -// -// // 상태가 변경된 Review 객체 -// Review updatedReview = Review.builder() -// .id(reviewId) -// .isRecommend(true) -// .reservation(review.getReservation()) -// .build(); -// -// // Mocking -// given(reviewRepository.findById(reviewId)).willReturn(Optional.of(review)); -// given(reviewMapper.toUpdatedReview(review)).willReturn(updatedReview); -// given(reviewRepository.save(updatedReview)).willReturn(updatedReview); -// -// // When -// boolean isRecommended = reviewService.toggleRecommend(reviewId, userId); -// -// // Then -// assertThat(isRecommended).isTrue(); -// verify(reviewRepository).findById(reviewId); -// verify(reviewMapper).toUpdatedReview(review); -// verify(reviewRepository).save(updatedReview); -// } -// -// @Test -// @DisplayName("리뷰 추천 상태 변경 - 리뷰를 찾을 수 없을 때 예외 발생") -// void toggleRecommend_ShouldThrowException_WhenReviewNotFound() { -// // Given -// Long reviewId = 1L; -// Long userId = 2L; -// -// given(reviewRepository.findById(reviewId)).willReturn(Optional.empty()); -// -// // When & Then -// assertThatThrownBy(() -> reviewService.toggleRecommend(reviewId, userId)) -// .isInstanceOf(GlobalException.class) -// .satisfies(exception -> { -// GlobalException globalException = (GlobalException) exception; -// assertThat(globalException.getErrorCode()).isEqualTo(ErrorCode.REVIEW_NOT_FOUND_BY_ID); -// }); -// -// verify(reviewRepository).findById(reviewId); -// verifyNoMoreInteractions(reviewMapper, reviewRepository); -// } -// -// @Test -// @DisplayName("리뷰 추천 상태 변경 - 권한 없는 유저일 때 예외 발생") -// void toggleRecommend_ShouldThrowException_WhenUserIsNotAuthorized() { -// // Given -// Long reviewId = 1L; -// Long userId = 2L; -// -// // Mocking Review 객체 생성 (userId 불일치) -// Review review = Review.builder() -// .id(reviewId) -// .isRecommend(false) -// .reservation(Reservation.builder() -// .user(User.builder().id(3L).build()) // 다른 User ID -// .build()) -// .build(); -// -// given(reviewRepository.findById(reviewId)).willReturn(Optional.of(review)); -// -// // When & Then -// assertThatThrownBy(() -> reviewService.toggleRecommend(reviewId, userId)) -// .isInstanceOf(GlobalException.class) -// .satisfies(exception -> { -// GlobalException globalException = (GlobalException) exception; -// assertThat(globalException.getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND_BY_ID); -// }); -// -// verify(reviewRepository).findById(reviewId); -// verifyNoMoreInteractions(reviewMapper, reviewRepository); -// } + @Test + @DisplayName("리뷰 삭제 성공") + void deleteReview_Success() { + // Given + Long reviewId = 1L; + Review existingReview = createSavedReview(mockReview); + List reviewImages = List.of( + ReviewImage.builder().imageUrl("url1").build(), + ReviewImage.builder().imageUrl("url2").build() + ); + + when(reviewRepository.findById(reviewId)).thenReturn(Optional.of(existingReview)); + when(reviewImageRepository.findByReview(existingReview)).thenReturn(reviewImages); + + // When + reviewService.deleteReview(reviewId); + + // Then + verify(reviewImageRepository, times(1)).findByReview(existingReview); + verify(reviewImageRepository, times(1)).deleteAll(reviewImages); + verify(s3BucketService, times(1)).remove(reviewImages.get(0).getImageUrl()); + verify(s3BucketService, times(1)).remove(reviewImages.get(1).getImageUrl()); + verify(reviewRepository, times(1)).delete(existingReview); + } + + @Test + @DisplayName("리뷰 추천 상태 변경 - 권한 있는 유저일 때 성공적으로 추천 상태 변경") + void toggleRecommend_ShouldToggleRecommendStatus_WhenUserIsAuthorized() { + // Given + Long reviewId = 1L; + Long userId = 2L; + + // Mocking Review 객체 생성 + Review review = Review.builder() + .id(reviewId) + .isRecommend(false) + .reservation(Reservation.builder() + .user(User.builder().id(userId).build()) + .build()) + .build(); + + // 상태가 변경된 Review 객체 + Review updatedReview = Review.builder() + .id(reviewId) + .isRecommend(true) + .reservation(review.getReservation()) + .build(); + + // Mocking + given(reviewRepository.findById(reviewId)).willReturn(Optional.of(review)); + given(reviewMapper.toUpdatedReview(review)).willReturn(updatedReview); + given(reviewRepository.save(updatedReview)).willReturn(updatedReview); + + // When + boolean isRecommended = reviewService.toggleRecommend(reviewId, userId); + + // Then + assertThat(isRecommended).isTrue(); + verify(reviewRepository).findById(reviewId); + verify(reviewMapper).toUpdatedReview(review); + verify(reviewRepository).save(updatedReview); + } + + @Test + @DisplayName("리뷰 추천 상태 변경 - 리뷰를 찾을 수 없을 때 예외 발생") + void toggleRecommend_ShouldThrowException_WhenReviewNotFound() { + // Given + Long reviewId = 1L; + Long userId = 2L; + + given(reviewRepository.findById(reviewId)).willReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> reviewService.toggleRecommend(reviewId, userId)) + .isInstanceOf(GlobalException.class) + .satisfies(exception -> { + GlobalException globalException = (GlobalException) exception; + assertThat(globalException.getErrorCode()).isEqualTo(ErrorCode.REVIEW_NOT_FOUND_BY_ID); + }); + + verify(reviewRepository).findById(reviewId); + verifyNoMoreInteractions(reviewMapper, reviewRepository); + } + + @Test + @DisplayName("리뷰 추천 상태 변경 - 권한 없는 유저일 때 예외 발생") + void toggleRecommend_ShouldThrowException_WhenUserIsNotAuthorized() { + // Given + Long reviewId = 1L; + Long userId = 2L; + + // Mocking Review 객체 생성 (userId 불일치) + Review review = Review.builder() + .id(reviewId) + .isRecommend(false) + .reservation(Reservation.builder() + .user(User.builder().id(3L).build()) // 다른 User ID + .build()) + .build(); + + given(reviewRepository.findById(reviewId)).willReturn(Optional.of(review)); + + // When & Then + assertThatThrownBy(() -> reviewService.toggleRecommend(reviewId, userId)) + .isInstanceOf(GlobalException.class) + .satisfies(exception -> { + GlobalException globalException = (GlobalException) exception; + assertThat(globalException.getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND_BY_ID); + }); + + verify(reviewRepository).findById(reviewId); + verifyNoMoreInteractions(reviewMapper, reviewRepository); + } } \ No newline at end of file