diff --git a/src/main/java/com/server/domain/photo/repository/PhotoRepository.java b/src/main/java/com/server/domain/photo/repository/PhotoRepository.java index 4666756..3ad1141 100644 --- a/src/main/java/com/server/domain/photo/repository/PhotoRepository.java +++ b/src/main/java/com/server/domain/photo/repository/PhotoRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface PhotoRepository extends JpaRepository { diff --git a/src/test/java/com/server/domain/route/service/RouteServiceTest.java b/src/test/java/com/server/domain/route/service/RouteServiceTest.java new file mode 100644 index 0000000..edb1ba6 --- /dev/null +++ b/src/test/java/com/server/domain/route/service/RouteServiceTest.java @@ -0,0 +1,525 @@ +package com.server.domain.route.service; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.server.domain.photo.dto.PhotoSummary; +import com.server.domain.photo.entity.Photo; +import com.server.domain.photo.entity.PhotoType; +import com.server.domain.place.entity.Place; +import com.server.domain.route.dto.RouteDto; +import com.server.domain.route.dto.RoutePlaceSummary; +import com.server.domain.route.entity.Route; +import com.server.domain.route.entity.RouteBookmark; +import com.server.domain.route.entity.RoutePlace; +import com.server.domain.route.entity.RouteStatus; +import com.server.domain.route.entity.RouteType; +import com.server.domain.route.repository.RouteBookmarkRepository; +import com.server.domain.route.repository.RoutePlaceRepository; +import com.server.domain.route.repository.RouteRepository; +import com.server.domain.user.entity.User; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +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.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@DisplayName("RouteService 단위 테스트") +class RouteServiceTest { + + @Mock + private RouteRepository routeRepository; + + @Mock + private RouteBookmarkRepository routeBookmarkRepository; + + @Mock + private RoutePlaceRepository routePlaceRepository; + + @Mock + private com.server.domain.photo.repository.PhotoRepository photoRepository; + + @InjectMocks + private RouteService routeService; + + private User user; + private Route route1; + private Route route2; + private Place place1; + private Place place2; + private Place place3; + private Photo photo1; + private Photo photo2; + private RoutePlace routePlace1; + private RoutePlace routePlace2; + private RoutePlace routePlace3; + + @BeforeEach + void setUp() { + // User 생성 + user = new User(); + org.springframework.test.util.ReflectionTestUtils.setField(user, "id", 1L); + org.springframework.test.util.ReflectionTestUtils.setField(user, "nickname", "테스트 유저"); + + // Place 생성 + place1 = Place.builder() + .id(1L) + .name("장소1") + .address("주소1") + .photos(new ArrayList<>()) + .build(); + + place2 = Place.builder() + .id(2L) + .name("장소2") + .address("주소2") + .photos(new ArrayList<>()) + .build(); + + place3 = Place.builder() + .id(3L) + .name("장소3") + .address("주소3") + .photos(new ArrayList<>()) + .build(); + + // Photo 생성 + photo1 = Photo.builder() + .id(1L) + .imgUrl("http://example.com/photo1.jpg") + .place(place1) + .type(PhotoType.PUBLIC) + .build(); + + photo2 = Photo.builder() + .id(2L) + .imgUrl("http://example.com/photo2.jpg") + .place(place2) + .type(PhotoType.PUBLIC) + .build(); + + place1.getPhotos().add(photo1); + place2.getPhotos().add(photo2); + + // Route 생성 + route1 = Route.builder() + .id(1L) + .name("데이트 코스") + .status(RouteStatus.ACTIVE) + .routeType(RouteType.COURSE) + .createdBy(user) + .build(); + + route2 = Route.builder() + .id(2L) + .name("여행 코스") + .status(RouteStatus.ACTIVE) + .routeType(RouteType.COURSE) + .createdBy(user) + .build(); + + // RoutePlace 생성 + routePlace1 = new RoutePlace(route1, place1); + routePlace1.setId(1L); + + routePlace2 = new RoutePlace(route1, place2); + routePlace2.setId(2L); + + routePlace3 = new RoutePlace(route2, place3); + routePlace3.setId(3L); + } + + @Test + @DisplayName("키워드로 루트 검색 - 사용자 정보 없음") + void searchRoutesByKeyword_WithoutUser() { + // given + String keyword = "데이트"; + List routes = List.of(route1); + List routePlaces = List.of(routePlace1, routePlace2); + + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(routes); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + when(photoRepository.findRepresentativePhotosByPlaceIds(List.of(1L, 2L), PhotoType.PUBLIC)) + .thenReturn(List.of(photo1, photo2)); + + // when + List result = routeService.searchRoutesByKeyword(keyword, null); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(1, result.size()), + () -> { + RouteDto routeDto = result.get(0); + assertEquals(1L, routeDto.getId()); + assertEquals("데이트 코스", routeDto.getName()); + assertEquals(2, routeDto.getPlaces().size()); + assertEquals(2, routeDto.getPhotos().size()); + assertFalse(routeDto.isBookmarked()); + } + ); + + verify(routeRepository).findByNameContainingIgnoreCase(keyword); + verify(routePlaceRepository).findByRouteIn(routes); + } + + @Test + @DisplayName("키워드로 루트 검색 - 북마크된 루트") + void searchRoutesByKeyword_WithBookmark() { + // given + String keyword = "코스"; + List routes = List.of(route1, route2); + List routePlaces = List.of(routePlace1, routePlace2, routePlace3); + + RouteBookmark bookmark = new RouteBookmark(1L, route1, user, null); + List bookmarks = List.of(bookmark); + + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(routes); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + when(routeBookmarkRepository.findByUserWithRoute(user)).thenReturn(bookmarks); + when(photoRepository.findRepresentativePhotosByPlaceIds(List.of(1L, 2L, 3L), PhotoType.PUBLIC)) + .thenReturn(List.of(photo1, photo2)); + + // when + List result = routeService.searchRoutesByKeyword(keyword, user); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(2, result.size()), + () -> { + RouteDto routeDto1 = result.get(0); + assertEquals(1L, routeDto1.getId()); + assertTrue(routeDto1.isBookmarked(), "route1은 북마크되어야 함"); + }, + () -> { + RouteDto routeDto2 = result.get(1); + assertEquals(2L, routeDto2.getId()); + assertFalse(routeDto2.isBookmarked(), "route2는 북마크되지 않아야 함"); + } + ); + + verify(routeRepository).findByNameContainingIgnoreCase(keyword); + verify(routePlaceRepository).findByRouteIn(routes); + verify(routeBookmarkRepository).findByUserWithRoute(user); + } + + @Test + @DisplayName("키워드로 루트 검색 - 검색 결과 없음") + void searchRoutesByKeyword_NoResults() { + // given + String keyword = "존재하지않는루트"; + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(List.of()); + + // when + List result = routeService.searchRoutesByKeyword(keyword, user); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertTrue(result.isEmpty()) + ); + + verify(routeRepository).findByNameContainingIgnoreCase(keyword); + } + + @Test + @DisplayName("루트 검색 - 장소 목록이 정확히 매핑됨") + void searchRoutesByKeyword_PlacesMapping() { + // given + String keyword = "데이트"; + List routes = List.of(route1); + List routePlaces = List.of(routePlace1, routePlace2); + + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(routes); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + when(photoRepository.findRepresentativePhotosByPlaceIds(List.of(1L, 2L), PhotoType.PUBLIC)) + .thenReturn(List.of(photo1, photo2)); + + // when + List result = routeService.searchRoutesByKeyword(keyword, null); + + // then + RouteDto routeDto = result.get(0); + List places = routeDto.getPlaces(); + + assertAll( + () -> assertEquals(2, places.size()), + () -> assertEquals(1L, places.get(0).getPlaceId()), + () -> assertEquals("장소1", places.get(0).getName()), + () -> assertEquals(2L, places.get(1).getPlaceId()), + () -> assertEquals("장소2", places.get(1).getName()) + ); + } + + @Test + @DisplayName("루트 검색 - 사진 목록이 정확히 매핑됨") + void searchRoutesByKeyword_PhotosMapping() { + // given + String keyword = "데이트"; + List routes = List.of(route1); + List routePlaces = List.of(routePlace1, routePlace2); + + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(routes); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + when(photoRepository.findRepresentativePhotosByPlaceIds(List.of(1L, 2L), PhotoType.PUBLIC)) + .thenReturn(List.of(photo1, photo2)); + + // when + List result = routeService.searchRoutesByKeyword(keyword, null); + + // then + RouteDto routeDto = result.get(0); + List photos = routeDto.getPhotos(); + + assertAll( + () -> assertEquals(2, photos.size()), + () -> assertEquals(1L, photos.get(0).getPhotoId()), + () -> assertEquals("http://example.com/photo1.jpg", photos.get(0).getImageUrl()), + () -> assertEquals(2L, photos.get(1).getPhotoId()), + () -> assertEquals("http://example.com/photo2.jpg", photos.get(1).getImageUrl()) + ); + } + + @Test + @DisplayName("루트 검색 - 사진이 없는 장소는 제외됨") + void searchRoutesByKeyword_PlacesWithoutPhotosExcluded() { + // given + String keyword = "여행"; + List routes = List.of(route2); + List routePlaces = List.of(routePlace3); // place3은 사진 없음 + + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(routes); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + when(photoRepository.findRepresentativePhotosByPlaceIds(List.of(3L), PhotoType.PUBLIC)) + .thenReturn(List.of()); // 사진 없음 + + // when + List result = routeService.searchRoutesByKeyword(keyword, null); + + // then + RouteDto routeDto = result.get(0); + assertAll( + () -> assertEquals(1, routeDto.getPlaces().size()), + () -> assertTrue(routeDto.getPhotos().isEmpty(), "사진이 없는 장소는 이미지 목록에서 제외됨") + ); + } + + @Test + @DisplayName("루트 검색 - 최대 10개의 이미지만 반환") + void searchRoutesByKeyword_MaximumTenPhotos() { + // given + String keyword = "테스트"; + List routes = List.of(route1); + List routePlaces = new ArrayList<>(); + + // 12개의 장소와 사진 생성 + for (int i = 1; i <= 12; i++) { + Place place = Place.builder() + .id((long) i) + .name("장소" + i) + .photos(new ArrayList<>()) + .build(); + + Photo photo = Photo.builder() + .id((long) i) + .imgUrl("http://example.com/photo" + i + ".jpg") + .place(place) + .type(PhotoType.PUBLIC) + .build(); + + place.getPhotos().add(photo); + + RoutePlace routePlace = new RoutePlace(route1, place); + routePlace.setId((long) i); + routePlaces.add(routePlace); + } + + when(routeRepository.findByNameContainingIgnoreCase(keyword)).thenReturn(routes); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + + // 12개 장소의 대표 사진 mock (실제로는 10개만 반환됨 - 서비스 로직에서 제한) + List mockPhotos = new ArrayList<>(); + for (int i = 1; i <= 12; i++) { + Place place = Place.builder().id((long) i).name("장소" + i).build(); + Photo photo = Photo.builder() + .id((long) i) + .imgUrl("http://example.com/photo" + i + ".jpg") + .place(place) + .type(PhotoType.PUBLIC) + .build(); + mockPhotos.add(photo); + } + when(photoRepository.findRepresentativePhotosByPlaceIds( + List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L), PhotoType.PUBLIC)) + .thenReturn(mockPhotos); + + // when + List result = routeService.searchRoutesByKeyword(keyword, null); + + // then + RouteDto routeDto = result.get(0); + assertAll( + () -> assertEquals(12, routeDto.getPlaces().size(), "모든 장소가 포함되어야 함"), + () -> assertEquals(10, routeDto.getPhotos().size(), "최대 10개의 사진만 포함되어야 함") + ); + } + + @Test + @DisplayName("활성 상태 루트 조회 - 키워드 없음") + void getActiveRoutesByKeyword_NoKeyword() { + // given + List routes = List.of(route1, route2); + when(routeRepository.findRoutesByStatus(RouteStatus.ACTIVE)).thenReturn(routes); + + // when + List result = routeService.getActiveRoutesByKeyword(null); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(2, result.size()) + ); + + verify(routeRepository).findRoutesByStatus(RouteStatus.ACTIVE); + } + + @Test + @DisplayName("활성 상태 루트 조회 - 키워드 있음") + void getActiveRoutesByKeyword_WithKeyword() { + // given + String keyword = "데이트"; + List routes = List.of(route1); + when(routeRepository.findRoutesByStatusAndKeyword(RouteStatus.ACTIVE, keyword)) + .thenReturn(routes); + + // when + List result = routeService.getActiveRoutesByKeyword(keyword); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(1, result.size()), + () -> assertEquals("데이트 코스", result.get(0).getName()) + ); + + verify(routeRepository).findRoutesByStatusAndKeyword(RouteStatus.ACTIVE, keyword); + } + + @Test + @DisplayName("북마크된 루트 조회") + void getBookmarkedRoutes() { + // given + RouteBookmark bookmark1 = new RouteBookmark(1L, route1, user, null); + + RouteBookmark bookmark2 = new RouteBookmark(2L, route2, user, null); + + List bookmarks = List.of(bookmark1, bookmark2); + when(routeBookmarkRepository.findByUserWithRoute(user)).thenReturn(bookmarks); + + // when + List result = routeService.getBookmarkedRoutes(user); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(2, result.size()), + () -> assertEquals(route1.getId(), result.get(0).getId()), + () -> assertEquals(route2.getId(), result.get(1).getId()) + ); + + verify(routeBookmarkRepository).findByUserWithRoute(user); + } + + @Test + @DisplayName("루트의 북마크 수 조회") + void getBookmarkCount() { + // given + long expectedCount = 5L; + when(routeBookmarkRepository.countByRouteAndNotDeleted(route1)).thenReturn(expectedCount); + + // when + long result = routeService.getBookmarkCount(route1); + + // then + assertEquals(expectedCount, result); + verify(routeBookmarkRepository).countByRouteAndNotDeleted(route1); + } + + @Test + @DisplayName("루트에 속한 장소 조회") + void getPlacesInRoute() { + // given + List places = List.of(place1, place2); + when(routePlaceRepository.findPlacesByRoute(route1)).thenReturn(places); + + // when + List result = routeService.getPlacesInRoute(route1); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(2, result.size()), + () -> assertEquals(place1.getId(), result.get(0).getId()), + () -> assertEquals(place2.getId(), result.get(1).getId()) + ); + + verify(routePlaceRepository).findPlacesByRoute(route1); + } + + @Test + @DisplayName("여러 루트에 속한 장소 조회") + void getPlacesInRoutes() { + // given + List routes = List.of(route1, route2); + List routePlaces = List.of(routePlace1, routePlace2, routePlace3); + when(routePlaceRepository.findByRouteIn(routes)).thenReturn(routePlaces); + + // when + List result = routeService.getPlacesInRoutes(routes); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(3, result.size()) + ); + + verify(routePlaceRepository).findByRouteIn(routes); + } + + @Test + @DisplayName("여러 루트 조회 - 빈 리스트") + void getPlacesInRoutes_EmptyList() { + // given & when + List result = routeService.getPlacesInRoutes(List.of()); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertTrue(result.isEmpty()) + ); + } + + @Test + @DisplayName("여러 루트 조회 - null") + void getPlacesInRoutes_Null() { + // given & when + List result = routeService.getPlacesInRoutes(null); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertTrue(result.isEmpty()) + ); + } +} diff --git a/src/test/java/com/server/domain/search/service/SearchFacadeTest.java b/src/test/java/com/server/domain/search/service/SearchFacadeTest.java new file mode 100644 index 0000000..6bd9a27 --- /dev/null +++ b/src/test/java/com/server/domain/search/service/SearchFacadeTest.java @@ -0,0 +1,384 @@ +package com.server.domain.search.service; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.server.domain.map.dto.MapPlaceDto; +import com.server.domain.map.dto.request.KeywordSearchRequest; +import com.server.domain.map.service.MapService; +import com.server.domain.place.dto.PlaceDto; +import com.server.domain.place.service.PlaceService; +import com.server.domain.route.dto.RouteDto; +import com.server.domain.route.service.RouteService; +import com.server.domain.search.dto.SearchHistoryDto; +import com.server.domain.search.dto.SearchResultResponse; +import com.server.domain.search.dto.request.SearchResultRequest; +import com.server.domain.search.entity.SearchPageType; +import com.server.domain.search.entity.SearchTargetType; +import com.server.domain.user.entity.User; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +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.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SearchFacade 단위 테스트") +class SearchFacadeTest { + + @Mock + private SearchService searchService; + + @Mock + private PlaceService placeService; + + @Mock + private RouteService routeService; + + @Mock + private MapService mapService; + + @InjectMocks + private SearchFacade searchFacade; + + private User user; + private PlaceDto placeDto1; + private PlaceDto placeDto2; + private RouteDto routeDto1; + private MapPlaceDto mapPlaceDto1; + + @BeforeEach + void setUp() { + user = new User(); + org.springframework.test.util.ReflectionTestUtils.setField(user, "id", 1L); + org.springframework.test.util.ReflectionTestUtils.setField(user, "nickname", "테스트 유저"); + + placeDto1 = PlaceDto.builder() + .id(1L) + .name("카페 A") + .address("서울시 강남구") + .bookmarked(false) + .score(4.5) + .photos(new ArrayList<>()) + .build(); + + placeDto2 = PlaceDto.builder() + .id(2L) + .name("레스토랑 B") + .address("서울시 서초구") + .bookmarked(false) + .score(4.0) + .photos(new ArrayList<>()) + .build(); + + routeDto1 = RouteDto.builder() + .id(1L) + .name("데이트 코스") + .places(new ArrayList<>()) + .photos(new ArrayList<>()) + .bookmarked(false) + .build(); + + mapPlaceDto1 = MapPlaceDto.builder() + .id("kakao123") + .placeName("카카오 장소") + .addressName("서울시 중구") + .build(); + } + + @Test + @DisplayName("장소 검색 - 일반 페이지") + void search_PlaceTarget_GeneralPage() { + // given + String keyword = "카페"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.PLACE); + request.setPageType(SearchPageType.MAIN); + + List places = List.of(placeDto1, placeDto2); + when(placeService.searchByKeyword(keyword)).thenReturn(places); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertNotNull(result.getSearchPlaces()), + () -> assertEquals(2, result.getSearchPlaces().size()), + () -> assertEquals("카페 A", result.getSearchPlaces().get(0).getName()), + () -> assertEquals("레스토랑 B", result.getSearchPlaces().get(1).getName()) + ); + + verify(searchService).saveSearchHistory(user, keyword); + verify(placeService).searchByKeyword(keyword); + verify(mapService, never()).searchByKeyword(any()); + } + + @Test + @DisplayName("장소 검색 - 일정 생성 페이지 (카카오 API 포함)") + void search_PlaceTarget_DateSchedulePage() { + // given + String keyword = "카페"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.PLACE); + request.setPageType(SearchPageType.DATE_SCHEDULE); + + List internalPlaces = new ArrayList<>(List.of(placeDto1)); + List kakaoPlaces = List.of(mapPlaceDto1); + + when(placeService.searchByKeyword(keyword)).thenReturn(internalPlaces); + when(mapService.searchByKeyword(any(KeywordSearchRequest.class))).thenReturn(kakaoPlaces); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertNotNull(result.getSearchPlaces()), + () -> assertEquals(2, result.getSearchPlaces().size(), + "내부 DB 결과와 카카오 API 결과가 합쳐져야 함"), + () -> assertEquals("카페 A", result.getSearchPlaces().get(0).getName()), + () -> assertEquals("카카오 장소", result.getSearchPlaces().get(1).getName()) + ); + + verify(searchService).saveSearchHistory(user, keyword); + verify(placeService).searchByKeyword(keyword); + verify(mapService).searchByKeyword(any(KeywordSearchRequest.class)); + } + + @Test + @DisplayName("루트 검색") + void search_RouteTarget() { + // given + String keyword = "데이트"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.ROUTE); + request.setPageType(SearchPageType.MAIN); + + List routes = List.of(routeDto1); + when(routeService.searchRoutesByKeyword(keyword, user)).thenReturn(routes); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertNotNull(result.getSearchRoutes()), + () -> assertEquals(1, result.getSearchRoutes().size()), + () -> assertEquals("데이트 코스", result.getSearchRoutes().get(0).getName()) + ); + + verify(searchService).saveSearchHistory(user, keyword); + verify(routeService).searchRoutesByKeyword(keyword, user); + verify(placeService, never()).searchByKeyword(anyString()); + } + + @Test + @DisplayName("장소 검색 - 결과 없음") + void search_PlaceTarget_NoResults() { + // given + String keyword = "존재하지않는장소"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.PLACE); + request.setPageType(SearchPageType.MAIN); + + when(placeService.searchByKeyword(keyword)).thenReturn(List.of()); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertNotNull(result.getSearchPlaces()), + () -> assertTrue(result.getSearchPlaces().isEmpty()) + ); + + verify(searchService).saveSearchHistory(user, keyword); + verify(placeService).searchByKeyword(keyword); + } + + @Test + @DisplayName("루트 검색 - 결과 없음") + void search_RouteTarget_NoResults() { + // given + String keyword = "존재하지않는루트"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.ROUTE); + request.setPageType(SearchPageType.MAIN); + + when(routeService.searchRoutesByKeyword(keyword, user)).thenReturn(List.of()); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertNotNull(result.getSearchRoutes()), + () -> assertTrue(result.getSearchRoutes().isEmpty()) + ); + + verify(searchService).saveSearchHistory(user, keyword); + verify(routeService).searchRoutesByKeyword(keyword, user); + } + + @Test + @DisplayName("최근 검색어 조회") + void getRecentSearches() { + // given + List searchHistory = List.of( + new SearchHistoryDto(1L, "카페", "2024-01-01T00:00:00"), + new SearchHistoryDto(2L, "레스토랑", "2024-01-02T00:00:00") + ); + + when(searchService.getRecentSearchHistory(user)).thenReturn(searchHistory); + + // when + List result = searchFacade.getRecentSearches(user); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(2, result.size()), + () -> assertEquals("카페", result.get(0).getKeyword()), + () -> assertEquals("레스토랑", result.get(1).getKeyword()) + ); + + verify(searchService).getRecentSearchHistory(user); + } + + @Test + @DisplayName("최근 검색어 조회 - 결과 없음") + void getRecentSearches_NoResults() { + // given + when(searchService.getRecentSearchHistory(user)).thenReturn(List.of()); + + // when + List result = searchFacade.getRecentSearches(user); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertTrue(result.isEmpty()) + ); + + verify(searchService).getRecentSearchHistory(user); + } + + @Test + @DisplayName("장소 검색 시 검색 히스토리 저장 확인") + void search_SavesSearchHistory() { + // given + String keyword = "테스트 키워드"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.PLACE); + request.setPageType(SearchPageType.MAIN); + + when(placeService.searchByKeyword(keyword)).thenReturn(List.of()); + + // when + searchFacade.search(user, request); + + // then + verify(searchService).saveSearchHistory(user, keyword); + } + + @Test + @DisplayName("루트 검색 시 검색 히스토리 저장 확인") + void search_RouteTarget_SavesSearchHistory() { + // given + String keyword = "루트 키워드"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.ROUTE); + request.setPageType(SearchPageType.MAIN); + + when(routeService.searchRoutesByKeyword(keyword, user)).thenReturn(List.of()); + + // when + searchFacade.search(user, request); + + // then + verify(searchService).saveSearchHistory(user, keyword); + } + + @Test + @DisplayName("일정 생성 페이지 - 내부 DB만 있을 때") + void search_DateSchedulePage_OnlyInternalDB() { + // given + String keyword = "카페"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.PLACE); + request.setPageType(SearchPageType.DATE_SCHEDULE); + + List internalPlaces = new ArrayList<>(List.of(placeDto1, placeDto2)); + + when(placeService.searchByKeyword(keyword)).thenReturn(internalPlaces); + when(mapService.searchByKeyword(any(KeywordSearchRequest.class))).thenReturn(List.of()); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(2, result.getSearchPlaces().size()), + () -> assertEquals("카페 A", result.getSearchPlaces().get(0).getName()), + () -> assertEquals("레스토랑 B", result.getSearchPlaces().get(1).getName()) + ); + + verify(placeService).searchByKeyword(keyword); + verify(mapService).searchByKeyword(any(KeywordSearchRequest.class)); + } + + @Test + @DisplayName("일정 생성 페이지 - 카카오 API만 있을 때") + void search_DateSchedulePage_OnlyKakaoAPI() { + // given + String keyword = "카페"; + SearchResultRequest request = new SearchResultRequest(); + request.setKeyword(keyword); + request.setTargetType(SearchTargetType.PLACE); + request.setPageType(SearchPageType.DATE_SCHEDULE); + + List kakaoPlaces = List.of(mapPlaceDto1); + + when(placeService.searchByKeyword(keyword)).thenReturn(new ArrayList<>()); + when(mapService.searchByKeyword(any(KeywordSearchRequest.class))).thenReturn(kakaoPlaces); + + // when + SearchResultResponse result = searchFacade.search(user, request); + + // then + assertAll( + () -> assertNotNull(result), + () -> assertEquals(1, result.getSearchPlaces().size()), + () -> assertEquals("카카오 장소", result.getSearchPlaces().get(0).getName()) + ); + + verify(placeService).searchByKeyword(keyword); + verify(mapService).searchByKeyword(any(KeywordSearchRequest.class)); + } +}