diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/config/GlobalExceptionHandler.java b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/config/GlobalExceptionHandler.java index 7aa1fd9fc..5495b3d74 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/config/GlobalExceptionHandler.java +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/config/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.example.keysetpagination.config; import com.example.keysetpagination.exception.ResourceNotFoundException; +import jakarta.validation.ConstraintViolationException; import java.net.URI; import java.time.Instant; import java.util.Comparator; @@ -40,6 +41,26 @@ ProblemDetail onException(MethodArgumentNotValidException methodArgumentNotValid return problemDetail; } + @ExceptionHandler(ConstraintViolationException.class) + ProblemDetail onException(ConstraintViolationException constraintViolationException) { + List apiValidationErrors = constraintViolationException.getConstraintViolations().stream() + .map(constraintViolation -> new ApiValidationError( + constraintViolation.getRootBeanClass().getSimpleName(), + constraintViolation.getPropertyPath().toString(), + constraintViolation.getInvalidValue(), + constraintViolation.getMessage())) + .sorted(Comparator.comparing(ApiValidationError::field)) + .toList(); + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( + HttpStatusCode.valueOf(400), constraintViolationException.getMessage()); + problemDetail.setTitle("Constraint Violation"); + problemDetail.setType(URI.create("https://api.boot-data-window-pagination.com/errors/validation")); + problemDetail.setProperty("errorCategory", "Validation"); + problemDetail.setProperty("timestamp", Instant.now()); + problemDetail.setProperty("violations", apiValidationErrors); + return problemDetail; + } + @ExceptionHandler(Exception.class) ProblemDetail onException(Exception exception) { if (exception instanceof ResourceNotFoundException resourceNotFoundException) { diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java index 83b9504c1..21bea4564 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchCriteria.java @@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.util.List; +import java.util.StringJoiner; public class SearchCriteria { @@ -48,4 +49,13 @@ public List getValues() { public void setValues(List values) { this.values = values; } + + @Override + public String toString() { + return new StringJoiner(", ", SearchCriteria.class.getSimpleName() + "[", "]") + .add("queryOperator=" + queryOperator) + .add("field='" + field + "'") + .add("values=" + values) + .toString(); + } } diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchRequest.java b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchRequest.java new file mode 100644 index 000000000..8d774cbce --- /dev/null +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SearchRequest.java @@ -0,0 +1,44 @@ +package com.example.keysetpagination.model.query; + +import java.util.ArrayList; +import java.util.List; + +public class SearchRequest { + + private List searchCriteriaList; + + private List sortRequests; + + public SearchRequest() { + this.searchCriteriaList = new ArrayList<>(); + this.sortRequests = new ArrayList<>(); + } + + public SearchRequest(List searchCriteriaList, List sortRequests) { + this.searchCriteriaList = searchCriteriaList != null ? searchCriteriaList : new ArrayList<>(); + this.sortRequests = sortRequests != null ? sortRequests : new ArrayList<>(); + } + + public List getSearchCriteriaList() { + return searchCriteriaList; + } + + public SearchRequest setSearchCriteriaList(List searchCriteriaList) { + this.searchCriteriaList = searchCriteriaList; + return this; + } + + public List getSortRequests() { + return sortRequests; + } + + public SearchRequest setSortRequests(List sortRequests) { + this.sortRequests = sortRequests; + return this; + } + + @Override + public String toString() { + return "SearchRequest{" + "searchCriteria=" + searchCriteriaList + ", sortRequests=" + sortRequests + '}'; + } +} diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SortRequest.java b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SortRequest.java new file mode 100644 index 000000000..610ff2649 --- /dev/null +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/model/query/SortRequest.java @@ -0,0 +1,42 @@ +package com.example.keysetpagination.model.query; + +import java.util.Objects; + +public class SortRequest { + + private String field; + private String direction; + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getDirection() { + return direction; + } + + public void setDirection(String direction) { + this.direction = direction; + } + + @Override + public String toString() { + return String.format("SortRequest{field='%s', direction='%s'}", field, direction); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SortRequest that)) return false; + return Objects.equals(field, that.field) && Objects.equals(direction, that.direction); + } + + @Override + public int hashCode() { + return Objects.hash(field, direction); + } +} diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/services/AnimalService.java b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/services/AnimalService.java index d127a39c1..03c89b3b1 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/services/AnimalService.java +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/services/AnimalService.java @@ -4,7 +4,7 @@ import com.example.keysetpagination.exception.AnimalNotFoundException; import com.example.keysetpagination.mapper.AnimalMapper; import com.example.keysetpagination.model.query.FindAnimalsQuery; -import com.example.keysetpagination.model.query.SearchCriteria; +import com.example.keysetpagination.model.query.SearchRequest; import com.example.keysetpagination.model.request.AnimalRequest; import com.example.keysetpagination.model.response.AnimalResponse; import com.example.keysetpagination.model.response.PagedResult; @@ -12,6 +12,8 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -21,11 +23,14 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; @Service @Transactional(readOnly = true) public class AnimalService { + private static final Logger log = LoggerFactory.getLogger(AnimalService.class); + private final AnimalRepository animalRepository; private final AnimalMapper animalMapper; private final EntitySpecification animalEntitySpecification; @@ -60,22 +65,36 @@ private Pageable createPageable(FindAnimalsQuery findAnimalsQuery) { return PageRequest.of(pageNo, findAnimalsQuery.pageSize(), sort); } - public Window searchAnimals(List searchCriteriaList, int pageSize, Long scrollId) { + public Window searchAnimals(SearchRequest searchRequest, int pageSize, Long scrollId) { Specification specification = - animalEntitySpecification.specificationBuilder(searchCriteriaList, Animal.class); + animalEntitySpecification.specificationBuilder(searchRequest.getSearchCriteriaList(), Animal.class); // Create initial ScrollPosition or continue from the given scrollId ScrollPosition position = scrollId == null ? ScrollPosition.keyset() : ScrollPosition.of(Collections.singletonMap("id", scrollId), ScrollPosition.Direction.FORWARD); + // Parse and create sort orders + List orders = CollectionUtils.isEmpty(searchRequest.getSortRequests()) + ? Collections.singletonList(new Sort.Order(Sort.Direction.ASC, "id")) + : searchRequest.getSortRequests().stream() + .map(sortRequest -> { + Sort.Direction direction = "desc" + .equalsIgnoreCase(Optional.ofNullable(sortRequest.getDirection()) + .orElse("asc")) + ? Sort.Direction.DESC + : Sort.Direction.ASC; + return new Sort.Order(direction, sortRequest.getField()); + }) + .toList(); + + log.debug( + "Executing search with criteria: {} and sort orders: {}", + searchRequest.getSearchCriteriaList(), + orders); return animalRepository - .findAll( - specification, - PageRequest.of(0, pageSize, Sort.by(Sort.Order.asc("id"))), - position, - Animal.class) + .findAll(specification, PageRequest.of(0, pageSize, Sort.by(orders)), position, Animal.class) .map(animalMapper::toResponse); } diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/web/controllers/AnimalController.java b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/web/controllers/AnimalController.java index 3dc2c5f1a..39b76502b 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/web/controllers/AnimalController.java +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/main/java/com/example/keysetpagination/web/controllers/AnimalController.java @@ -2,7 +2,7 @@ import com.example.keysetpagination.exception.AnimalNotFoundException; import com.example.keysetpagination.model.query.FindAnimalsQuery; -import com.example.keysetpagination.model.query.SearchCriteria; +import com.example.keysetpagination.model.query.SearchRequest; import com.example.keysetpagination.model.request.AnimalRequest; import com.example.keysetpagination.model.response.AnimalResponse; import com.example.keysetpagination.model.response.PagedResult; @@ -10,13 +10,13 @@ import com.example.keysetpagination.utils.AppConstants; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import java.net.URI; -import java.util.List; import org.springframework.data.domain.Window; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; @@ -38,7 +38,7 @@ class AnimalController { private final AnimalService animalService; - public AnimalController(AnimalService animalService) { + AnimalController(AnimalService animalService) { this.animalService = animalService; } @@ -65,15 +65,17 @@ PagedResult getAllAnimals( }) @PostMapping("/search") public Window searchAnimals( - @Parameter(description = "Number of items per page (max 100)") + @Parameter(description = "Number of items per page (max 100)", in = ParameterIn.QUERY) @RequestParam(defaultValue = "10") @Min(1) @Max(100) int pageSize, - @Parameter(description = "Scroll ID for pagination") @RequestParam(required = false) Long scrollId, - @RequestBody @Valid List searchCriteria) { + @Parameter(description = "Scroll ID for pagination", in = ParameterIn.QUERY) @RequestParam(required = false) + Long scrollId, + @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true) @RequestBody @Valid + SearchRequest searchRequest) { - return animalService.searchAnimals(searchCriteria, pageSize, scrollId); + return animalService.searchAnimals(searchRequest, pageSize, scrollId); } @GetMapping("/{id}") diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerIT.java b/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerIT.java index 9d8de0017..0a4cd4906 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerIT.java +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerIT.java @@ -80,15 +80,17 @@ void shouldSearchAnimals() throws Exception { .param("pageSize", "2") .content( """ - [ - { - "queryOperator": "EQ", - "field": "type", - "values": [ - "Bird" + { + "searchCriteriaList": [ + { + "queryOperator": "EQ", + "field": "type", + "values": [ + "Bird" + ] + } ] - } - ] + } """) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -112,15 +114,22 @@ void shouldSearchAnimals() throws Exception { String.valueOf(animalResponses.getLast().getId())) .content( """ - [ - { - "queryOperator": "EQ", - "field": "type", - "values": [ - "Bird" + { + "searchCriteriaList": [ + { + "queryOperator": "EQ", + "field": "type", + "values": [ + "Bird" + ] + } + ], + "sortRequests": [ + { + "field": "type" + } ] - } - ] + } """) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -137,16 +146,19 @@ void shouldReturnResultForNotEqualType() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "NE", - "field": "type", - "values": [ - "Mammal", - "Bird" - ] - } - ] + { + "searchCriteriaList": [ + { + "queryOperator": "NE", + "field": "type", + "values": [ + "Mammal", + "Bird" + ] + } + ], + "sortRequests": [] + } """)) .andExpect(status().isOk()) // Total animals (10) - Mammals (3) - Birds (3) = 4 animals @@ -162,15 +174,17 @@ void shouldReturnEmptyResultForNonExistentType() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "EQ", - "field": "name", - "values": [ - "NonExistent" - ] - } - ] + { + "searchCriteriaList": [ + { + "queryOperator": "EQ", + "field": "name", + "values": [ + "NonExistent" + ] + } + ] + } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content", hasSize(0))) @@ -301,14 +315,18 @@ void shouldReturnResultForEqualOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "EQ", - "field": "name", - "values": ["Lion"] - } - ] - """)) + { + "searchCriteriaList": [ + { + "queryOperator": "EQ", + "field": "name", + "values": [ + "Lion" + ] + } + ] + } + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(1))) .andExpect(jsonPath("$.content[0].name", is("Lion"))) @@ -324,13 +342,17 @@ void shouldReturnResultForNotEqualOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "NE", - "field": "type", - "values": ["Mammal"] - } - ] + { + "searchCriteriaList": [ + { + "queryOperator": "NE", + "field": "type", + "values": [ + "Mammal" + ] + } + ] + } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(7))) // Total 10 animals - 3 mammals @@ -346,14 +368,19 @@ void shouldReturnResultForInOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "IN", - "field": "type", - "values": ["Bird", "Fish"] - } - ] - """)) + { + "searchCriteriaList": [ + { + "queryOperator": "IN", + "field": "type", + "values": [ + "Bird", + "Fish" + ] + } + ] + } + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(4))) // "Parrot", "Penguin", "Shark", "Eagle" .andExpect(jsonPath("$.last", is(true))); @@ -368,13 +395,18 @@ void shouldReturnResultForNotInOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "NOT_IN", - "field": "type", - "values": ["Bird", "Fish"] - } - ] + { + "searchCriteriaList": [ + { + "queryOperator": "NOT_IN", + "field": "type", + "values": [ + "Bird", + "Fish" + ] + } + ] + } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(6))) @@ -390,17 +422,19 @@ void shouldReturnResultForLikeOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "LIKE", - "field": "name", - "values": ["%e%"] - } - ] + { + "searchCriteriaList": [ + { + "queryOperator": "LIKE", + "field": "name", + "values": ["%e%"] + } + ] + } """)) .andExpect(status().isOk()) .andExpect(jsonPath( - "$.content.size()", is(6))) // "Elephant", "Penguin", "Crocodile", ""Eagle"", "Whale", "Snake" + "$.content.size()", is(6))) // "Elephant", "Penguin", "Crocodile", "Eagle", "Whale", "Snake" .andExpect(jsonPath("$.last", is(true))); } @@ -413,14 +447,16 @@ void shouldReturnResultForContainsOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "CONTAINS", - "field": "name", - "values": ["ar"] - } - ] - """)) + { + "searchCriteriaList": [ + { + "queryOperator": "CONTAINS", + "field": "name", + "values": ["ar"] + } + ] + } + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(2))) // "Parrot", "Shark" .andExpect(jsonPath("$.last", is(true))); @@ -435,14 +471,22 @@ void shouldReturnResultForStartsWithOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "STARTS_WITH", - "field": "name", - "values": ["P"] - } - ] - """)) + { + "searchCriteriaList": [ + { + "queryOperator": "STARTS_WITH", + "field": "name", + "values": ["P"] + } + ], + "sortRequests" : [ + { + "field": "name", + "direction" : "desc" + } + ] + } + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(2))) // "Parrot", "Penguin" .andExpect(jsonPath("$.last", is(true))); @@ -457,14 +501,16 @@ void shouldReturnResultForEndsWithOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "ENDS_WITH", - "field": "name", - "values": ["g"] - } - ] - """)) + { + "searchCriteriaList": [ + { + "queryOperator": "ENDS_WITH", + "field": "name", + "values": ["g"] + } + ] + } + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(1))) // "Frog" .andExpect(jsonPath("$.content[0].name", is("Frog"))) @@ -482,14 +528,19 @@ void shouldReturnResultForBetweenOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(String.format( """ - [ - { - "queryOperator": "BETWEEN", - "field": "id", - "values": ["%d", "%d"] - } - ] - """, + { + "searchCriteriaList": [ + { + "queryOperator": "BETWEEN", + "field": "id", + "values": [ + %d, + %d + ] + } + ] + } + """, minId, maxId))) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(5))) // Animals with IDs between minId and maxId @@ -505,18 +556,24 @@ void shouldReturnResultForAndOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "EQ", - "field": "type", - "values": ["Bird"] - }, - { - "queryOperator": "EQ", - "field": "habitat", - "values": ["Rainforest"] - } - ] + { + "searchCriteriaList": [ + { + "queryOperator": "EQ", + "field": "type", + "values": [ + "Bird" + ] + }, + { + "queryOperator": "EQ", + "field": "habitat", + "values": [ + "Rainforest" + ] + } + ] + } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(1))) // "Parrot" @@ -533,14 +590,16 @@ void shouldReturnResultForOrOperator() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content( """ - [ - { - "queryOperator": "OR", - "field": "name", - "values": ["Shark", "Eagle"] - } - ] - """)) + { + "searchCriteriaList": [ + { + "queryOperator": "OR", + "field": "name", + "values": ["Shark", "Eagle"] + } + ] + } + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()", is(2))) // "Shark" and "Eagle" .andExpect(jsonPath("$.last", is(true))); diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerTest.java b/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerTest.java index 30d170685..bf0af90de 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerTest.java +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/test/java/com/example/keysetpagination/web/controllers/AnimalControllerTest.java @@ -19,6 +19,7 @@ import com.example.keysetpagination.entities.Animal; import com.example.keysetpagination.exception.AnimalNotFoundException; import com.example.keysetpagination.model.query.FindAnimalsQuery; +import com.example.keysetpagination.model.query.SearchRequest; import com.example.keysetpagination.model.request.AnimalRequest; import com.example.keysetpagination.model.response.AnimalResponse; import com.example.keysetpagination.model.response.PagedResult; @@ -113,6 +114,52 @@ void shouldFetchAllAnimalsWithCustomPageSize() throws Exception { .andExpect(jsonPath("$.hasPrevious", is(true))); } + @Test + void shouldReturnBadRequestWhenPageSizeIsLessThanMin() throws Exception { + SearchRequest searchRequest = new SearchRequest(); + mockMvc.perform(post("/api/animals/search") + .param("pageSize", "0") // Less than minimum value of 1 + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(searchRequest))) + .andExpect(status().isBadRequest()) + .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(jsonPath("$.type", is("https://api.boot-data-window-pagination.com/errors/validation"))) + .andExpect(jsonPath("$.title", is("Constraint Violation"))) + .andExpect(jsonPath("$.status", is(400))) + .andExpect(jsonPath("$.detail").value("searchAnimals.pageSize: must be greater than or equal to 1")) + .andExpect(jsonPath("$.instance", is("/api/animals/search"))) + .andExpect(jsonPath("$.errorCategory", is("Validation"))) + .andExpect(jsonPath("$.timestamp", notNullValue())); + } + + @Test + void shouldReturnBadRequestWhenPageSizeExceedsMax() throws Exception { + SearchRequest searchRequest = new SearchRequest(); + mockMvc.perform(post("/api/animals/search") + .param("pageSize", "101") // Exceeds maximum value of 100 + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(searchRequest))) + .andExpect(status().isBadRequest()) + .andExpect(header().string("Content-Type", is(MediaType.APPLICATION_PROBLEM_JSON_VALUE))) + .andExpect(jsonPath("$.type", is("https://api.boot-data-window-pagination.com/errors/validation"))) + .andExpect(jsonPath("$.title", is("Constraint Violation"))) + .andExpect(jsonPath("$.status", is(400))) + .andExpect(jsonPath("$.detail").value("searchAnimals.pageSize: must be less than or equal to 100")) + .andExpect(jsonPath("$.instance", is("/api/animals/search"))) + .andExpect(jsonPath("$.errorCategory", is("Validation"))) + .andExpect(jsonPath("$.timestamp", notNullValue())); + } + + @Test + void shouldReturnOkWhenPageSizeIsWithinValidRange() throws Exception { + SearchRequest searchRequest = new SearchRequest(); + mockMvc.perform(post("/api/animals/search") + .param("pageSize", "50") // Within valid range (1-100) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(searchRequest))) + .andExpect(status().isOk()); + } + @Test void shouldFindAnimalById() throws Exception { Long animalId = 1L; diff --git a/jpa/keyset-pagination/boot-data-window-pagination/src/test/resources/logback-test.xml b/jpa/keyset-pagination/boot-data-window-pagination/src/test/resources/logback-test.xml index 92589da77..6a5d8ad37 100644 --- a/jpa/keyset-pagination/boot-data-window-pagination/src/test/resources/logback-test.xml +++ b/jpa/keyset-pagination/boot-data-window-pagination/src/test/resources/logback-test.xml @@ -12,4 +12,5 @@ +