From 52cb909c6a9d89975ef2ffbd9a5b80e254cb9d32 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Tue, 2 Dec 2025 18:38:56 -0300 Subject: [PATCH] feat: add /books/search endpoint with description filtering - Add GET /books/search endpoint with case-insensitive description search - Implement custom JPQL query with H2 CLOB handling in BooksRepository - Add searchByDescription method to BooksService (no caching for dynamic results) - Add request validation with @NotBlank annotation - Add comprehensive OpenAPI documentation with detailed response descriptions - Add 10 new unit tests across all layers (4 controller, 2 service, 4 repository) - Add case-insensitivity test to verify LOWER() functions work correctly - Expand BooksDataInitializer with 10 additional free programming books - Update BookFakes test data to use unique ISBNs and avoid conflicts Closes #128 --- .java-version | 1 + .../boot/controllers/BooksController.java | 22 ++- .../boot/models/BooksDataInitializer.java | 165 ++++++++++++++++++ .../boot/repositories/BooksRepository.java | 13 ++ .../spring/boot/services/BooksService.java | 13 ++ .../samples/spring/boot/test/BookFakes.java | 22 ++- .../controllers/BooksControllerTests.java | 92 ++++++++++ .../repositories/BooksRepositoryTests.java | 39 +++++ .../boot/test/services/BooksServiceTests.java | 44 +++++ 9 files changed, 397 insertions(+), 14 deletions(-) create mode 100644 .java-version diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..aabe6ec --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java index e7a9da5..356c650 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/BooksController.java @@ -5,8 +5,6 @@ import java.net.URI; import java.util.List; -import jakarta.validation.Valid; - import org.hibernate.validator.constraints.ISBN; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -16,9 +14,12 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO; +import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.info.Contact; @@ -29,9 +30,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; - -import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO; -import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; @RestController @Tag(name = "Books") @@ -100,6 +100,18 @@ public ResponseEntity> getAll() { return ResponseEntity.status(HttpStatus.OK).body(books); } + @GetMapping("/books/search") + @Operation(summary = "Searches books by description keyword") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK - Returns matching books (or empty array if none found)", content = @Content(mediaType = "application/json", schema = @Schema(implementation = BookDTO[].class))), + @ApiResponse(responseCode = "400", description = "Bad Request - Missing or blank description parameter", content = @Content) + }) + public ResponseEntity> searchByDescription( + @RequestParam @NotBlank(message = "Description parameter must not be blank") String description) { + List books = booksService.searchByDescription(description); + return ResponseEntity.status(HttpStatus.OK).body(books); + } + /* * ------------------------------------------------------------------------- * HTTP PUT diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/BooksDataInitializer.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/BooksDataInitializer.java index e029df8..312c239 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/BooksDataInitializer.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/BooksDataInitializer.java @@ -116,6 +116,171 @@ public static List seed() { definitions and measures of productivity."""); book9781484242216.setWebsite("https://link.springer.com/book/10.1007/978-1-4842-4221-6"); books.add(book9781484242216); + Book book9781642002232 = new Book(); + book9781642002232.setIsbn("9781642002232"); + book9781642002232.setTitle("Database Design Succinctly"); + book9781642002232.setAuthor("Joseph D. Booth"); + book9781642002232.setPublisher("Syncfusion"); + book9781642002232.setPublished(LocalDate.of(2022, 5, 25)); + book9781642002232.setPages(87); + book9781642002232.setDescription( + """ + The way a user might perceive and use data and the optimal way a computer \ + system might store it are often very different. In this Database Design \ + Succinctly®, learn how to model the user's information into data in a computer \ + database system in such a way as to allow the system to produce useful results \ + for the end user. Joseph D. Booth will cover how to design a database system to \ + allow businesses to get better reporting and control over their information, as \ + well as how to improve their data to make sure it is as accurate as possible."""); + book9781642002232.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/database-design-succinctly"); + books.add(book9781642002232); + Book book9781642001174 = new Book(); + book9781642001174.setIsbn("9781642001174"); + book9781642001174.setTitle("SOLID Principles Succinctly"); + book9781642001174.setAuthor("Gaurav Kumar Arora"); + book9781642001174.setPublisher("Syncfusion"); + book9781642001174.setPublished(LocalDate.of(2016, 10, 31)); + book9781642001174.setPages(78); + book9781642001174.setDescription( + """ + There is always room for improving one's coding ability, and SOLID design \ + principles offer one way to see marked improvements in final output. With SOLID \ + Principles Succinctly®, author Gaurav Kumar Arora will instruct you in how to \ + use SOLID principles to take your programming skills to the next level."""); + book9781642001174.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/solidprinciplessuccinctly"); + books.add(book9781642001174); + Book book9781642001440 = new Book(); + book9781642001440.setIsbn("9781642001440"); + book9781642001440.setTitle("Java Succinctly Part 1"); + book9781642001440.setAuthor("Christopher Rose"); + book9781642001440.setPublisher("Syncfusion"); + book9781642001440.setPublished(LocalDate.of(2017, 8, 29)); + book9781642001440.setPages(125); + book9781642001440.setDescription( + """ + Java is a high-level, cross-platform, object-oriented programming language that \ + allows applications to be written once and run on a multitude of different \ + devices. Java applications are ubiquitous, and the language is consistently \ + ranked as one of the most popular and dominant in the world. Christopher Rose's \ + Java Succinctly® Part 1 describes the foundations of Java—from printing a line \ + of text to the console, to inheritance hierarchies in object-oriented \ + programming. The e-book covers practical aspects of programming, such as \ + debugging and using an IDE, as well as the core mechanics of the language."""); + book9781642001440.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/java-succinctly-part-1"); + books.add(book9781642001440); + Book book9781642001457 = new Book(); + book9781642001457.setIsbn("9781642001457"); + book9781642001457.setTitle("Java Succinctly Part 2"); + book9781642001457.setAuthor("Christopher Rose"); + book9781642001457.setPublisher("Syncfusion"); + book9781642001457.setPublished(LocalDate.of(2017, 9, 5)); + book9781642001457.setPages(134); + book9781642001457.setDescription( + """ + In this second e-book on Java, Christopher Rose takes readers through some of \ + the more advanced features of the language. Java Succinctly® Part 2 explores \ + powerful and practical features of Java, such as multithreading, building GUI \ + applications, and 2-D graphics and game programming. Then learn techniques for \ + using these mechanisms in coherent projects by building a calculator app and a \ + simple game with the author."""); + book9781642001457.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/java-succinctly-part-2"); + books.add(book9781642001457); + Book book9781642001495 = new Book(); + book9781642001495.setIsbn("9781642001495"); + book9781642001495.setTitle("Scala Succinctly"); + book9781642001495.setAuthor("Chris Rose"); + book9781642001495.setPublisher("Syncfusion"); + book9781642001495.setPublished(LocalDate.of(2017, 10, 16)); + book9781642001495.setPages(110); + book9781642001495.setDescription( + """ + Learning a new programming language can be a daunting task, but Scala \ + Succinctly® makes it a simple matter. Author Chris Rose guides readers through \ + the basics of Scala, from installation to syntax shorthand, so that they can \ + get up and running quickly."""); + book9781642001495.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/scala-succinctly"); + books.add(book9781642001495); + Book book9781642001242 = new Book(); + book9781642001242.setIsbn("9781642001242"); + book9781642001242.setTitle("SQL Queries Succinctly"); + book9781642001242.setAuthor("Nick Harrison"); + book9781642001242.setPublisher("Syncfusion"); + book9781642001242.setPublished(LocalDate.of(2017, 2, 4)); + book9781642001242.setPages(102); + book9781642001242.setDescription( + """ + SQL is the language of data, and therefore the intermediary language for those \ + who straddle the line between technology and business. Every business \ + application needs a database and SQL is the key to working with these databases. \ + Nick Harrison's SQL Queries Succinctly® will show you how to craft queries in \ + SQL, from basic CRUD statements and slicing and dicing the data, to applying \ + filters and using aggregate functions to summarize the data. You will look at \ + solving common problems, navigating hierarchical data, and exploring the data \ + dictionary."""); + book9781642001242.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/sql-queries-succinctly"); + books.add(book9781642001242); + Book book9781642001563 = new Book(); + book9781642001563.setIsbn("9781642001563"); + book9781642001563.setTitle("Docker Succinctly"); + book9781642001563.setAuthor("Elton Stoneman"); + book9781642001563.setPublisher("Syncfusion"); + book9781642001563.setPublished(LocalDate.of(2018, 1, 16)); + book9781642001563.setPages(98); + book9781642001563.setDescription( + """ + Containers have revolutionized software development, allowing developers to \ + bundle their applications with everything they need, from the operating system \ + up, into a single package. Docker is one of the most popular platforms for \ + containers, allowing them to be hosted on-premises or on the cloud, and to run \ + on Linux, Windows, and Mac machines. With Docker Succinctly® by Elton Stoneman, \ + learn the basics of building Docker images, sharing them on the Docker Hub, \ + orchestrating containers to deliver large applications, and much more."""); + book9781642001563.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/docker-succinctly"); + books.add(book9781642001563); + Book book9781642001792 = new Book(); + book9781642001792.setIsbn("9781642001792"); + book9781642001792.setTitle("Kubernetes Succinctly"); + book9781642001792.setAuthor("Rahul Rai, Tarun Pabbi"); + book9781642001792.setPublisher("Syncfusion"); + book9781642001792.setPublished(LocalDate.of(2019, 3, 1)); + book9781642001792.setPages(121); + book9781642001792.setDescription( + """ + With excellent orchestration and routing capabilities, Kubernetes is an \ + enterprise-grade platform for building microservices applications. Kubernetes is \ + evolving as the de facto container management tool used by organizations and \ + cloud vendors all over the world. Kubernetes Succinctly® by Rahul Rai and Tarun \ + Pabbi is your guide to learning Kubernetes and leveraging its many capabilities \ + for developing, validating, and maintaining your applications."""); + book9781642001792.setWebsite("https://www.syncfusion.com/succinctly-free-ebooks/kubernetes-succinctly"); + books.add(book9781642001792); + Book book9781838820756 = new Book(); + book9781838820756.setIsbn("9781838820756"); + book9781838820756.setTitle("The Kubernetes Workshop"); + book9781838820756 + .setAuthor("Zachary Arnold, Sahil Dua, Wei Huang, Faisal Masood, Mélony Qin, Mohammed Abu Taleb"); + book9781838820756.setPublisher("Packt"); + book9781838820756.setPublished(LocalDate.of(2020, 9, 1)); + book9781838820756.setPages(780); + book9781838820756.setDescription( + """ + Thanks to its extensive support for managing hundreds of containers that run \ + cloud-native applications, Kubernetes is the most popular open source container \ + orchestration platform that makes cluster management easy. This workshop adopts a \ + practical approach to get you acquainted with the Kubernetes environment and its \ + applications. Starting with an introduction to the fundamentals of Kubernetes, \ + you'll install and set up your Kubernetes environment. You'll understand how to \ + write YAML files and deploy your first simple web application container using \ + Pod. You'll then assign human-friendly names to Pods, explore various Kubernetes \ + entities and functions, and discover when to use them. As you work through the \ + chapters, this Kubernetes book will show you how you can make full-scale use of \ + Kubernetes by applying a variety of techniques for designing components and \ + deploying clusters. You'll also get to grips with security policies for limiting \ + access to certain functions inside the cluster. Toward the end of the book, \ + you'll get a rundown of Kubernetes advanced features for building your own \ + controller and upgrading to a Kubernetes cluster without downtime."""); + book9781838820756.setWebsite("https://www.packtpub.com/free-ebook/the-kubernetes-workshop/9781838820756"); + books.add(book9781838820756); return books; } } diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/BooksRepository.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/BooksRepository.java index 5537ba9..cf0f2fd 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/BooksRepository.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/BooksRepository.java @@ -1,10 +1,13 @@ package ar.com.nanotaboada.java.samples.spring.boot.repositories; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ar.com.nanotaboada.java.samples.spring.boot.models.Book; +import java.util.List; import java.util.Optional; @Repository @@ -16,4 +19,14 @@ public interface BooksRepository extends CrudRepository { // Non-default methods in interfaces are not shown in coverage reports // https://www.jacoco.org/jacoco/trunk/doc/faq.html Optional findByIsbn(String isbn); + + /** + * Finds books whose description contains the given keyword (case-insensitive). + * Uses JPQL with CAST to handle CLOB description field. + * + * @param keyword the keyword to search for in the description + * @return a list of books matching the search criteria + */ + @Query("SELECT b FROM Book b WHERE LOWER(CAST(b.description AS string)) LIKE LOWER(CONCAT('%', :keyword, '%'))") + List findByDescriptionContainingIgnoreCase(@Param("keyword") String keyword); } diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java index d286ea6..70bed16 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/BooksService.java @@ -60,6 +60,19 @@ public List retrieveAll() { .toList(); } + /* + * ------------------------------------------------------------------------- + * Search + * ------------------------------------------------------------------------- + */ + + public List searchByDescription(String keyword) { + return booksRepository.findByDescriptionContainingIgnoreCase(keyword) + .stream() + .map(this::mapFrom) + .toList(); + } + /* * ------------------------------------------------------------------------- * Update diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java index a71cfda..f591bce 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java @@ -16,18 +16,22 @@ public static Book createOneValid() { Book book = new Book(); book.setIsbn("9781484200773"); book.setTitle("Pro Git"); - book.setSubtitle("Everything you neeed to know about Git"); - book.setAuthor("Scott Chacon and Ben Straub"); - book.setPublisher("lulu.com; First Edition"); + book.setSubtitle("Everything You Need to Know About Git"); + book.setAuthor("Scott Chacon, Ben Straub"); + book.setPublisher("Apress"); book.setPublished(LocalDate.of(2014, 11, 18)); - book.setPages(458); + book.setPages(456); book.setDescription( """ - Pro Git (Second Edition) is your fully-updated guide to Git and its \ - usage in the modern world. Git has come a long way since it was first developed by \ - Linus Torvalds for Linux kernel development. It has taken the open source world by \ - storm since its inception in 2005, and this book teaches you how to use it like a \ - pro."""); + Pro Git is your fully-updated guide to Git and its usage in the modern world. \ + Git has come a long way since it was first developed by Linus Torvalds for Linux \ + kernel development. It has taken the open source world by storm since its \ + inception in 2005, and this book teaches you how to use it like a pro. Effective \ + and well-implemented version control is a necessity for successful web projects, \ + whether large or small. This book will help you master the fundamentals of Git, \ + including branching and merging, creating and managing repositories, customizing \ + your workflow, and using Git in a team environment. You'll also learn advanced \ + topics such as Git internals, debugging, automation, and customization."""); book.setWebsite("https://git-scm.com/book/en/v2"); return book; } diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java index c4e524a..e4d98d2 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java @@ -340,4 +340,96 @@ void givenDelete_whenPathVariableIsInvalidISBN_thenResponseStatusIsBadRequest() verify(booksServiceMock, never()).delete(anyString()); assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } + + /* + * ------------------------------------------------------------------------- + * HTTP GET /books/search + * ------------------------------------------------------------------------- + */ + + @Test + void givenSearchByDescription_whenRequestParamIsValidAndMatchingBooksExist_thenResponseStatusIsOKAndResultIsBooks() + throws Exception { + // Arrange + List bookDTOs = BookDTOFakes.createManyValid(); + String keyword = "Java"; + Mockito + .when(booksServiceMock.searchByDescription(anyString())) + .thenReturn(bookDTOs); + MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get(PATH + "/search") + .param("description", keyword); + // Act + MockHttpServletResponse response = application + .perform(request) + .andReturn() + .getResponse(); + response.setContentType("application/json;charset=UTF-8"); + String content = response.getContentAsString(); + List result = new ObjectMapper().readValue(content, new TypeReference>() { + }); + // Assert + verify(booksServiceMock, times(1)).searchByDescription(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result).usingRecursiveComparison().isEqualTo(bookDTOs); + } + + @Test + void givenSearchByDescription_whenRequestParamIsValidAndNoMatchingBooks_thenResponseStatusIsOKAndResultIsEmptyList() + throws Exception { + // Arrange + String keyword = "nonexistentkeyword"; + Mockito + .when(booksServiceMock.searchByDescription(anyString())) + .thenReturn(List.of()); + MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get(PATH + "/search") + .param("description", keyword); + // Act + MockHttpServletResponse response = application + .perform(request) + .andReturn() + .getResponse(); + response.setContentType("application/json;charset=UTF-8"); + String content = response.getContentAsString(); + List result = new ObjectMapper().readValue(content, new TypeReference>() { + }); + // Assert + verify(booksServiceMock, times(1)).searchByDescription(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result).isEmpty(); + } + + @Test + void givenSearchByDescription_whenRequestParamIsBlank_thenResponseStatusIsBadRequest() + throws Exception { + // Arrange + MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get(PATH + "/search") + .param("description", ""); + // Act + MockHttpServletResponse response = application + .perform(request) + .andReturn() + .getResponse(); + // Assert + verify(booksServiceMock, never()).searchByDescription(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void givenSearchByDescription_whenRequestParamIsMissing_thenResponseStatusIsBadRequest() + throws Exception { + // Arrange + MockHttpServletRequestBuilder request = MockMvcRequestBuilders + .get(PATH + "/search"); + // Act + MockHttpServletResponse response = application + .perform(request) + .andReturn() + .getResponse(); + // Assert + verify(booksServiceMock, never()).searchByDescription(anyString()); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } } diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java index 47c5ac7..6871504 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -42,4 +43,42 @@ void givenFindByIsbn_whenISBNDoesNotExist_thenShouldReturnEmptyOptional() { // Assert assertThat(actual).isEmpty(); } + + @Test + void givenFindByDescriptionContainingIgnoreCase_whenKeywordMatchesDescription_thenShouldReturnMatchingBooks() { + // Arrange + List books = BookFakes.createManyValid(); + for (Book book : books) { + repository.save(book); + } + // Act + List actual = repository.findByDescriptionContainingIgnoreCase("Java"); + // Assert + assertThat(actual).isNotEmpty(); + assertThat(actual).allMatch(book -> book.getDescription().toLowerCase().contains("java")); + } + + @Test + void givenFindByDescriptionContainingIgnoreCase_whenKeywordDoesNotMatch_thenShouldReturnEmptyList() { + // Arrange + Book book = BookFakes.createOneValid(); + repository.save(book); + // Act + List actual = repository.findByDescriptionContainingIgnoreCase("nonexistentkeyword"); + // Assert + assertThat(actual).isEmpty(); + } + + @Test + void givenFindByDescriptionContainingIgnoreCase_whenKeywordIsDifferentCase_thenShouldStillMatch() { + // Arrange + Book book = BookFakes.createOneValid(); + book.setDescription("This book covers Advanced PRAGMATISM topics"); + repository.save(book); + // Act + List actual = repository.findByDescriptionContainingIgnoreCase("pragmatism"); + // Assert + assertThat(actual).hasSize(1); + assertThat(actual.get(0).getIsbn()).isEqualTo(book.getIsbn()); + } } diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java index 3e62ba7..930e213 100644 --- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java +++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java @@ -218,4 +218,48 @@ void givenDelete_whenRepositoryExistsByIdReturnsTrue_thenRepositoryDeleteBookAnd verify(modelMapperMock, never()).map(bookDTO, Book.class); assertThat(result).isTrue(); } + + /* + * ------------------------------------------------------------------------- + * Search + * ------------------------------------------------------------------------- + */ + + @Test + void givenSearchByDescription_whenRepositoryReturnsMatchingBooks_thenResultIsEqualToBooks() { + // Arrange + List books = BookFakes.createManyValid(); + List bookDTOs = BookDTOFakes.createManyValid(); + String keyword = "Java"; + Mockito + .when(booksRepositoryMock.findByDescriptionContainingIgnoreCase(keyword)) + .thenReturn(books); + for (int index = 0; index < books.size(); index++) { + Mockito + .when(modelMapperMock.map(books.get(index), BookDTO.class)) + .thenReturn(bookDTOs.get(index)); + } + // Act + List result = booksService.searchByDescription(keyword); + // Assert + verify(booksRepositoryMock, times(1)).findByDescriptionContainingIgnoreCase(keyword); + for (Book book : books) { + verify(modelMapperMock, times(1)).map(book, BookDTO.class); + } + assertThat(result).usingRecursiveComparison().isEqualTo(bookDTOs); + } + + @Test + void givenSearchByDescription_whenRepositoryReturnsEmptyList_thenResultIsEmptyList() { + // Arrange + String keyword = "nonexistentkeyword"; + Mockito + .when(booksRepositoryMock.findByDescriptionContainingIgnoreCase(keyword)) + .thenReturn(List.of()); + // Act + List result = booksService.searchByDescription(keyword); + // Assert + verify(booksRepositoryMock, times(1)).findByDescriptionContainingIgnoreCase(keyword); + assertThat(result).isEmpty(); + } }