diff --git a/src/main/java/com/devoops/accommodation/controller/AccommodationController.java b/src/main/java/com/devoops/accommodation/controller/AccommodationController.java index d4463f3..feb60e2 100644 --- a/src/main/java/com/devoops/accommodation/controller/AccommodationController.java +++ b/src/main/java/com/devoops/accommodation/controller/AccommodationController.java @@ -43,12 +43,14 @@ public ResponseEntity> getAll( } @GetMapping("/search") - public ResponseEntity> search( + public ResponseEntity> search( @RequestParam String location, @RequestParam int guests, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - return ResponseEntity.ok(accommodationService.search(location, guests, startDate, endDate)); + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "12") int size) { + return ResponseEntity.ok(accommodationService.search(location, guests, startDate, endDate, page, size)); } @GetMapping("/{id}") diff --git a/src/main/java/com/devoops/accommodation/repository/AccommodationRepository.java b/src/main/java/com/devoops/accommodation/repository/AccommodationRepository.java index 8f5f0fe..8b78295 100644 --- a/src/main/java/com/devoops/accommodation/repository/AccommodationRepository.java +++ b/src/main/java/com/devoops/accommodation/repository/AccommodationRepository.java @@ -1,6 +1,8 @@ package com.devoops.accommodation.repository; import com.devoops.accommodation.entity.Accommodation; +import org.springframework.data.domain.Page; +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; diff --git a/src/main/java/com/devoops/accommodation/service/AccommodationService.java b/src/main/java/com/devoops/accommodation/service/AccommodationService.java index 5f37f92..9233b60 100644 --- a/src/main/java/com/devoops/accommodation/service/AccommodationService.java +++ b/src/main/java/com/devoops/accommodation/service/AccommodationService.java @@ -15,6 +15,7 @@ import com.devoops.accommodation.repository.AvailabilityPeriodRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -115,14 +116,14 @@ public void delete(UUID id, UserContext userContext) { } @Transactional(readOnly = true) - public List search(String location, int guests, LocalDate startDate, LocalDate endDate) { + public Page search(String location, int guests, LocalDate startDate, LocalDate endDate, int page, int size) { if (!endDate.isAfter(startDate)) { throw new IllegalArgumentException("End date must be after start date"); } long nights = ChronoUnit.DAYS.between(startDate, endDate); List candidates = accommodationRepository.searchByLocationAndGuests(location, guests); - List results = new ArrayList<>(); + List allResults = new ArrayList<>(); for (Accommodation accommodation : candidates) { Optional coveringPeriod = availabilityPeriodRepository @@ -139,7 +140,7 @@ public List search(String location, int guests, Loc totalPrice = unitPrice.multiply(BigDecimal.valueOf(nights)); } - results.add(new AccommodationSearchResponse( + allResults.add(new AccommodationSearchResponse( accommodation.getId(), accommodation.getHostId(), accommodation.getName(), @@ -158,7 +159,13 @@ public List search(String location, int guests, Loc } } - return results; + int start = page * size; + int end = Math.min(start + size, allResults.size()); + List pageContent = start >= allResults.size() + ? List.of() + : allResults.subList(start, end); + + return new PageImpl<>(pageContent, PageRequest.of(page, size), allResults.size()); } private Accommodation findAccommodationOrThrow(UUID id) { diff --git a/src/test/java/com/devoops/accommodation/controller/AccommodationSearchControllerTest.java b/src/test/java/com/devoops/accommodation/controller/AccommodationSearchControllerTest.java index 431ad65..f2f985c 100644 --- a/src/test/java/com/devoops/accommodation/controller/AccommodationSearchControllerTest.java +++ b/src/test/java/com/devoops/accommodation/controller/AccommodationSearchControllerTest.java @@ -15,6 +15,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -65,13 +67,14 @@ private AccommodationSearchResponse createSearchResponse() { class SearchEndpoint { @Test - @DisplayName("With valid parameters returns 200 with results") + @DisplayName("With valid parameters returns 200 with paginated results") void search_WithValidParams_Returns200WithResults() throws Exception { var startDate = LocalDate.of(2026, 3, 5); var endDate = LocalDate.of(2026, 3, 10); + var searchResponse = createSearchResponse(); - when(accommodationService.search("Belgrade", 2, startDate, endDate)) - .thenReturn(List.of(createSearchResponse())); + when(accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12)) + .thenReturn(new PageImpl<>(List.of(searchResponse), PageRequest.of(0, 12), 1)); mockMvc.perform(get("/api/accommodation/search") .param("location", "Belgrade") @@ -79,21 +82,24 @@ void search_WithValidParams_Returns200WithResults() throws Exception { .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(ACCOMMODATION_ID.toString())) - .andExpect(jsonPath("$[0].name").value("Test Apartment")) - .andExpect(jsonPath("$[0].totalPrice").value(500.00)) - .andExpect(jsonPath("$[0].unitPrice").value(50.00)) - .andExpect(jsonPath("$[0].numberOfNights").value(5)); + .andExpect(jsonPath("$.content[0].id").value(ACCOMMODATION_ID.toString())) + .andExpect(jsonPath("$.content[0].name").value("Test Apartment")) + .andExpect(jsonPath("$.content[0].totalPrice").value(500.00)) + .andExpect(jsonPath("$.content[0].unitPrice").value(50.00)) + .andExpect(jsonPath("$.content[0].numberOfNights").value(5)) + .andExpect(jsonPath("$.totalElements").value(1)) + .andExpect(jsonPath("$.number").value(0)) + .andExpect(jsonPath("$.last").value(true)); } @Test - @DisplayName("With no results returns 200 with empty list") - void search_WithNoResults_Returns200WithEmptyList() throws Exception { + @DisplayName("With no results returns 200 with empty page") + void search_WithNoResults_Returns200WithEmptyPage() throws Exception { var startDate = LocalDate.of(2026, 3, 5); var endDate = LocalDate.of(2026, 3, 10); - when(accommodationService.search("Nowhere", 2, startDate, endDate)) - .thenReturn(List.of()); + when(accommodationService.search("Nowhere", 2, startDate, endDate, 0, 12)) + .thenReturn(new PageImpl<>(List.of(), PageRequest.of(0, 12), 0)); mockMvc.perform(get("/api/accommodation/search") .param("location", "Nowhere") @@ -101,8 +107,9 @@ void search_WithNoResults_Returns200WithEmptyList() throws Exception { .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$").isArray()) - .andExpect(jsonPath("$").isEmpty()); + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content").isEmpty()) + .andExpect(jsonPath("$.totalElements").value(0)); } @Test @@ -111,8 +118,8 @@ void search_WithoutAuthHeaders_Returns200() throws Exception { var startDate = LocalDate.of(2026, 3, 5); var endDate = LocalDate.of(2026, 3, 10); - when(accommodationService.search("Belgrade", 2, startDate, endDate)) - .thenReturn(List.of()); + when(accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12)) + .thenReturn(new PageImpl<>(List.of(), PageRequest.of(0, 12), 0)); mockMvc.perform(get("/api/accommodation/search") .param("location", "Belgrade") @@ -152,12 +159,12 @@ void search_WithMissingDates_Returns400() throws Exception { } @Test - @DisplayName("With invalid date returns IllegalArgumentException") + @DisplayName("With invalid date returns 400") void search_WithInvalidDates_Returns400() throws Exception { var startDate = LocalDate.of(2026, 3, 10); var endDate = LocalDate.of(2026, 3, 5); - when(accommodationService.search("Belgrade", 2, startDate, endDate)) + when(accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12)) .thenThrow(new IllegalArgumentException("End date must be after start date")); mockMvc.perform(get("/api/accommodation/search") @@ -167,5 +174,26 @@ void search_WithInvalidDates_Returns400() throws Exception { .param("endDate", "2026-03-05")) .andExpect(status().isBadRequest()); } + + @Test + @DisplayName("With custom page and size parameters uses them") + void search_WithCustomPageAndSize_UsesThem() throws Exception { + var startDate = LocalDate.of(2026, 3, 5); + var endDate = LocalDate.of(2026, 3, 10); + + when(accommodationService.search("Belgrade", 2, startDate, endDate, 1, 6)) + .thenReturn(new PageImpl<>(List.of(), PageRequest.of(1, 6), 0)); + + mockMvc.perform(get("/api/accommodation/search") + .param("location", "Belgrade") + .param("guests", "2") + .param("startDate", "2026-03-05") + .param("endDate", "2026-03-10") + .param("page", "1") + .param("size", "6")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.number").value(1)) + .andExpect(jsonPath("$.size").value(6)); + } } } diff --git a/src/test/java/com/devoops/accommodation/integration/AccommodationSearchIntegrationTest.java b/src/test/java/com/devoops/accommodation/integration/AccommodationSearchIntegrationTest.java index 01491dc..3eab1ab 100644 --- a/src/test/java/com/devoops/accommodation/integration/AccommodationSearchIntegrationTest.java +++ b/src/test/java/com/devoops/accommodation/integration/AccommodationSearchIntegrationTest.java @@ -155,18 +155,20 @@ void setup_CreatePerUnitAvailabilityPeriod() throws Exception { @Test @Order(5) - @DisplayName("Search with matching criteria returns results with prices") - void search_WithMatchingCriteria_ReturnsResultsWithPrices() throws Exception { + @DisplayName("Search with matching criteria returns paginated results with prices") + void search_WithMatchingCriteria_ReturnsPaginatedResultsWithPrices() throws Exception { mockMvc.perform(get(SEARCH_PATH) .param("location", "Belgrade") .param("guests", "2") .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(2))) - .andExpect(jsonPath("$[*].totalPrice").exists()) - .andExpect(jsonPath("$[*].unitPrice").exists()) - .andExpect(jsonPath("$[*].numberOfNights").exists()); + .andExpect(jsonPath("$.content", hasSize(2))) + .andExpect(jsonPath("$.content[*].totalPrice").exists()) + .andExpect(jsonPath("$.content[*].unitPrice").exists()) + .andExpect(jsonPath("$.content[*].numberOfNights").exists()) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.last").value(true)); } @Test @@ -180,11 +182,11 @@ void search_PerGuestAccommodation_CalculatesWithGuestMultiplier() throws Excepti .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Belgrade Apartment")) - .andExpect(jsonPath("$[0].unitPrice").value(50.00)) - .andExpect(jsonPath("$[0].totalPrice").value(500.00)) - .andExpect(jsonPath("$[0].numberOfNights").value(5)); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Belgrade Apartment")) + .andExpect(jsonPath("$.content[0].unitPrice").value(50.00)) + .andExpect(jsonPath("$.content[0].totalPrice").value(500.00)) + .andExpect(jsonPath("$.content[0].numberOfNights").value(5)); } @Test @@ -198,50 +200,53 @@ void search_PerUnitAccommodation_CalculatesWithoutGuestMultiplier() throws Excep .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(1))) - .andExpect(jsonPath("$[0].name").value("Belgrade Studio")) - .andExpect(jsonPath("$[0].unitPrice").value(100.00)) - .andExpect(jsonPath("$[0].totalPrice").value(500.00)) - .andExpect(jsonPath("$[0].numberOfNights").value(5)); + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].name").value("Belgrade Studio")) + .andExpect(jsonPath("$.content[0].unitPrice").value(100.00)) + .andExpect(jsonPath("$.content[0].totalPrice").value(500.00)) + .andExpect(jsonPath("$.content[0].numberOfNights").value(5)); } @Test @Order(8) - @DisplayName("Search with non-matching location returns empty list") - void search_WithNonMatchingLocation_ReturnsEmptyList() throws Exception { + @DisplayName("Search with non-matching location returns empty page") + void search_WithNonMatchingLocation_ReturnsEmptyPage() throws Exception { mockMvc.perform(get(SEARCH_PATH) .param("location", "Paris") .param("guests", "2") .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.totalElements").value(0)); } @Test @Order(9) - @DisplayName("Search with too many guests returns empty list") - void search_WithTooManyGuests_ReturnsEmptyList() throws Exception { + @DisplayName("Search with too many guests returns empty page") + void search_WithTooManyGuests_ReturnsEmptyPage() throws Exception { mockMvc.perform(get(SEARCH_PATH) .param("location", "Belgrade") .param("guests", "10") .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.totalElements").value(0)); } @Test @Order(10) - @DisplayName("Search with dates outside availability returns empty list") - void search_WithDatesOutsideAvailability_ReturnsEmptyList() throws Exception { + @DisplayName("Search with dates outside availability returns empty page") + void search_WithDatesOutsideAvailability_ReturnsEmptyPage() throws Exception { mockMvc.perform(get(SEARCH_PATH) .param("location", "Belgrade") .param("guests", "2") .param("startDate", "2026-05-01") .param("endDate", "2026-05-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", hasSize(0))); + .andExpect(jsonPath("$.content", hasSize(0))) + .andExpect(jsonPath("$.totalElements").value(0)); } @Test @@ -279,7 +284,7 @@ void search_WithInvalidDates_Returns400() throws Exception { @Test @Order(14) - @DisplayName("Search returns all accommodation fields") + @DisplayName("Search returns all accommodation fields in paginated response") void search_ReturnsAllAccommodationFields() throws Exception { mockMvc.perform(get(SEARCH_PATH) .param("location", "Belgrade Center") @@ -287,13 +292,32 @@ void search_ReturnsAllAccommodationFields() throws Exception { .param("startDate", "2026-03-05") .param("endDate", "2026-03-10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(accommodationId)) - .andExpect(jsonPath("$[0].hostId").value(HOST_ID.toString())) - .andExpect(jsonPath("$[0].name").value("Belgrade Apartment")) - .andExpect(jsonPath("$[0].address").value("123 Belgrade Center, Serbia")) - .andExpect(jsonPath("$[0].minGuests").value(1)) - .andExpect(jsonPath("$[0].maxGuests").value(4)) - .andExpect(jsonPath("$[0].pricingMode").value("PER_GUEST")) - .andExpect(jsonPath("$[0].approvalMode").value("MANUAL")); + .andExpect(jsonPath("$.content[0].id").value(accommodationId)) + .andExpect(jsonPath("$.content[0].hostId").value(HOST_ID.toString())) + .andExpect(jsonPath("$.content[0].name").value("Belgrade Apartment")) + .andExpect(jsonPath("$.content[0].address").value("123 Belgrade Center, Serbia")) + .andExpect(jsonPath("$.content[0].minGuests").value(1)) + .andExpect(jsonPath("$.content[0].maxGuests").value(4)) + .andExpect(jsonPath("$.content[0].pricingMode").value("PER_GUEST")) + .andExpect(jsonPath("$.content[0].approvalMode").value("MANUAL")); + } + + @Test + @Order(15) + @DisplayName("Search with pagination returns correct page") + void search_WithPagination_ReturnsCorrectPage() throws Exception { + mockMvc.perform(get(SEARCH_PATH) + .param("location", "Belgrade") + .param("guests", "2") + .param("startDate", "2026-03-05") + .param("endDate", "2026-03-10") + .param("page", "0") + .param("size", "1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.totalPages").value(2)) + .andExpect(jsonPath("$.last").value(false)) + .andExpect(jsonPath("$.number").value(0)); } } diff --git a/src/test/java/com/devoops/accommodation/service/AccommodationSearchServiceTest.java b/src/test/java/com/devoops/accommodation/service/AccommodationSearchServiceTest.java index 0a83a09..dcfd137 100644 --- a/src/test/java/com/devoops/accommodation/service/AccommodationSearchServiceTest.java +++ b/src/test/java/com/devoops/accommodation/service/AccommodationSearchServiceTest.java @@ -15,6 +15,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; import java.math.BigDecimal; import java.time.LocalDate; @@ -84,11 +85,12 @@ void search_WithMatchingCriteria_ReturnsResults() { when(availabilityPeriodRepository.findCoveringPeriod(ACCOMMODATION_ID, startDate, endDate)) .thenReturn(Optional.of(period)); - List results = accommodationService.search("Belgrade", 2, startDate, endDate); + Page results = accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12); - assertThat(results).hasSize(1); - assertThat(results.get(0).id()).isEqualTo(ACCOMMODATION_ID); - assertThat(results.get(0).name()).isEqualTo("Test Apartment"); + assertThat(results.getContent()).hasSize(1); + assertThat(results.getContent().get(0).id()).isEqualTo(ACCOMMODATION_ID); + assertThat(results.getContent().get(0).name()).isEqualTo("Test Apartment"); + assertThat(results.getTotalElements()).isEqualTo(1); } @Test @@ -104,12 +106,12 @@ void search_WithPerGuestPricing_CalculatesTotalCorrectly() { when(availabilityPeriodRepository.findCoveringPeriod(ACCOMMODATION_ID, startDate, endDate)) .thenReturn(Optional.of(period)); - List results = accommodationService.search("Belgrade", 2, startDate, endDate); + Page results = accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12); - assertThat(results.get(0).unitPrice()).isEqualByComparingTo(new BigDecimal("50.00")); + assertThat(results.getContent().get(0).unitPrice()).isEqualByComparingTo(new BigDecimal("50.00")); // 50 * 5 nights * 2 guests = 500 - assertThat(results.get(0).totalPrice()).isEqualByComparingTo(new BigDecimal("500.00")); - assertThat(results.get(0).numberOfNights()).isEqualTo(5); + assertThat(results.getContent().get(0).totalPrice()).isEqualByComparingTo(new BigDecimal("500.00")); + assertThat(results.getContent().get(0).numberOfNights()).isEqualTo(5); } @Test @@ -125,12 +127,12 @@ void search_WithPerUnitPricing_CalculatesTotalWithoutGuestMultiplier() { when(availabilityPeriodRepository.findCoveringPeriod(ACCOMMODATION_ID, startDate, endDate)) .thenReturn(Optional.of(period)); - List results = accommodationService.search("Belgrade", 3, startDate, endDate); + Page results = accommodationService.search("Belgrade", 3, startDate, endDate, 0, 12); - assertThat(results.get(0).unitPrice()).isEqualByComparingTo(new BigDecimal("100.00")); + assertThat(results.getContent().get(0).unitPrice()).isEqualByComparingTo(new BigDecimal("100.00")); // 100 * 5 nights = 500 (no guest multiplier) - assertThat(results.get(0).totalPrice()).isEqualByComparingTo(new BigDecimal("500.00")); - assertThat(results.get(0).numberOfNights()).isEqualTo(5); + assertThat(results.getContent().get(0).totalPrice()).isEqualByComparingTo(new BigDecimal("500.00")); + assertThat(results.getContent().get(0).numberOfNights()).isEqualTo(5); } @Test @@ -145,23 +147,25 @@ void search_WithNoCoveringPeriod_ExcludesAccommodation() { when(availabilityPeriodRepository.findCoveringPeriod(ACCOMMODATION_ID, startDate, endDate)) .thenReturn(Optional.empty()); - List results = accommodationService.search("Belgrade", 2, startDate, endDate); + Page results = accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12); - assertThat(results).isEmpty(); + assertThat(results.getContent()).isEmpty(); + assertThat(results.getTotalElements()).isZero(); } @Test - @DisplayName("With no matching location returns empty list") - void search_WithNoMatchingLocation_ReturnsEmptyList() { + @DisplayName("With no matching location returns empty page") + void search_WithNoMatchingLocation_ReturnsEmptyPage() { var startDate = LocalDate.of(2026, 3, 5); var endDate = LocalDate.of(2026, 3, 10); when(accommodationRepository.searchByLocationAndGuests("Nowhere", 2)) .thenReturn(List.of()); - List results = accommodationService.search("Nowhere", 2, startDate, endDate); + Page results = accommodationService.search("Nowhere", 2, startDate, endDate, 0, 12); - assertThat(results).isEmpty(); + assertThat(results.getContent()).isEmpty(); + assertThat(results.getTotalElements()).isZero(); } @Test @@ -170,7 +174,7 @@ void search_WithEndDateBeforeStartDate_ThrowsIllegalArgumentException() { var startDate = LocalDate.of(2026, 3, 10); var endDate = LocalDate.of(2026, 3, 5); - assertThatThrownBy(() -> accommodationService.search("Belgrade", 2, startDate, endDate)) + assertThatThrownBy(() -> accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("End date must be after start date"); } @@ -180,7 +184,7 @@ void search_WithEndDateBeforeStartDate_ThrowsIllegalArgumentException() { void search_WithEqualDates_ThrowsIllegalArgumentException() { var date = LocalDate.of(2026, 3, 10); - assertThatThrownBy(() -> accommodationService.search("Belgrade", 2, date, date)) + assertThatThrownBy(() -> accommodationService.search("Belgrade", 2, date, date, 0, 12)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("End date must be after start date"); } @@ -211,10 +215,11 @@ void search_WithMultipleCandidates_ReturnsOnlyAvailable() { when(availabilityPeriodRepository.findCoveringPeriod(id2, startDate, endDate)) .thenReturn(Optional.empty()); - List results = accommodationService.search("Belgrade", 2, startDate, endDate); + Page results = accommodationService.search("Belgrade", 2, startDate, endDate, 0, 12); - assertThat(results).hasSize(1); - assertThat(results.get(0).id()).isEqualTo(ACCOMMODATION_ID); + assertThat(results.getContent()).hasSize(1); + assertThat(results.getContent().get(0).id()).isEqualTo(ACCOMMODATION_ID); + assertThat(results.getTotalElements()).isEqualTo(1); } @Test @@ -230,11 +235,49 @@ void search_WithSingleNight_CalculatesPriceCorrectly() { when(availabilityPeriodRepository.findCoveringPeriod(ACCOMMODATION_ID, startDate, endDate)) .thenReturn(Optional.of(period)); - List results = accommodationService.search("Belgrade", 1, startDate, endDate); + Page results = accommodationService.search("Belgrade", 1, startDate, endDate, 0, 12); - assertThat(results.get(0).numberOfNights()).isEqualTo(1); + assertThat(results.getContent().get(0).numberOfNights()).isEqualTo(1); // 80 * 1 night * 1 guest = 80 - assertThat(results.get(0).totalPrice()).isEqualByComparingTo(new BigDecimal("80.00")); + assertThat(results.getContent().get(0).totalPrice()).isEqualByComparingTo(new BigDecimal("80.00")); + } + + @Test + @DisplayName("Pagination returns correct page slice") + void search_WithPagination_ReturnsCorrectSlice() { + var accommodation1 = createAccommodation(PricingMode.PER_GUEST); + var id2 = UUID.randomUUID(); + var accommodation2 = Accommodation.builder() + .id(id2).hostId(HOST_ID).name("Second").address("Belgrade") + .minGuests(1).maxGuests(4).pricingMode(PricingMode.PER_GUEST) + .approvalMode(ApprovalMode.MANUAL).build(); + var period1 = createPeriod(new BigDecimal("50.00")); + var period2 = AvailabilityPeriod.builder() + .id(UUID.randomUUID()).accommodationId(id2) + .startDate(LocalDate.of(2026, 3, 1)).endDate(LocalDate.of(2026, 3, 31)) + .pricePerDay(new BigDecimal("60.00")).build(); + var startDate = LocalDate.of(2026, 3, 5); + var endDate = LocalDate.of(2026, 3, 10); + + when(accommodationRepository.searchByLocationAndGuests("Belgrade", 2)) + .thenReturn(List.of(accommodation1, accommodation2)); + when(availabilityPeriodRepository.findCoveringPeriod(ACCOMMODATION_ID, startDate, endDate)) + .thenReturn(Optional.of(period1)); + when(availabilityPeriodRepository.findCoveringPeriod(id2, startDate, endDate)) + .thenReturn(Optional.of(period2)); + + // Page 0, size 1 — should return first result only + Page page0 = accommodationService.search("Belgrade", 2, startDate, endDate, 0, 1); + assertThat(page0.getContent()).hasSize(1); + assertThat(page0.getTotalElements()).isEqualTo(2); + assertThat(page0.getTotalPages()).isEqualTo(2); + assertThat(page0.isLast()).isFalse(); + + // Page 1, size 1 — should return second result only + Page page1 = accommodationService.search("Belgrade", 2, startDate, endDate, 1, 1); + assertThat(page1.getContent()).hasSize(1); + assertThat(page1.getTotalElements()).isEqualTo(2); + assertThat(page1.isLast()).isTrue(); } } }