diff --git a/src/main/java/com/github/nramc/dev/journey/api/web/resources/Resources.java b/src/main/java/com/github/nramc/dev/journey/api/web/resources/Resources.java index e7a246eb..7605f834 100644 --- a/src/main/java/com/github/nramc/dev/journey/api/web/resources/Resources.java +++ b/src/main/java/com/github/nramc/dev/journey/api/web/resources/Resources.java @@ -7,4 +7,5 @@ public class Resources { public static final String HOME = ""; public static final String CREATE_JOURNEY = "/rest/journey"; public static final String FIND_JOURNEY = "/rest/journey/{id}"; + public static final String FIND_JOURNEYS = "/rest/journeys"; } diff --git a/src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResource.java b/src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResource.java index 79b72271..dcb10b35 100644 --- a/src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResource.java +++ b/src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResource.java @@ -6,16 +6,21 @@ import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hibernate.validator.constraints.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Optional; import static com.github.nramc.dev.journey.api.web.resources.Resources.FIND_JOURNEY; +import static com.github.nramc.dev.journey.api.web.resources.Resources.FIND_JOURNEYS; @RestController @Slf4j @@ -33,4 +38,21 @@ public ResponseEntity findAndReturnJson(@Valid @NotBlank @P return ResponseEntity.of(findJourneyResponse); } + @GetMapping(value = FIND_JOURNEYS, produces = MediaType.APPLICATION_JSON_VALUE) + public Page findAllAndReturnJson( + @RequestParam(name = "pageIndex", defaultValue = "0") int pageIndex, + @RequestParam(name = "pageSize", defaultValue = "10") int pageSize, + @RequestParam(name = "sort", defaultValue = "createdDate") String sortColumn, + @RequestParam(name = "order", defaultValue = "DESC") Sort.Direction sortOrder) { + + Pageable pageable = PageRequest.of(pageIndex, pageSize, Sort.by(sortOrder, sortColumn)); + + Page entityPage = journeyRepository.findAll(pageable); + Page responsePage = entityPage.map(FindJourneyConverter::convert); + + log.info("Journey exists:[{}] pages:[{}] total:[{}]", + responsePage.hasContent(), responsePage.getTotalPages(), responsePage.getTotalElements()); + return responsePage; + } + } diff --git a/src/test/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResourceTest.java b/src/test/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResourceTest.java index 7679c135..b17bce2d 100644 --- a/src/test/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResourceTest.java +++ b/src/test/java/com/github/nramc/dev/journey/api/web/resources/rest/find/FindJourneyResourceTest.java @@ -6,25 +6,33 @@ import com.github.nramc.dev.journey.api.repository.journey.JourneyRepository; import com.github.nramc.dev.journey.api.web.resources.Resources; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; 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.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.Optional; +import java.util.stream.IntStream; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(controllers = {FindJourneyResource.class}) -@ActiveProfiles({"prod", "test"}) +@Testcontainers +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles({"test"}) +@AutoConfigureMockMvc class FindJourneyResourceTest { private static final String VALID_UUID = "ecc76991-0137-4152-b3b2-efce70a37ed0"; private static final String VALID_JSON_RESPONSE = """ @@ -62,12 +70,17 @@ class FindJourneyResourceTest { .build(); @Autowired private MockMvc mockMvc; - @MockBean + @Container + @ServiceConnection + static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:latest")) + .withExposedPorts(27017); + + @Autowired private JourneyRepository journeyRepository; @Test void findAndReturnJson_whenJourneyExists_ShouldReturnValidJson() throws Exception { - Mockito.when(journeyRepository.findById(VALID_UUID)).thenReturn(Optional.of(VALID_JOURNEY)); + journeyRepository.save(VALID_JOURNEY); mockMvc.perform(MockMvcRequestBuilders.get(Resources.FIND_JOURNEY, VALID_UUID) .accept(MediaType.APPLICATION_JSON) @@ -79,7 +92,7 @@ void findAndReturnJson_whenJourneyExists_ShouldReturnValidJson() throws Exceptio @Test void findAndReturnJson_whenJourneyNotExists_shouldReturnError() throws Exception { - Mockito.when(journeyRepository.findById(VALID_UUID)).thenReturn(Optional.empty()); + //Mockito.when(journeyRepository.findById(VALID_UUID)).thenReturn(Optional.empty()); mockMvc.perform(MockMvcRequestBuilders.get(Resources.FIND_JOURNEY, VALID_UUID) .accept(MediaType.APPLICATION_JSON) @@ -96,4 +109,104 @@ void findAndReturnJson_whenIdNotValid_thenShouldThrowError() throws Exception { .andExpect(status().isBadRequest()); } + + @Test + void findAllAndReturnJson_whenPagingAndSortingFieldGiven_shouldReturnCorrespondingPageWithRequestedSorting_withSecondPageAndAscendingSort() throws Exception { + // setup data + IntStream.range(0, 10).forEach(index -> { + journeyRepository.save(VALID_JOURNEY.toBuilder().id("ID_" + index).createdDate(LocalDate.now().plusDays(index)).build()); + }); + + // Request result with page number 1 (second page) and order by id ascending + mockMvc.perform(MockMvcRequestBuilders.get(Resources.FIND_JOURNEYS) + .accept(MediaType.APPLICATION_JSON) + .param("sort", "id") + .param("order", "ASC") + .param("pageIndex", "1") + .param("pageSize", "5") + ).andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + // assert paging + .andExpect(jsonPath("$.pageable.pageNumber").value("1")) + .andExpect(jsonPath("$.pageable.pageSize").value("5")) + .andExpect(jsonPath("$.totalPages").value("2")) + .andExpect(jsonPath("$.totalElements").value("10")) + // assert sorting order + .andExpect(jsonPath("$.sort.sorted").value("true")) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].id").value("ID_5")) + .andExpect(jsonPath("$.content[1].id").value("ID_6")) + .andExpect(jsonPath("$.content[2].id").value("ID_7")) + .andExpect(jsonPath("$.content[3].id").value("ID_8")) + .andExpect(jsonPath("$.content[4].id").value("ID_9")) + .andExpect(jsonPath("$.content[5]").doesNotExist()); + } + + @Test + void findAllAndReturnJson_whenPagingAndSortingFieldGiven_shouldReturnCorrespondingPageWithRequestedSorting_withSecondPageAndDescendingSort() throws Exception { + // setup data + IntStream.range(0, 10).forEach(index -> { + journeyRepository.save(VALID_JOURNEY.toBuilder().id("ID_" + index).createdDate(LocalDate.now().plusDays(index)).build()); + }); + + // Request result with page number 1 (second page) and order by id ascending + mockMvc.perform(MockMvcRequestBuilders.get(Resources.FIND_JOURNEYS) + .accept(MediaType.APPLICATION_JSON) + .param("sort", "id") + .param("order", "DESC") + .param("pageIndex", "1") + .param("pageSize", "5") + ).andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + // assert paging + .andExpect(jsonPath("$.pageable.pageNumber").value("1")) + .andExpect(jsonPath("$.pageable.pageSize").value("5")) + .andExpect(jsonPath("$.totalPages").value("2")) + .andExpect(jsonPath("$.totalElements").value("10")) + // assert sorting order + .andExpect(jsonPath("$.sort.sorted").value("true")) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].id").value("ID_4")) + .andExpect(jsonPath("$.content[1].id").value("ID_3")) + .andExpect(jsonPath("$.content[2].id").value("ID_2")) + .andExpect(jsonPath("$.content[3].id").value("ID_1")) + .andExpect(jsonPath("$.content[4].id").value("ID_0")) + .andExpect(jsonPath("$.content[5]").doesNotExist()); + } + + @Test + void findAllAndReturnJson_whenPagingAndSortingParamsNotGiven_thenShouldConsiderDefaultValues() throws Exception { + // setup data + IntStream.range(0, 10).forEach(index -> { + journeyRepository.save(VALID_JOURNEY.toBuilder().id("ID_" + index).createdDate(LocalDate.now().plusDays(index)).build()); + }); + + // Request result with page number 1 (second page) and order by id ascending + mockMvc.perform(MockMvcRequestBuilders.get(Resources.FIND_JOURNEYS) + .accept(MediaType.APPLICATION_JSON) + ).andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + // assert paging + .andExpect(jsonPath("$.pageable.pageNumber").value("0")) + .andExpect(jsonPath("$.pageable.pageSize").value("10")) + .andExpect(jsonPath("$.totalPages").value("1")) + .andExpect(jsonPath("$.totalElements").value("10")) + // assert sorting order + .andExpect(jsonPath("$.sort.sorted").value("true")) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].createdDate").value(LocalDate.now().plusDays(9).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[1].createdDate").value(LocalDate.now().plusDays(8).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[2].createdDate").value(LocalDate.now().plusDays(7).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[3].createdDate").value(LocalDate.now().plusDays(6).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[4].createdDate").value(LocalDate.now().plusDays(5).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[5].createdDate").value(LocalDate.now().plusDays(4).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[6].createdDate").value(LocalDate.now().plusDays(3).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[7].createdDate").value(LocalDate.now().plusDays(2).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[8].createdDate").value(LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[9].createdDate").value(LocalDate.now().plusDays(0).format(DateTimeFormatter.ISO_LOCAL_DATE))) + .andExpect(jsonPath("$.content[10]").doesNotExist()); + } } \ No newline at end of file diff --git a/src/test/resources/data/find-all-journey/all-journey-page-format.json b/src/test/resources/data/find-all-journey/all-journey-page-format.json new file mode 100644 index 00000000..c0326d7d --- /dev/null +++ b/src/test/resources/data/find-all-journey/all-journey-page-format.json @@ -0,0 +1,39 @@ +{ + "content": [ + { + "id": "ID_5", "name": "First Flight Experience", "title": "One of the most beautiful experience ever in my life", + "description": "Travelled first time for work deputation to Germany, Munich city", "category": "Travel", + "city": "Munich", "country": "Germany", "tags": ["Travel", "Germany", "Munich"], "thumbnail": "valid image id", + "location": {"type": "Point", "type": "Point", "coordinates": [48.183160038296585, 11.53090747669896]}, + "journeyDate": "2024-03-27", "createdDate": "2024-04-01" + }, { + "id": "ID_6", "name": "First Flight Experience", "title": "One of the most beautiful experience ever in my life", + "description": "Travelled first time for work deputation to Germany, Munich city", "category": "Travel", + "city": "Munich", "country": "Germany", "tags": ["Travel", "Germany", "Munich"], "thumbnail": "valid image id", + "location": {"type": "Point", "type": "Point", "coordinates": [48.183160038296585, 11.53090747669896]}, + "journeyDate": "2024-03-27", "createdDate": "2024-04-02" + }, { + "id": "ID_7", "name": "First Flight Experience", "title": "One of the most beautiful experience ever in my life", + "description": "Travelled first time for work deputation to Germany, Munich city", "category": "Travel", + "city": "Munich", "country": "Germany", "tags": ["Travel", "Germany", "Munich"], "thumbnail": "valid image id", + "location": {"type": "Point", "type": "Point", "coordinates": [48.183160038296585, 11.53090747669896]}, + "journeyDate": "2024-03-27", "createdDate": "2024-04-03" + }, { + "id": "ID_8", "name": "First Flight Experience", "title": "One of the most beautiful experience ever in my life", + "description": "Travelled first time for work deputation to Germany, Munich city", "category": "Travel", + "city": "Munich", "country": "Germany", "tags": ["Travel", "Germany", "Munich"], "thumbnail": "valid image id", + "location": {"type": "Point", "type": "Point", "coordinates": [48.183160038296585, 11.53090747669896]}, + "journeyDate": "2024-03-27", "createdDate": "2024-04-04" + }, { + "id": "ID_9", "name": "First Flight Experience", "title": "One of the most beautiful experience ever in my life", + "description": "Travelled first time for work deputation to Germany, Munich city", "category": "Travel", + "city": "Munich", "country": "Germany", "tags": ["Travel", "Germany", "Munich"], "thumbnail": "valid image id", + "location": {"type": "Point", "type": "Point", "coordinates": [48.183160038296585, 11.53090747669896]}, + "journeyDate": "2024-03-27", "createdDate": "2024-04-05" + } + ], "pageable": { + "pageNumber": 1, "pageSize": 5, "sort": {"unsorted": false, "sorted": true, "empty": false}, "offset": 5, + "paged": true, "unpaged": false +}, "totalPages": 2, "totalElements": 10, "last": true, "numberOfElements": 5, + "sort": {"unsorted": false, "sorted": true, "empty": false}, "size": 5, "first": false, "number": 1, "empty": false +} \ No newline at end of file