searchByDescription(String keyword) {
- return booksRepository.findByDescriptionContainingIgnoreCase(keyword)
- .stream()
- .map(this::mapFrom)
- .toList();
- }
-
- /*
- * -------------------------------------------------------------------------
- * Update
- * -------------------------------------------------------------------------
- */
-
- @CachePut(value = "books", key = "#bookDTO.isbn")
- public boolean update(BookDTO bookDTO) {
- if (booksRepository.existsById(bookDTO.getIsbn())) {
- Book book = mapFrom(bookDTO);
- booksRepository.save(book);
- return true;
- } else {
- return false;
- }
- }
-
- /*
- * -------------------------------------------------------------------------
- * Delete
- * -------------------------------------------------------------------------
- */
-
- @CacheEvict(value = "books", key = "#isbn")
- public boolean delete(String isbn) {
- if (booksRepository.existsById(isbn)) {
- booksRepository.deleteById(isbn);
- return true;
- } else {
- return false;
- }
- }
-
- private BookDTO mapFrom(Book book) {
- return modelMapper.map(book, BookDTO.class);
- }
-
- private Book mapFrom(BookDTO dto) {
- return modelMapper.map(dto, Book.class);
- }
-}
diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java
new file mode 100644
index 0000000..3e3366e
--- /dev/null
+++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java
@@ -0,0 +1,261 @@
+package ar.com.nanotaboada.java.samples.spring.boot.services;
+
+import java.util.List;
+
+import org.modelmapper.ModelMapper;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import ar.com.nanotaboada.java.samples.spring.boot.models.Player;
+import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO;
+import ar.com.nanotaboada.java.samples.spring.boot.repositories.PlayersRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Service layer for managing Player business logic.
+ *
+ * This service acts as an intermediary between the PlayersController and {@link PlayersRepository},
+ * providing CRUD operations, search functionality, and caching.
+ *
+ *
+ * Key Features:
+ *
+ * - Caching: Uses Spring Cache abstraction for improved performance
+ * - DTO Mapping: Converts between {@link Player} entities and {@link PlayerDTO} objects
+ * - Business Logic: Encapsulates domain-specific operations
+ *
+ *
+ * Cache Strategy:
+ *
+ * - @Cacheable: Retrieval operations (read-through cache)
+ * - @CacheEvict(allEntries=true): Mutating operations (create/update/delete) - invalidates entire cache to maintain
+ * consistency
+ *
+ *
+ *
+ * Why invalidate all entries? The {@code retrieveAll()} method caches the full player list under a single key.
+ * Individual cache evictions would leave this list stale. Using {@code allEntries=true} ensures both individual
+ * player caches and the list cache stay synchronized after any data modification.
+ *
+ *
+ * @see PlayersRepository
+ * @see PlayerDTO
+ * @see Player
+ * @see org.modelmapper.ModelMapper
+ * @since 4.0.2025
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PlayersService {
+
+ private final PlayersRepository playersRepository;
+ private final ModelMapper modelMapper;
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Create
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Creates a new player and stores it in the database.
+ *
+ * This method converts the PlayerDTO to a Player entity, persists it, and returns the saved player with its auto-generated
+ * ID. The result is automatically cached using the player's ID as the cache key.
+ *
+ *
+ * Conflict Detection: Checks if a player with the same squad number already exists (optimization).
+ * Squad numbers are unique identifiers (jersey numbers). Database constraint ensures uniqueness even under
+ * concurrent operations. If a race condition occurs between check and save, DataIntegrityViolationException
+ * is caught and null is returned to indicate conflict.
+ *
+ *
+ * @param playerDTO the player data to create (must not be null)
+ * @return the created player with auto-generated ID, or null if squad number already exists
+ * @see org.springframework.cache.annotation.CacheEvict
+ */
+ @Transactional
+ @CacheEvict(value = "players", allEntries = true)
+ public PlayerDTO create(PlayerDTO playerDTO) {
+ log.debug("Creating new player with squad number: {}", playerDTO.getSquadNumber());
+
+ // Check if squad number already exists (optimization to avoid unnecessary DB write)
+ if (playersRepository.findBySquadNumber(playerDTO.getSquadNumber()).isPresent()) {
+ log.warn("Cannot create player - squad number {} already exists", playerDTO.getSquadNumber());
+ return null; // Conflict: squad number already taken
+ }
+
+ try {
+ Player player = mapFrom(playerDTO);
+ Player savedPlayer = playersRepository.save(player);
+ PlayerDTO result = mapFrom(savedPlayer);
+ log.info("Player created successfully - ID: {}, Squad Number: {}", result.getId(), result.getSquadNumber());
+ return result;
+ } catch (DataIntegrityViolationException _) {
+ // Handle race condition: concurrent request created player with same squad number
+ // between our check and save operation
+ log.warn("Cannot create player - squad number {} already exists (race condition)", playerDTO.getSquadNumber());
+ return null;
+ }
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Retrieve
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Retrieves a player by their unique identifier.
+ *
+ * This method uses caching to improve performance. If the player is found in the cache, it will be returned without
+ * hitting the database. Otherwise, it queries the database and caches the result.
+ * Null results (player not found) are not cached to avoid serving stale misses.
+ *
+ *
+ * @param id the unique identifier of the player (must not be null)
+ * @return the player DTO if found, null otherwise
+ * @see org.springframework.cache.annotation.Cacheable
+ */
+ @Cacheable(value = "players", key = "#id", unless = "#result == null")
+ public PlayerDTO retrieveById(Long id) {
+ return playersRepository.findById(id)
+ .map(this::mapFrom)
+ .orElse(null);
+ }
+
+ /**
+ * Retrieves all players from the database.
+ *
+ * This method returns the complete Argentina 2022 FIFA World Cup squad (26 players).
+ * Results are cached to improve performance on subsequent calls.
+ *
+ *
+ * @return a list of all players (empty list if none found)
+ * @see org.springframework.cache.annotation.Cacheable
+ */
+ @Cacheable(value = "players")
+ public List retrieveAll() {
+ return playersRepository.findAll()
+ .stream()
+ .map(this::mapFrom)
+ .toList();
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Search
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Searches for players by league name (case-insensitive, partial match).
+ *
+ * This method performs a wildcard search on the league field, matching any player whose league name contains the search
+ * term (e.g., "Premier" matches "Premier League").
+ *
+ *
+ * @param league the league name to search for (must not be null or blank)
+ * @return a list of matching players (empty list if none found)
+ */
+ public List searchByLeague(String league) {
+ return playersRepository.findByLeagueContainingIgnoreCase(league)
+ .stream()
+ .map(this::mapFrom)
+ .toList();
+ }
+
+ /**
+ * Searches for a player by their squad number.
+ *
+ * This method performs an exact match on the squad number field. Squad numbers are jersey numbers that users recognize
+ * (e.g., Messi is #10).
+ *
+ *
+ * @param squadNumber the squad number to search for (jersey number, typically 1-99)
+ * @return the player DTO if found, null otherwise
+ */
+ public PlayerDTO searchBySquadNumber(Integer squadNumber) {
+ return playersRepository.findBySquadNumber(squadNumber)
+ .map(this::mapFrom)
+ .orElse(null);
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Update
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Updates an existing player's information.
+ *
+ * This method performs a full update (PUT semantics) of the player entity. If the player exists, it updates all fields and
+ * refreshes the cache. If the player doesn't exist, returns false without making changes.
+ *
+ *
+ * @param playerDTO the player data to update (must include a valid ID)
+ * @return true if the player was updated successfully, false if not found
+ * @see org.springframework.cache.annotation.CacheEvict
+ */
+ @Transactional
+ @CacheEvict(value = "players", allEntries = true)
+ public boolean update(PlayerDTO playerDTO) {
+ log.debug("Updating player with ID: {}", playerDTO.getId());
+
+ if (playerDTO.getId() != null && playersRepository.existsById(playerDTO.getId())) {
+ Player player = mapFrom(playerDTO);
+ playersRepository.save(player);
+ log.info("Player updated successfully - ID: {}", playerDTO.getId());
+ return true;
+ } else {
+ log.warn("Cannot update player - ID {} not found", playerDTO.getId());
+ return false;
+ }
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Delete
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Deletes a player by their unique identifier.
+ *
+ * This method removes the player from the database and evicts it from the cache. If the player doesn't exist, returns
+ * false without making changes.
+ *
+ *
+ * @param id the unique identifier of the player to delete (must not be null)
+ * @return true if the player was deleted successfully, false if not found
+ * @see org.springframework.cache.annotation.CacheEvict
+ */
+ @Transactional
+ @CacheEvict(value = "players", allEntries = true)
+ public boolean delete(Long id) {
+ log.debug("Deleting player with ID: {}", id);
+
+ if (playersRepository.existsById(id)) {
+ playersRepository.deleteById(id);
+ log.info("Player deleted successfully - ID: {}", id);
+ return true;
+ } else {
+ log.warn("Cannot delete player - ID {} not found", id);
+ return false;
+ }
+ }
+
+ private PlayerDTO mapFrom(Player player) {
+ return modelMapper.map(player, PlayerDTO.class);
+ }
+
+ private Player mapFrom(PlayerDTO dto) {
+ return modelMapper.map(dto, Player.class);
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f556a99..52a6bdf 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -16,10 +16,11 @@ springdoc.swagger-ui.path=/swagger/index.html
# SQLite Database Configuration
# Uses environment variable STORAGE_PATH if set, otherwise defaults to local path
-# Dates are stored as Unix timestamps (INTEGER) for robustness - no date format config needed
-spring.datasource.url=jdbc:sqlite:${STORAGE_PATH:storage/books-sqlite3.db}
+# Contains players table with dates stored as ISO-8601 TEXT
+spring.datasource.url=jdbc:sqlite:${STORAGE_PATH:storage/players-sqlite3.db}
spring.datasource.driver-class-name=org.sqlite.JDBC
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
spring.jpa.hibernate.ddl-auto=none
+spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true
diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOFakes.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOFakes.java
deleted file mode 100644
index d0d661a..0000000
--- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookDTOFakes.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package ar.com.nanotaboada.java.samples.spring.boot.test;
-
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.List;
-
-import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO;
-
-public final class BookDTOFakes {
-
- private BookDTOFakes() {
- throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
- }
-
- public static BookDTO createOneValid() {
- BookDTO bookDTO = new BookDTO();
- bookDTO.setIsbn("978-1484200773");
- bookDTO.setTitle("Pro Git");
- bookDTO.setSubtitle("Everything you neeed to know about Git");
- bookDTO.setAuthor("Scott Chacon and Ben Straub");
- bookDTO.setPublisher("lulu.com; First Edition");
- bookDTO.setPublished(LocalDate.of(2014, 11, 18));
- bookDTO.setPages(458);
- bookDTO.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.""");
- bookDTO.setWebsite("https://git-scm.com/book/en/v2");
- return bookDTO;
- }
-
- public static BookDTO createOneInvalid() {
- BookDTO bookDTO = new BookDTO();
- bookDTO.setIsbn("978-1234567890"); // Invalid (invalid ISBN)
- bookDTO.setTitle("Title");
- bookDTO.setSubtitle("Sub Title");
- bookDTO.setAuthor("Author");
- bookDTO.setPublisher("Publisher");
- bookDTO.setPublished(LocalDate.now()); // Invalid (must be a past date)
- bookDTO.setPages(123);
- bookDTO.setDescription("Description");
- bookDTO.setWebsite("https://domain.com/");
- return bookDTO;
- }
-
- public static List createManyValid() {
- ArrayList bookDTOs = new ArrayList<>();
- BookDTO bookDTO9781838986698 = new BookDTO();
- bookDTO9781838986698.setIsbn("9781838986698");
- bookDTO9781838986698.setTitle("The Java Workshop");
- bookDTO9781838986698
- .setSubtitle("Learn object-oriented programming and kickstart your career in software development");
- bookDTO9781838986698.setAuthor("David Cuartielles, Andreas Göransson, Eric Foster-Johnson");
- bookDTO9781838986698.setPublisher("Packt Publishing");
- bookDTO9781838986698.setPublished(LocalDate.of(2019, 10, 31));
- bookDTO9781838986698.setPages(606);
- bookDTO9781838986698.setDescription(
- """
- Java is a versatile, popular programming language used across a wide range of \
- industries. Learning how to write effective Java code can take your career to \
- the next level, and The Java Workshop will help you do just that. This book is \
- designed to take the pain out of Java coding and teach you everything you need \
- to know to be productive in building real-world software. The Workshop starts by \
- showing you how to use classes, methods, and the built-in Collections API to \
- manipulate data structures effortlessly. You'll dive right into learning about \
- object-oriented programming by creating classes and interfaces and making use of \
- inheritance and polymorphism. After learning how to handle exceptions, you'll \
- study the modules, packages, and libraries that help you organize your code. As \
- you progress, you'll discover how to connect to external databases and web \
- servers, work with regular expressions, and write unit tests to validate your \
- code. You'll also be introduced to functional programming and see how to \
- implement it using lambda functions. By the end of this Workshop, you'll be \
- well-versed with key Java concepts and have the knowledge and confidence to \
- tackle your own ambitious projects with Java.""");
- bookDTO9781838986698.setWebsite("https://www.packtpub.com/free-ebook/the-java-workshop/9781838986698");
- bookDTOs.add(bookDTO9781838986698);
- BookDTO bookDTO9781789613476 = new BookDTO();
- bookDTO9781789613476.setIsbn("9781789613476");
- bookDTO9781789613476.setTitle("Hands-On Microservices with Spring Boot and Spring Cloud");
- bookDTO9781789613476
- .setSubtitle("Build and deploy Java microservices using Spring Cloud, Istio, and Kubernetes");
- bookDTO9781789613476.setAuthor("Magnus Larsson");
- bookDTO9781789613476.setPublisher("Packt Publishing");
- bookDTO9781789613476.setPublished(LocalDate.of(2019, 9, 20));
- bookDTO9781789613476.setPages(668);
- bookDTO9781789613476.setDescription(
- """
- Microservices architecture allows developers to build and maintain applications \
- with ease, and enterprises are rapidly adopting it to build software using \
- Spring Boot as their default framework. With this book, you'll learn how to \
- efficiently build and deploy microservices using Spring Boot. This microservices \
- book will take you through tried and tested approaches to building distributed \
- systems and implementing microservices architecture in your organization. \
- Starting with a set of simple cooperating microservices developed using Spring \
- Boot, you'll learn how you can add functionalities such as persistence, make \
- your microservices reactive, and describe their APIs using Swagger/OpenAPI. As \
- you advance, you'll understand how to add different services from Spring Cloud \
- to your microservice system. The book also demonstrates how to deploy your \
- microservices using Kubernetes and manage them with Istio for improved security \
- and traffic management. Finally, you'll explore centralized log management using \
- the EFK stack and monitor microservices using Prometheus and Grafana. By the end \
- of this book, you'll be able to build microservices that are scalable and robust \
- using Spring Boot and Spring Cloud.""");
- bookDTO9781789613476.setWebsite(
- "https://www.packtpub.com/free-ebook/hands-on-microservices-with-spring-boot-and-spring-cloud/9781789613476");
- bookDTOs.add(bookDTO9781789613476);
- BookDTO bookDTO9781838555726 = new BookDTO();
- bookDTO9781838555726.setIsbn("9781838555726");
- bookDTO9781838555726.setTitle("Mastering Kotlin");
- bookDTO9781838555726.setSubtitle(
- "Learn advanced Kotlin programming techniques to build apps for Android, iOS, and the web");
- bookDTO9781838555726.setAuthor("Nate Ebel");
- bookDTO9781838555726.setPublisher("Packt Publishing");
- bookDTO9781838555726.setPublished(LocalDate.of(2019, 10, 11));
- bookDTO9781838555726.setPages(434);
- bookDTO9781838555726.setDescription(
- """
- Using Kotlin without taking advantage of its power and interoperability is like \
- owning a sports car and never taking it out of the garage. While documentation \
- and introductory resources can help you learn the basics of Kotlin, the fact \
- that it's a new language means that there are limited learning resources and \
- code bases available in comparison to Java and other established languages. This \
- Kotlin book will show you how to leverage software designs and concepts that \
- have made Java the most dominant enterprise programming language. You'll \
- understand how Kotlin is a modern approach to object-oriented programming (OOP). \
- This book will take you through the vast array of features that Kotlin provides \
- over other languages. These features include seamless interoperability with \
- Java, efficient syntax, built-in functional programming constructs, and support \
- for creating your own DSL. Finally, you will gain an understanding of \
- implementing practical design patterns and best practices to help you master the \
- Kotlin language. By the end of the book, you'll have obtained an advanced \
- understanding of Kotlin in order to be able to build production-grade \
- applications.""");
- bookDTO9781838555726.setWebsite("https://www.packtpub.com/free-ebook/mastering-kotlin/9781838555726");
- bookDTOs.add(bookDTO9781838555726);
- BookDTO bookDTO9781484242216 = new BookDTO();
- bookDTO9781484242216.setIsbn("9781484242216");
- bookDTO9781484242216.setTitle("Rethinking Productivity in Software Engineering");
- bookDTO9781484242216.setAuthor("Caitlin Sadowski, Thomas Zimmermann");
- bookDTO9781484242216.setPublisher("Apress");
- bookDTO9781484242216.setPublished(LocalDate.of(2019, 5, 7));
- bookDTO9781484242216.setPages(301);
- bookDTO9781484242216.setDescription(
- """
- Get the most out of this foundational reference and improve the productivity of \
- your software teams. This open access book collects the wisdom of the 2017 \
- "Dagstuhl" seminar on productivity in software engineering, a meeting of \
- community leaders, who came together with the goal of rethinking traditional \
- definitions and measures of productivity.""");
- bookDTO9781484242216.setWebsite("https://link.springer.com/book/10.1007/978-1-4842-4221-6");
- bookDTOs.add(bookDTO9781484242216);
- return bookDTOs;
- }
-}
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
deleted file mode 100644
index f591bce..0000000
--- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/BookFakes.java
+++ /dev/null
@@ -1,160 +0,0 @@
-package ar.com.nanotaboada.java.samples.spring.boot.test;
-
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.List;
-
-import ar.com.nanotaboada.java.samples.spring.boot.models.Book;
-
-public final class BookFakes {
-
- private BookFakes() {
- throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
- }
-
- public static Book createOneValid() {
- Book book = new Book();
- book.setIsbn("9781484200773");
- book.setTitle("Pro Git");
- 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(456);
- book.setDescription(
- """
- 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;
- }
-
- public static Book createOneInvalid() {
- Book book = new Book();
- book.setIsbn("9781234567890"); // Invalid (invalid ISBN)
- book.setTitle("Title");
- book.setSubtitle("Sub Title");
- book.setAuthor("Author");
- book.setPublisher("Publisher");
- book.setPublished(LocalDate.now()); // Invalid (must be a past date)
- book.setPages(123);
- book.setDescription("Description");
- book.setWebsite("https://domain.com/");
- return book;
- }
-
- public static List createManyValid() {
- ArrayList books = new ArrayList<>();
- Book book9781838986698 = new Book();
- book9781838986698.setIsbn("9781838986698");
- book9781838986698.setTitle("The Java Workshop");
- book9781838986698
- .setSubtitle("Learn object-oriented programming and kickstart your career in software development");
- book9781838986698.setAuthor("David Cuartielles, Andreas Göransson, Eric Foster-Johnson");
- book9781838986698.setPublisher("Packt Publishing");
- book9781838986698.setPublished(LocalDate.of(2019, 10, 31));
- book9781838986698.setPages(606);
- book9781838986698.setDescription(
- """
- Java is a versatile, popular programming language used across a wide range of \
- industries. Learning how to write effective Java code can take your career to \
- the next level, and The Java Workshop will help you do just that. This book is \
- designed to take the pain out of Java coding and teach you everything you need \
- to know to be productive in building real-world software. The Workshop starts by \
- showing you how to use classes, methods, and the built-in Collections API to \
- manipulate data structures effortlessly. You'll dive right into learning about \
- object-oriented programming by creating classes and interfaces and making use of \
- inheritance and polymorphism. After learning how to handle exceptions, you'll \
- study the modules, packages, and libraries that help you organize your code. As \
- you progress, you'll discover how to connect to external databases and web \
- servers, work with regular expressions, and write unit tests to validate your \
- code. You'll also be introduced to functional programming and see how to \
- implement it using lambda functions. By the end of this Workshop, you'll be \
- well-versed with key Java concepts and have the knowledge and confidence to \
- tackle your own ambitious projects with Java.""");
- book9781838986698.setWebsite("https://www.packtpub.com/free-ebook/the-java-workshop/9781838986698");
- books.add(book9781838986698);
- Book book9781789613476 = new Book();
- book9781789613476.setIsbn("9781789613476");
- book9781789613476.setTitle("Hands-On Microservices with Spring Boot and Spring Cloud");
- book9781789613476.setSubtitle("Build and deploy Java microservices using Spring Cloud, Istio, and Kubernetes");
- book9781789613476.setAuthor("Magnus Larsson");
- book9781789613476.setPublisher("Packt Publishing");
- book9781789613476.setPublished(LocalDate.of(2019, 9, 20));
- book9781789613476.setPages(668);
- book9781789613476.setDescription(
- """
- Microservices architecture allows developers to build and maintain applications \
- with ease, and enterprises are rapidly adopting it to build software using \
- Spring Boot as their default framework. With this book, you'll learn how to \
- efficiently build and deploy microservices using Spring Boot. This microservices \
- book will take you through tried and tested approaches to building distributed \
- systems and implementing microservices architecture in your organization. \
- Starting with a set of simple cooperating microservices developed using Spring \
- Boot, you'll learn how you can add functionalities such as persistence, make \
- your microservices reactive, and describe their APIs using Swagger/OpenAPI. As \
- you advance, you'll understand how to add different services from Spring Cloud \
- to your microservice system. The book also demonstrates how to deploy your \
- microservices using Kubernetes and manage them with Istio for improved security \
- and traffic management. Finally, you'll explore centralized log management using \
- the EFK stack and monitor microservices using Prometheus and Grafana. By the end \
- of this book, you'll be able to build microservices that are scalable and robust \
- using Spring Boot and Spring Cloud.""");
- book9781789613476.setWebsite(
- "https://www.packtpub.com/free-ebook/hands-on-microservices-with-spring-boot-and-spring-cloud/9781789613476");
- books.add(book9781789613476);
- Book book9781838555726 = new Book();
- book9781838555726.setIsbn("9781838555726");
- book9781838555726.setTitle("Mastering Kotlin");
- book9781838555726.setSubtitle(
- "Learn advanced Kotlin programming techniques to build apps for Android, iOS, and the web");
- book9781838555726.setAuthor("Nate Ebel");
- book9781838555726.setPublisher("Packt Publishing");
- book9781838555726.setPublished(LocalDate.of(2019, 10, 11));
- book9781838555726.setPages(434);
- book9781838555726.setDescription(
- """
- Using Kotlin without taking advantage of its power and interoperability is like \
- owning a sports car and never taking it out of the garage. While documentation \
- and introductory resources can help you learn the basics of Kotlin, the fact \
- that it's a new language means that there are limited learning resources and \
- code bases available in comparison to Java and other established languages. This \
- Kotlin book will show you how to leverage software designs and concepts that \
- have made Java the most dominant enterprise programming language. You'll \
- understand how Kotlin is a modern approach to object-oriented programming (OOP). \
- This book will take you through the vast array of features that Kotlin provides \
- over other languages. These features include seamless interoperability with \
- Java, efficient syntax, built-in functional programming constructs, and support \
- for creating your own DSL. Finally, you will gain an understanding of \
- implementing practical design patterns and best practices to help you master the \
- Kotlin language. By the end of the book, you'll have obtained an advanced \
- understanding of Kotlin in order to be able to build production-grade \
- applications.""");
- book9781838555726.setWebsite("https://www.packtpub.com/free-ebook/mastering-kotlin/9781838555726");
- books.add(book9781838555726);
- Book book9781484242216 = new Book();
- book9781484242216.setIsbn("9781484242216");
- book9781484242216.setTitle("Rethinking Productivity in Software Engineering");
- book9781484242216.setAuthor("Caitlin Sadowski, Thomas Zimmermann");
- book9781484242216.setPublisher("Apress");
- book9781484242216.setPublished(LocalDate.of(2019, 5, 7));
- book9781484242216.setPages(301);
- book9781484242216.setDescription(
- """
- Get the most out of this foundational reference and improve the productivity of \
- your software teams. This open access book collects the wisdom of the 2017 \
- "Dagstuhl" seminar on productivity in software engineering, a meeting of \
- community leaders, who came together with the goal of rethinking traditional \
- definitions and measures of productivity.""");
- book9781484242216.setWebsite("https://link.springer.com/book/10.1007/978-1-4842-4221-6");
- books.add(book9781484242216);
- return books;
- }
-}
diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/PlayerDTOFakes.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/PlayerDTOFakes.java
new file mode 100644
index 0000000..8cadd71
--- /dev/null
+++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/PlayerDTOFakes.java
@@ -0,0 +1,207 @@
+package ar.com.nanotaboada.java.samples.spring.boot.test;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+
+import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO;
+
+public final class PlayerDTOFakes {
+
+ private PlayerDTOFakes() {
+ throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
+ }
+
+ /**
+ * Leandro Paredes - Test data for CREATE operations
+ *
+ * Usage:
+ * - Service tests: Mock expected data for playersService.create()
+ * - Controller tests: Mock expected data for POST /players
+ *
+ * Note: Not pre-seeded in test DB (ID 19 slot is empty)
+ */
+ public static PlayerDTO createOneValid() {
+ PlayerDTO playerDTO = new PlayerDTO();
+ playerDTO.setId(null); // Will be auto-generated as 19
+ playerDTO.setFirstName("Leandro");
+ playerDTO.setMiddleName("Daniel");
+ playerDTO.setLastName("Paredes");
+ playerDTO.setDateOfBirth(LocalDate.of(1994, 6, 29));
+ playerDTO.setSquadNumber(5);
+ playerDTO.setPosition("Defensive Midfield");
+ playerDTO.setAbbrPosition("DM");
+ playerDTO.setTeam("AS Roma");
+ playerDTO.setLeague("Serie A");
+ playerDTO.setStarting11(false);
+ return playerDTO;
+ }
+
+ /**
+ * Damián Martínez - Player ID 1 BEFORE update
+ *
+ * Usage:
+ * - Service tests: Mock expected data for playersService.retrieveById(1L)
+ * - Controller tests: Mock expected data for GET /players/1
+ *
+ * Note: Repository tests query DB directly (pre-seeded in dml.sql)
+ */
+ public static PlayerDTO createOneForUpdate() {
+ PlayerDTO playerDTO = new PlayerDTO();
+ playerDTO.setId(1L);
+ playerDTO.setFirstName("Damián");
+ playerDTO.setMiddleName("Emiliano");
+ playerDTO.setLastName("Martínez");
+ playerDTO.setDateOfBirth(LocalDate.of(1992, 9, 2));
+ playerDTO.setSquadNumber(23);
+ playerDTO.setPosition("Goalkeeper");
+ playerDTO.setAbbrPosition("GK");
+ playerDTO.setTeam("Aston Villa FC");
+ playerDTO.setLeague("Premier League");
+ playerDTO.setStarting11(true);
+ return playerDTO;
+ }
+
+ /**
+ * Emiliano Martínez - Expected result AFTER updating player ID 1
+ *
+ * Usage:
+ * - Service tests: Mock expected data after playersService.update()
+ * - Controller tests: Mock expected data after PUT /players/1
+ *
+ * Update changes:
+ * - firstName: "Damián" → "Emiliano"
+ * - middleName: "Emiliano" → null
+ */
+ public static PlayerDTO createOneUpdated() {
+ PlayerDTO playerDTO = new PlayerDTO();
+ playerDTO.setId(1L);
+ playerDTO.setFirstName("Emiliano");
+ playerDTO.setMiddleName(null);
+ playerDTO.setLastName("Martínez");
+ playerDTO.setDateOfBirth(LocalDate.of(1992, 9, 2));
+ playerDTO.setSquadNumber(23);
+ playerDTO.setPosition("Goalkeeper");
+ playerDTO.setAbbrPosition("GK");
+ playerDTO.setTeam("Aston Villa FC");
+ playerDTO.setLeague("Premier League");
+ playerDTO.setStarting11(true);
+ return playerDTO;
+ }
+
+ /**
+ * Invalid player data - Test data for validation failure scenarios
+ *
+ * Usage:
+ * - Controller tests: Verify validation annotations work
+ * (@NotBlank, @Past, @Positive)
+ *
+ * Violations: blank names, future date, negative squad number, blank fields
+ */
+ public static PlayerDTO createOneInvalid() {
+ PlayerDTO playerDTO = new PlayerDTO();
+ playerDTO.setId(999L);
+ playerDTO.setFirstName(""); // Invalid (blank)
+ playerDTO.setMiddleName(null);
+ playerDTO.setLastName(""); // Invalid (blank)
+ playerDTO.setDateOfBirth(LocalDate.now()); // Invalid (must be a past date)
+ playerDTO.setSquadNumber(-1); // Invalid (must be positive)
+ playerDTO.setPosition(""); // Invalid (blank)
+ playerDTO.setAbbrPosition(null);
+ playerDTO.setTeam(""); // Invalid (blank)
+ playerDTO.setLeague(null);
+ playerDTO.setStarting11(null);
+ return playerDTO;
+ }
+
+ /**
+ * ALL 26 players - Complete Argentina 2022 FIFA World Cup squad
+ *
+ * Usage:
+ * - Service tests: Mock expected data for playersService.retrieveAll()
+ * - Controller tests: Mock expected data for GET /players
+ *
+ * Includes:
+ * - 25 players pre-seeded in test DB (IDs 1-26, excluding 19)
+ * - Leandro Paredes (ID 19, created during tests)
+ *
+ * Note: Repository tests query real in-memory DB directly (25 players
+ * pre-seeded)
+ */
+ public static List createAll() {
+ return Arrays.asList(
+ // Starting 11
+ createPlayerDTOWithId(1L, "Damián", "Emiliano", "Martínez", LocalDate.of(1992, 9, 2), 23, "Goalkeeper",
+ "GK", "Aston Villa FC", "Premier League", true),
+ createPlayerDTOWithId(2L, "Nahuel", null, "Molina", LocalDate.of(1998, 4, 6), 26, "Right-Back", "RB",
+ "Atlético Madrid", "La Liga", true),
+ createPlayerDTOWithId(3L, "Cristian", "Gabriel", "Romero", LocalDate.of(1998, 4, 27), 13, "Centre-Back",
+ "CB", "Tottenham Hotspur", "Premier League", true),
+ createPlayerDTOWithId(4L, "Nicolás", "Hernán Gonzalo", "Otamendi", LocalDate.of(1988, 2, 12), 19,
+ "Centre-Back", "CB", "SL Benfica", "Liga Portugal", true),
+ createPlayerDTOWithId(5L, "Nicolás", "Alejandro", "Tagliafico", LocalDate.of(1992, 8, 31), 3,
+ "Left-Back", "LB", "Olympique Lyon", "Ligue 1", true),
+ createPlayerDTOWithId(6L, "Ángel", "Fabián", "Di María", LocalDate.of(1988, 2, 14), 11, "Right Winger",
+ "RW", "SL Benfica", "Liga Portugal", true),
+ createPlayerDTOWithId(7L, "Rodrigo", "Javier", "de Paul", LocalDate.of(1994, 5, 24), 7,
+ "Central Midfield", "CM", "Atlético Madrid", "La Liga", true),
+ createPlayerDTOWithId(8L, "Enzo", "Jeremías", "Fernández", LocalDate.of(2001, 1, 17), 24,
+ "Central Midfield", "CM", "Chelsea FC", "Premier League", true),
+ createPlayerDTOWithId(9L, "Alexis", null, "Mac Allister", LocalDate.of(1998, 12, 24), 20,
+ "Central Midfield", "CM", "Liverpool FC", "Premier League", true),
+ createPlayerDTOWithId(10L, "Lionel", "Andrés", "Messi", LocalDate.of(1987, 6, 24), 10, "Right Winger",
+ "RW", "Inter Miami CF", "Major League Soccer", true),
+ createPlayerDTOWithId(11L, "Julián", null, "Álvarez", LocalDate.of(2000, 1, 31), 9, "Centre-Forward",
+ "CF", "Manchester City", "Premier League", true),
+ // Substitutes
+ createPlayerDTOWithId(12L, "Franco", "Daniel", "Armani", LocalDate.of(1986, 10, 16), 1, "Goalkeeper",
+ "GK", "River Plate", "Copa de la Liga", false),
+ createPlayerDTOWithId(13L, "Gerónimo", null, "Rulli", LocalDate.of(1992, 5, 20), 12, "Goalkeeper", "GK",
+ "Ajax Amsterdam", "Eredivisie", false),
+ createPlayerDTOWithId(14L, "Juan", "Marcos", "Foyth", LocalDate.of(1998, 1, 12), 2, "Right-Back", "RB",
+ "Villarreal", "La Liga", false),
+ createPlayerDTOWithId(15L, "Gonzalo", "Ariel", "Montiel", LocalDate.of(1997, 1, 1), 4, "Right-Back",
+ "RB", "Nottingham Forest", "Premier League", false),
+ createPlayerDTOWithId(16L, "Germán", "Alejo", "Pezzella", LocalDate.of(1991, 6, 27), 6, "Centre-Back",
+ "CB", "Real Betis Balompié", "La Liga", false),
+ createPlayerDTOWithId(17L, "Marcos", "Javier", "Acuña", LocalDate.of(1991, 10, 28), 8, "Left-Back",
+ "LB", "Sevilla FC", "La Liga", false),
+ createPlayerDTOWithId(18L, "Lisandro", null, "Martínez", LocalDate.of(1998, 1, 18), 25, "Centre-Back",
+ "CB", "Manchester United", "Premier League", false),
+ // Leandro Paredes (ID 19) - created during tests
+ createPlayerDTOWithId(19L, "Leandro", "Daniel", "Paredes", LocalDate.of(1994, 6, 29), 5,
+ "Defensive Midfield", "DM", "AS Roma", "Serie A", false),
+ createPlayerDTOWithId(20L, "Exequiel", "Alejandro", "Palacios", LocalDate.of(1998, 10, 5), 14,
+ "Central Midfield", "CM", "Bayer 04 Leverkusen", "Bundesliga", false),
+ createPlayerDTOWithId(21L, "Alejandro", "Darío", "Gómez", LocalDate.of(1988, 2, 15), 17, "Left Winger",
+ "LW", "AC Monza", "Serie A", false),
+ createPlayerDTOWithId(22L, "Guido", null, "Rodríguez", LocalDate.of(1994, 4, 12), 18,
+ "Defensive Midfield", "DM", "Real Betis Balompié", "La Liga", false),
+ createPlayerDTOWithId(23L, "Ángel", "Martín", "Correa", LocalDate.of(1995, 3, 9), 15, "Right Winger",
+ "RW", "Atlético Madrid", "La Liga", false),
+ createPlayerDTOWithId(24L, "Thiago", "Ezequiel", "Almada", LocalDate.of(2001, 4, 26), 16,
+ "Attacking Midfield", "AM", "Atlanta United FC", "Major League Soccer", false),
+ createPlayerDTOWithId(25L, "Paulo", "Exequiel", "Dybala", LocalDate.of(1993, 11, 15), 21,
+ "Second Striker", "SS", "AS Roma", "Serie A", false),
+ createPlayerDTOWithId(26L, "Lautaro", "Javier", "Martínez", LocalDate.of(1997, 8, 22), 22,
+ "Centre-Forward", "CF", "Inter Milan", "Serie A", false));
+ }
+
+ private static PlayerDTO createPlayerDTOWithId(Long id, String firstName, String middleName, String lastName,
+ LocalDate dateOfBirth, Integer squadNumber, String position,
+ String abbrPosition, String team, String league, Boolean starting11) {
+ PlayerDTO playerDTO = new PlayerDTO();
+ playerDTO.setId(id);
+ playerDTO.setFirstName(firstName);
+ playerDTO.setMiddleName(middleName);
+ playerDTO.setLastName(lastName);
+ playerDTO.setDateOfBirth(dateOfBirth);
+ playerDTO.setSquadNumber(squadNumber);
+ playerDTO.setPosition(position);
+ playerDTO.setAbbrPosition(abbrPosition);
+ playerDTO.setTeam(team);
+ playerDTO.setLeague(league);
+ playerDTO.setStarting11(starting11);
+ return playerDTO;
+ }
+}
diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/PlayerFakes.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/PlayerFakes.java
new file mode 100644
index 0000000..5eea56c
--- /dev/null
+++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/PlayerFakes.java
@@ -0,0 +1,210 @@
+package ar.com.nanotaboada.java.samples.spring.boot.test;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+
+import ar.com.nanotaboada.java.samples.spring.boot.models.Player;
+
+public final class PlayerFakes {
+
+ private PlayerFakes() {
+ throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
+ }
+
+ /**
+ * Leandro Paredes - Test data for CREATE operations
+ *
+ * Usage:
+ * - Repository tests: Insert into real in-memory DB (gets ID 19)
+ * - Service tests: Mock expected data for playersService.create()
+ * - Controller tests: Mock expected data for POST /players
+ *
+ * Note: Not pre-seeded in test DB (ID 19 slot is empty)
+ */
+ public static Player createOneValid() {
+ Player player = new Player();
+ player.setId(null); // Will be auto-generated as 19
+ player.setFirstName("Leandro");
+ player.setMiddleName("Daniel");
+ player.setLastName("Paredes");
+ player.setDateOfBirth(LocalDate.of(1994, 6, 29));
+ player.setSquadNumber(5);
+ player.setPosition("Defensive Midfield");
+ player.setAbbrPosition("DM");
+ player.setTeam("AS Roma");
+ player.setLeague("Serie A");
+ player.setStarting11(false);
+ return player;
+ }
+
+ /**
+ * Damián Martínez - Player ID 1 BEFORE update
+ *
+ * Usage:
+ * - Service tests: Mock expected data for playersService.retrieveById(1L)
+ * - Controller tests: Mock expected data for GET /players/1
+ *
+ * Note: Repository tests query DB directly (pre-seeded in dml.sql)
+ */
+ public static Player createOneForUpdate() {
+ Player player = new Player();
+ player.setId(1L);
+ player.setFirstName("Damián");
+ player.setMiddleName("Emiliano");
+ player.setLastName("Martínez");
+ player.setDateOfBirth(LocalDate.of(1992, 9, 2));
+ player.setSquadNumber(23);
+ player.setPosition("Goalkeeper");
+ player.setAbbrPosition("GK");
+ player.setTeam("Aston Villa FC");
+ player.setLeague("Premier League");
+ player.setStarting11(true);
+ return player;
+ }
+
+ /**
+ * Emiliano Martínez - Expected result AFTER updating player ID 1
+ *
+ * Usage:
+ * - Service tests: Mock expected data after playersService.update()
+ * - Controller tests: Mock expected data after PUT /players/1
+ *
+ * Update changes:
+ * - firstName: "Damián" → "Emiliano"
+ * - middleName: "Emiliano" → null
+ *
+ * Note: Repository tests should query DB directly for before/after states
+ */
+ public static Player createOneUpdated() {
+ Player player = new Player();
+ player.setId(1L);
+ player.setFirstName("Emiliano");
+ player.setMiddleName(null);
+ player.setLastName("Martínez");
+ player.setDateOfBirth(LocalDate.of(1992, 9, 2));
+ player.setSquadNumber(23);
+ player.setPosition("Goalkeeper");
+ player.setAbbrPosition("GK");
+ player.setTeam("Aston Villa FC");
+ player.setLeague("Premier League");
+ player.setStarting11(true);
+ return player;
+ }
+
+ /**
+ * Invalid player data - Test data for validation failure scenarios
+ *
+ * Usage:
+ * - Controller tests: Verify validation annotations work
+ * (@NotBlank, @Past, @Positive)
+ *
+ * Violations: blank names, future date, negative squad number, blank fields
+ */
+ public static Player createOneInvalid() {
+ Player player = new Player();
+ player.setId(999L);
+ player.setFirstName(""); // Invalid (blank)
+ player.setMiddleName(null);
+ player.setLastName(""); // Invalid (blank)
+ player.setDateOfBirth(LocalDate.now()); // Invalid (must be a past date)
+ player.setSquadNumber(-1); // Invalid (must be positive)
+ player.setPosition(""); // Invalid (blank)
+ player.setAbbrPosition(null);
+ player.setTeam(""); // Invalid (blank)
+ player.setLeague(null);
+ player.setStarting11(null);
+ return player;
+ }
+
+ /**
+ * ALL 26 players - Complete Argentina 2022 FIFA World Cup squad
+ *
+ * Usage:
+ * - Service tests: Mock expected data for playersService.retrieveAll()
+ * - Controller tests: Mock expected data for GET /players
+ *
+ * Includes:
+ * - 25 players pre-seeded in test DB (IDs 1-26, excluding 19)
+ * - Leandro Paredes (ID 19, created during tests)
+ *
+ * Note: Repository tests query real in-memory DB directly (25 players
+ * pre-seeded)
+ */
+ public static List createAll() {
+ return Arrays.asList(
+ // Starting 11
+ createPlayerWithId(1L, "Damián", "Emiliano", "Martínez", LocalDate.of(1992, 9, 2), 23, "Goalkeeper",
+ "GK", "Aston Villa FC", "Premier League", true),
+ createPlayerWithId(2L, "Nahuel", null, "Molina", LocalDate.of(1998, 4, 6), 26, "Right-Back", "RB",
+ "Atlético Madrid", "La Liga", true),
+ createPlayerWithId(3L, "Cristian", "Gabriel", "Romero", LocalDate.of(1998, 4, 27), 13, "Centre-Back",
+ "CB", "Tottenham Hotspur", "Premier League", true),
+ createPlayerWithId(4L, "Nicolás", "Hernán Gonzalo", "Otamendi", LocalDate.of(1988, 2, 12), 19,
+ "Centre-Back", "CB", "SL Benfica", "Liga Portugal", true),
+ createPlayerWithId(5L, "Nicolás", "Alejandro", "Tagliafico", LocalDate.of(1992, 8, 31), 3, "Left-Back",
+ "LB", "Olympique Lyon", "Ligue 1", true),
+ createPlayerWithId(6L, "Ángel", "Fabián", "Di María", LocalDate.of(1988, 2, 14), 11, "Right Winger",
+ "RW", "SL Benfica", "Liga Portugal", true),
+ createPlayerWithId(7L, "Rodrigo", "Javier", "de Paul", LocalDate.of(1994, 5, 24), 7, "Central Midfield",
+ "CM", "Atlético Madrid", "La Liga", true),
+ createPlayerWithId(8L, "Enzo", "Jeremías", "Fernández", LocalDate.of(2001, 1, 17), 24,
+ "Central Midfield", "CM", "Chelsea FC", "Premier League", true),
+ createPlayerWithId(9L, "Alexis", null, "Mac Allister", LocalDate.of(1998, 12, 24), 20,
+ "Central Midfield", "CM", "Liverpool FC", "Premier League", true),
+ createPlayerWithId(10L, "Lionel", "Andrés", "Messi", LocalDate.of(1987, 6, 24), 10, "Right Winger",
+ "RW", "Inter Miami CF", "Major League Soccer", true),
+ createPlayerWithId(11L, "Julián", null, "Álvarez", LocalDate.of(2000, 1, 31), 9, "Centre-Forward", "CF",
+ "Manchester City", "Premier League", true),
+ // Substitutes
+ createPlayerWithId(12L, "Franco", "Daniel", "Armani", LocalDate.of(1986, 10, 16), 1, "Goalkeeper", "GK",
+ "River Plate", "Copa de la Liga", false),
+ createPlayerWithId(13L, "Gerónimo", null, "Rulli", LocalDate.of(1992, 5, 20), 12, "Goalkeeper", "GK",
+ "Ajax Amsterdam", "Eredivisie", false),
+ createPlayerWithId(14L, "Juan", "Marcos", "Foyth", LocalDate.of(1998, 1, 12), 2, "Right-Back", "RB",
+ "Villarreal", "La Liga", false),
+ createPlayerWithId(15L, "Gonzalo", "Ariel", "Montiel", LocalDate.of(1997, 1, 1), 4, "Right-Back", "RB",
+ "Nottingham Forest", "Premier League", false),
+ createPlayerWithId(16L, "Germán", "Alejo", "Pezzella", LocalDate.of(1991, 6, 27), 6, "Centre-Back",
+ "CB", "Real Betis Balompié", "La Liga", false),
+ createPlayerWithId(17L, "Marcos", "Javier", "Acuña", LocalDate.of(1991, 10, 28), 8, "Left-Back", "LB",
+ "Sevilla FC", "La Liga", false),
+ createPlayerWithId(18L, "Lisandro", null, "Martínez", LocalDate.of(1998, 1, 18), 25, "Centre-Back",
+ "CB", "Manchester United", "Premier League", false),
+ // Leandro Paredes (ID 19) - created during tests
+ createPlayerWithId(19L, "Leandro", "Daniel", "Paredes", LocalDate.of(1994, 6, 29), 5,
+ "Defensive Midfield", "DM", "AS Roma", "Serie A", false),
+ createPlayerWithId(20L, "Exequiel", "Alejandro", "Palacios", LocalDate.of(1998, 10, 5), 14,
+ "Central Midfield", "CM", "Bayer 04 Leverkusen", "Bundesliga", false),
+ createPlayerWithId(21L, "Alejandro", "Darío", "Gómez", LocalDate.of(1988, 2, 15), 17, "Left Winger",
+ "LW", "AC Monza", "Serie A", false),
+ createPlayerWithId(22L, "Guido", null, "Rodríguez", LocalDate.of(1994, 4, 12), 18, "Defensive Midfield",
+ "DM", "Real Betis Balompié", "La Liga", false),
+ createPlayerWithId(23L, "Ángel", "Martín", "Correa", LocalDate.of(1995, 3, 9), 15, "Right Winger", "RW",
+ "Atlético Madrid", "La Liga", false),
+ createPlayerWithId(24L, "Thiago", "Ezequiel", "Almada", LocalDate.of(2001, 4, 26), 16,
+ "Attacking Midfield", "AM", "Atlanta United FC", "Major League Soccer", false),
+ createPlayerWithId(25L, "Paulo", "Exequiel", "Dybala", LocalDate.of(1993, 11, 15), 21, "Second Striker",
+ "SS", "AS Roma", "Serie A", false),
+ createPlayerWithId(26L, "Lautaro", "Javier", "Martínez", LocalDate.of(1997, 8, 22), 22,
+ "Centre-Forward", "CF", "Inter Milan", "Serie A", false));
+ }
+
+ private static Player createPlayerWithId(Long id, String firstName, String middleName, String lastName,
+ LocalDate dateOfBirth, Integer squadNumber, String position,
+ String abbrPosition, String team, String league, Boolean starting11) {
+ Player player = new Player();
+ player.setId(id);
+ player.setFirstName(firstName);
+ player.setMiddleName(middleName);
+ player.setLastName(lastName);
+ player.setDateOfBirth(dateOfBirth);
+ player.setSquadNumber(squadNumber);
+ player.setPosition(position);
+ player.setAbbrPosition(abbrPosition);
+ player.setTeam(team);
+ player.setLeague(league);
+ player.setStarting11(starting11);
+ return player;
+ }
+}
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
deleted file mode 100644
index cb76cd4..0000000
--- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/BooksControllerTests.java
+++ /dev/null
@@ -1,437 +0,0 @@
-package ar.com.nanotaboada.java.samples.spring.boot.test.controllers;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import java.util.List;
-
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.cache.test.autoconfigure.AutoConfigureCache;
-import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.test.context.bean.override.mockito.MockitoBean;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
-import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import ar.com.nanotaboada.java.samples.spring.boot.controllers.BooksController;
-import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO;
-import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository;
-import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService;
-import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOFakes;
-
-@DisplayName("HTTP Methods on Controller")
-@WebMvcTest(BooksController.class)
-@AutoConfigureCache
-class BooksControllerTests {
-
- private static final String PATH = "/books";
-
- @Autowired
- private MockMvc application;
-
- @MockitoBean
- private BooksService booksServiceMock;
-
- @MockitoBean
- private BooksRepository booksRepositoryMock;
-
- /*
- * -------------------------------------------------------------------------
- * HTTP POST
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenPost_whenRequestBodyIsValidButExistingBook_thenResponseStatusIsConflict()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String body = new ObjectMapper().writeValueAsString(bookDTO);
- Mockito
- .when(booksServiceMock.create(any(BookDTO.class)))
- .thenReturn(false); // Existing
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .post(PATH)
- .content(body)
- .contentType(MediaType.APPLICATION_JSON);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, times(1)).create(any(BookDTO.class));
- assertThat(response.getStatus()).isEqualTo(HttpStatus.CONFLICT.value());
-
- }
-
- @Test
- void givenPost_whenRequestBodyIsValidAndNonExistentBook_thenResponseStatusIsCreated()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String body = new ObjectMapper().writeValueAsString(bookDTO);
- Mockito
- .when(booksServiceMock.create(any(BookDTO.class)))
- .thenReturn(true); // Non-existent
- Mockito
- .when(booksServiceMock.create(any(BookDTO.class)))
- .thenReturn(true);
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .post(PATH)
- .content(body)
- .contentType(MediaType.APPLICATION_JSON);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- response.setContentType("application/json;charset=UTF-8");
- // Assert
- verify(booksServiceMock, times(1)).create(any(BookDTO.class));
- assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
- assertThat(response.getHeader(HttpHeaders.LOCATION)).isNotNull();
- assertThat(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/" + bookDTO.getIsbn());
-
- }
-
- @Test
- void givenPost_whenRequestBodyIsInvalidBook_thenResponseStatusIsBadRequest()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneInvalid();
- String body = new ObjectMapper().writeValueAsString(bookDTO);
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .post(PATH)
- .content(body)
- .contentType(MediaType.APPLICATION_JSON);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, never()).create(any(BookDTO.class));
- assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- }
-
- /*
- * -------------------------------------------------------------------------
- * HTTP GET
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenGetByIsbn_whenRequestPathVariableIsValidAndExistingISBN_thenResponseStatusIsOKAndResultIsBook()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String isbn = bookDTO.getIsbn();
- Mockito
- .when(booksServiceMock.retrieveByIsbn(anyString()))
- .thenReturn(bookDTO); // Existing
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .get(PATH + "/{isbn}", isbn);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- response.setContentType("application/json;charset=UTF-8");
- String content = response.getContentAsString();
- BookDTO result = new ObjectMapper().readValue(content, BookDTO.class);
- // Assert
- verify(booksServiceMock, times(1)).retrieveByIsbn(anyString());
- assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
- assertThat(result).usingRecursiveComparison().isEqualTo(bookDTO);
- }
-
- @Test
- void givenGetByIsbn_whenRequestPathVariableIsValidButNonExistentISBN_thenResponseStatusIsNotFound()
- throws Exception {
- // Arrange
- String isbn = "9781484242216";
- Mockito
- .when(booksServiceMock.retrieveByIsbn(anyString()))
- .thenReturn(null); // Non-existent
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .get(PATH + "/{isbn}", isbn);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, times(1)).retrieveByIsbn(anyString());
- assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
- }
-
- @Test
- void givenGetAll_whenRequestPathIsBooks_thenResponseIsOkAndResultIsBooks()
- throws Exception {
- // Arrange
- List bookDTOs = BookDTOFakes.createManyValid();
- Mockito
- .when(booksServiceMock.retrieveAll())
- .thenReturn(bookDTOs);
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .get(PATH);
- // 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)).retrieveAll();
- assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
- assertThat(result).usingRecursiveComparison().isEqualTo(bookDTOs);
- }
-
- /*
- * -------------------------------------------------------------------------
- * HTTP PUT
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenPut_whenRequestBodyIsValidAndExistingBook_thenResponseStatusIsNoContent()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String body = new ObjectMapper().writeValueAsString(bookDTO);
- Mockito
- .when(booksServiceMock.update(any(BookDTO.class)))
- .thenReturn(true); // Existing
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .put(PATH)
- .content(body)
- .contentType(MediaType.APPLICATION_JSON);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, times(1)).update(any(BookDTO.class));
- assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
- }
-
- @Test
- void givenPut_whenRequestBodyIsValidButNonExistentBook_thenResponseStatusIsNotFound()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String body = new ObjectMapper().writeValueAsString(bookDTO);
- Mockito
- .when(booksServiceMock.update(any(BookDTO.class)))
- .thenReturn(false); // Non-existent
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .put(PATH)
- .content(body)
- .contentType(MediaType.APPLICATION_JSON);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, times(1)).update(any(BookDTO.class));
- assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
- }
-
- @Test
- void givenPut_whenRequestBodyIsInvalidBook_thenResponseStatusIsBadRequest()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneInvalid();
- String body = new ObjectMapper().writeValueAsString(bookDTO);
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .put(PATH)
- .content(body)
- .contentType(MediaType.APPLICATION_JSON);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, never()).update(any(BookDTO.class));
- assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
- }
-
- /*
- * -------------------------------------------------------------------------
- * HTTP DELETE
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenDelete_whenPathVariableIsValidAndExistingISBN_thenResponseStatusIsNoContent()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String isbn = bookDTO.getIsbn();
- Mockito
- .when(booksServiceMock.delete(anyString()))
- .thenReturn(true); // Existing
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .delete(PATH + "/{isbn}", isbn);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, times(1)).delete(anyString());
- assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
- }
-
- @Test
- void givenDelete_whenPathVariableIsValidButNonExistentISBN_thenResponseStatusIsNotFound()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- String isbn = bookDTO.getIsbn();
- Mockito
- .when(booksServiceMock.delete(anyString()))
- .thenReturn(false); // Non-existent
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .delete(PATH + "/{isbn}", isbn);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- verify(booksServiceMock, times(1)).delete(anyString());
- assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
- }
-
- @Test
- void givenDelete_whenPathVariableIsInvalidISBN_thenResponseStatusIsBadRequest()
- throws Exception {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneInvalid();
- String isbn = bookDTO.getIsbn();
- MockHttpServletRequestBuilder request = MockMvcRequestBuilders
- .delete(PATH + "/{isbn}", isbn);
- // Act
- MockHttpServletResponse response = application
- .perform(request)
- .andReturn()
- .getResponse();
- // Assert
- 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/controllers/PlayersControllerTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java
new file mode 100644
index 0000000..cc64250
--- /dev/null
+++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/controllers/PlayersControllerTests.java
@@ -0,0 +1,573 @@
+package ar.com.nanotaboada.java.samples.spring.boot.test.controllers;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.cache.test.autoconfigure.AutoConfigureCache;
+import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import ar.com.nanotaboada.java.samples.spring.boot.controllers.PlayersController;
+import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO;
+import ar.com.nanotaboada.java.samples.spring.boot.repositories.PlayersRepository;
+import ar.com.nanotaboada.java.samples.spring.boot.services.PlayersService;
+import ar.com.nanotaboada.java.samples.spring.boot.test.PlayerDTOFakes;
+
+@DisplayName("HTTP Methods on Controller")
+@WebMvcTest(PlayersController.class)
+@AutoConfigureCache
+@AutoConfigureJsonTesters
+class PlayersControllerTests {
+
+ private static final String PATH = "/players";
+
+ @Autowired
+ private MockMvc application;
+
+ @MockitoBean
+ private PlayersService playersServiceMock;
+
+ @MockitoBean
+ private PlayersRepository playersRepositoryMock;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @TestConfiguration
+ static class ObjectMapperTestConfig {
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper().findAndRegisterModules();
+ }
+ }
+
+ /*
+ * -------------------------------------------------------------------------
+ * HTTP POST
+ * -------------------------------------------------------------------------
+ */
+
+ /**
+ * Given valid player data is provided
+ * When POST /players is called and the service successfully creates the player
+ * Then response status is 201 Created and Location header points to the new resource
+ */
+ @Test
+ void post_validPlayer_returnsCreated()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ PlayerDTO savedPlayer = PlayerDTOFakes.createOneValid();
+ savedPlayer.setId(19L); // Simulating auto-generated ID
+ String body = objectMapper.writeValueAsString(playerDTO);
+ Mockito
+ .when(playersServiceMock.create(any(PlayerDTO.class)))
+ .thenReturn(savedPlayer);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .post(PATH)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).create(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
+ assertThat(response.getHeader(HttpHeaders.LOCATION)).isNotNull();
+ assertThat(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/19");
+ }
+
+ /**
+ * Given invalid player data is provided (validation fails)
+ * When POST /players is called
+ * Then response status is 400 Bad Request and service is never called
+ */
+ @Test
+ void post_invalidPlayer_returnsBadRequest()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneInvalid();
+ String body = objectMapper.writeValueAsString(playerDTO);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .post(PATH)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, never()).create(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+ }
+
+ /**
+ * Given a player with the same squad number already exists
+ * When POST /players is called and the service detects a conflict
+ * Then response status is 409 Conflict
+ */
+ @Test
+ void post_squadNumberExists_returnsConflict()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ String body = objectMapper.writeValueAsString(playerDTO);
+ Mockito
+ .when(playersServiceMock.create(any(PlayerDTO.class)))
+ .thenReturn(null); // Conflict: squad number already exists
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .post(PATH)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).create(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.CONFLICT.value());
+ }
+
+ /*
+ * -------------------------------------------------------------------------
+ * HTTP GET
+ * -------------------------------------------------------------------------
+ */
+
+ /**
+ * Given a player with ID 1 exists (Damián Martínez)
+ * When GET /players/1 is called and the service returns the player
+ * Then response status is 200 OK and the player data is returned
+ */
+ @Test
+ void getById_playerExists_returnsOkWithPlayer()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneForUpdate();
+ Long id = 1L;
+ Mockito
+ .when(playersServiceMock.retrieveById(id))
+ .thenReturn(playerDTO);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH + "/{id}", id);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ String content = response.getContentAsString();
+ PlayerDTO result = objectMapper.readValue(content, PlayerDTO.class);
+ // Assert
+ assertThat(response.getContentType()).contains("application/json");
+ verify(playersServiceMock, times(1)).retrieveById(id);
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(result).usingRecursiveComparison().isEqualTo(playerDTO);
+ }
+
+ /**
+ * Given a player with ID 999 does not exist
+ * When GET /players/999 is called and the service returns null
+ * Then response status is 404 Not Found
+ */
+ @Test
+ void getById_playerNotFound_returnsNotFound()
+ throws Exception {
+ // Arrange
+ Long id = 999L;
+ Mockito
+ .when(playersServiceMock.retrieveById(anyLong()))
+ .thenReturn(null);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH + "/{id}", id);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).retrieveById(anyLong());
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
+ }
+
+ /**
+ * Given 26 players exist in the database
+ * When GET /players is called and the service returns all players
+ * Then response status is 200 OK and all 26 players are returned
+ */
+ @Test
+ void getAll_playersExist_returnsOkWithAllPlayers()
+ throws Exception {
+ // Arrange
+ List playerDTOs = PlayerDTOFakes.createAll();
+ Mockito
+ .when(playersServiceMock.retrieveAll())
+ .thenReturn(playerDTOs);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ String content = response.getContentAsString();
+ List result = objectMapper.readValue(content, new TypeReference>() {
+ });
+ // Assert
+ assertThat(response.getContentType()).contains("application/json");
+ verify(playersServiceMock, times(1)).retrieveAll();
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(result).hasSize(26);
+ assertThat(result).usingRecursiveComparison().isEqualTo(playerDTOs);
+ }
+
+ /**
+ * Given 7 players exist in Premier League
+ * When GET /players/search/league/Premier is called and the service returns matching players
+ * Then response status is 200 OK and 7 players are returned
+ */
+ @Test
+ void searchByLeague_matchingPlayersExist_returnsOkWithList()
+ throws Exception {
+ // Arrange
+ List playerDTOs = PlayerDTOFakes.createAll().stream()
+ .filter(p -> p.getLeague().contains("Premier"))
+ .toList();
+ Mockito
+ .when(playersServiceMock.searchByLeague(any()))
+ .thenReturn(playerDTOs);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH + "/search/league/{league}", "Premier");
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ String content = response.getContentAsString();
+ List result = objectMapper.readValue(content, new TypeReference>() {
+ });
+ // Assert
+ assertThat(response.getContentType()).contains("application/json");
+ verify(playersServiceMock, times(1)).searchByLeague(any());
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(result).hasSize(7);
+ assertThat(result).usingRecursiveComparison().isEqualTo(playerDTOs);
+ }
+
+ /**
+ * Given no players exist in "NonexistentLeague"
+ * When GET /players/search/league/NonexistentLeague is called and the service returns an empty list
+ * Then response status is 200 OK and an empty list is returned
+ */
+ @Test
+ void searchByLeague_noMatches_returnsOkWithEmptyList()
+ throws Exception {
+ // Arrange
+ Mockito
+ .when(playersServiceMock.searchByLeague(any()))
+ .thenReturn(List.of());
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH + "/search/league/{league}", "NonexistentLeague");
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ String content = response.getContentAsString();
+ List result = objectMapper.readValue(content, new TypeReference>() {
+ });
+ // Assert
+ assertThat(response.getContentType()).contains("application/json");
+ verify(playersServiceMock, times(1)).searchByLeague(any());
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(result).isEmpty();
+ }
+
+ /**
+ * Given a player with squad number 10 exists (Messi)
+ * When GET /players/search/squadnumber/10 is called and the service returns the player
+ * Then response status is 200 OK and the player data is returned
+ */
+ @Test
+ void searchBySquadNumber_playerExists_returnsOkWithPlayer()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createAll().stream()
+ .filter(player -> player.getSquadNumber() == 10)
+ .findFirst()
+ .orElseThrow();
+ Mockito
+ .when(playersServiceMock.searchBySquadNumber(10))
+ .thenReturn(playerDTO);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH + "/search/squadnumber/{squadNumber}", 10);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ String content = response.getContentAsString();
+ PlayerDTO result = objectMapper.readValue(content, PlayerDTO.class);
+ // Assert
+ assertThat(response.getContentType()).contains("application/json");
+ verify(playersServiceMock, times(1)).searchBySquadNumber(10);
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
+ assertThat(result).usingRecursiveComparison().isEqualTo(playerDTO);
+ assertThat(result.getSquadNumber()).isEqualTo(10);
+ }
+
+ /**
+ * Given no player with squad number 99 exists
+ * When GET /players/search/squadnumber/99 is called and the service returns null
+ * Then response status is 404 Not Found
+ */
+ @Test
+ void searchBySquadNumber_playerNotFound_returnsNotFound()
+ throws Exception {
+ // Arrange
+ Mockito
+ .when(playersServiceMock.searchBySquadNumber(99))
+ .thenReturn(null);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .get(PATH + "/search/squadnumber/{squadNumber}", 99);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).searchBySquadNumber(99);
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
+ }
+
+ /*
+ * -------------------------------------------------------------------------
+ * HTTP PUT
+ * -------------------------------------------------------------------------
+ */
+
+ /**
+ * Given a player exists and valid update data is provided
+ * When PUT /players/{id} is called and the service successfully updates the player
+ * Then response status is 204 No Content
+ */
+ @Test
+ void put_playerExists_returnsNoContent()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ playerDTO.setId(1L); // Set ID for update operation
+ Long id = playerDTO.getId();
+ String body = objectMapper.writeValueAsString(playerDTO);
+ Mockito
+ .when(playersServiceMock.update(any(PlayerDTO.class)))
+ .thenReturn(true);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .put(PATH + "/{id}", id)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).update(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
+ }
+
+ /**
+ * Given a player with the provided ID does not exist
+ * When PUT /players/{id} is called and the service returns false
+ * Then response status is 404 Not Found
+ */
+ @Test
+ void put_playerNotFound_returnsNotFound()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ playerDTO.setId(999L); // Set ID for update operation
+ Long id = playerDTO.getId();
+ String body = objectMapper.writeValueAsString(playerDTO);
+ Mockito
+ .when(playersServiceMock.update(any(PlayerDTO.class)))
+ .thenReturn(false);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .put(PATH + "/{id}", id)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).update(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
+ }
+
+ /**
+ * Given invalid player data is provided (validation fails)
+ * When PUT /players/{id} is called
+ * Then response status is 400 Bad Request and service is never called
+ */
+ @Test
+ void put_invalidPlayer_returnsBadRequest()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneInvalid();
+ Long id = playerDTO.getId();
+ String body = objectMapper.writeValueAsString(playerDTO);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .put(PATH + "/{id}", id)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, never()).update(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+ }
+
+ /**
+ * Given the path ID does not match the body ID
+ * When PUT /players/{id} is called
+ * Then response status is 400 Bad Request and service is never called
+ */
+ @Test
+ void put_idMismatch_returnsBadRequest()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ playerDTO.setId(999L); // Body has different ID
+ Long pathId = 1L; // Path has different ID
+ String body = objectMapper.writeValueAsString(playerDTO);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .put(PATH + "/{id}", pathId)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, never()).update(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+ }
+
+ /**
+ * Given the body ID is null (ID only in path)
+ * When PUT /players/{id} is called
+ * Then the ID is set from the path and the update proceeds normally
+ */
+ @Test
+ void put_nullBodyId_setsIdFromPath()
+ throws Exception {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ playerDTO.setId(null); // Body has null ID
+ Long pathId = 1L;
+ String body = objectMapper.writeValueAsString(playerDTO);
+ Mockito
+ .when(playersServiceMock.update(any(PlayerDTO.class)))
+ .thenReturn(true);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .put(PATH + "/{id}", pathId)
+ .content(body)
+ .contentType(MediaType.APPLICATION_JSON);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).update(any(PlayerDTO.class));
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
+ }
+
+ /*
+ * -------------------------------------------------------------------------
+ * HTTP DELETE
+ * -------------------------------------------------------------------------
+ */
+
+ /**
+ * Given a player with ID 1 exists
+ * When DELETE /players/1 is called and the service successfully deletes the player
+ * Then response status is 204 No Content
+ */
+ @Test
+ void delete_playerExists_returnsNoContent()
+ throws Exception {
+ // Arrange
+ Long id = 1L;
+ Mockito
+ .when(playersServiceMock.delete(anyLong()))
+ .thenReturn(true);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .delete(PATH + "/{id}", id);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).delete(anyLong());
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
+ }
+
+ /**
+ * Given a player with ID 999 does not exist
+ * When DELETE /players/999 is called and the service returns false
+ * Then response status is 404 Not Found
+ */
+ @Test
+ void delete_playerNotFound_returnsNotFound()
+ throws Exception {
+ // Arrange
+ Long id = 999L;
+ Mockito
+ .when(playersServiceMock.delete(anyLong()))
+ .thenReturn(false);
+ MockHttpServletRequestBuilder request = MockMvcRequestBuilders
+ .delete(PATH + "/{id}", id);
+ // Act
+ MockHttpServletResponse response = application
+ .perform(request)
+ .andReturn()
+ .getResponse();
+ // Assert
+ verify(playersServiceMock, times(1)).delete(anyLong());
+ assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.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
deleted file mode 100644
index 8237184..0000000
--- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/BooksRepositoryTests.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package ar.com.nanotaboada.java.samples.spring.boot.test.repositories;
-
-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;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.cache.test.autoconfigure.AutoConfigureCache;
-import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
-
-import ar.com.nanotaboada.java.samples.spring.boot.models.Book;
-import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository;
-import ar.com.nanotaboada.java.samples.spring.boot.test.BookFakes;
-
-@DisplayName("Derived Query Methods on Repository")
-@DataJpaTest
-@AutoConfigureCache
-class BooksRepositoryTests {
-
- @Autowired
- private BooksRepository repository;
-
- @Test
- void givenFindByIsbn_whenISBNAlreadyExists_thenShouldReturnExistingBook() {
- // Arrange
- Book existing = BookFakes.createOneValid();
- repository.save(existing); // Exists
- // Act
- Optional actual = repository.findByIsbn(existing.getIsbn());
- // Assert
- assertTrue(actual.isPresent());
- assertThat(actual.get()).usingRecursiveComparison().isEqualTo(existing);
- }
-
- @Test
- void givenFindByIsbn_whenISBNDoesNotExist_thenShouldReturnEmptyOptional() {
- // Arrange
- Book expected = BookFakes.createOneValid();
- // Act
- Optional actual = repository.findByIsbn(expected.getIsbn());
- // 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/repositories/PlayersRepositoryTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/PlayersRepositoryTests.java
new file mode 100644
index 0000000..529a6cb
--- /dev/null
+++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/repositories/PlayersRepositoryTests.java
@@ -0,0 +1,114 @@
+package ar.com.nanotaboada.java.samples.spring.boot.test.repositories;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.cache.test.autoconfigure.AutoConfigureCache;
+import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
+import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
+import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase.Replace;
+
+import ar.com.nanotaboada.java.samples.spring.boot.models.Player;
+import ar.com.nanotaboada.java.samples.spring.boot.repositories.PlayersRepository;
+import ar.com.nanotaboada.java.samples.spring.boot.test.PlayerFakes;
+
+@DisplayName("Derived Query Methods on Repository")
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = Replace.NONE)
+@AutoConfigureCache
+class PlayersRepositoryTests {
+
+ @Autowired
+ private PlayersRepository repository;
+
+ /**
+ * Given Leandro Paredes is created and saved to the database with an auto-generated ID
+ * When findById() is called with that ID
+ * Then the player is returned
+ */
+ @Test
+ void findById_playerExists_returnsPlayer() {
+ // Arrange
+ Player leandro = PlayerFakes.createOneValid();
+ Player saved = repository.save(leandro);
+ // Act
+ Optional actual = repository.findById(saved.getId());
+ // Assert
+ assertThat(actual).isPresent();
+ assertThat(actual.get()).usingRecursiveComparison().isEqualTo(saved);
+ }
+
+ /**
+ * Given the database does not contain a player with ID 999
+ * When findById(999) is called
+ * Then an empty Optional is returned
+ */
+ @Test
+ void findById_playerNotFound_returnsEmpty() {
+ // Act
+ Optional actual = repository.findById(999L);
+ // Assert
+ assertThat(actual).isEmpty();
+ }
+
+ /**
+ * Given the database contains 7 players in Premier League (pre-seeded in-memory database)
+ * When findByLeagueContainingIgnoreCase("Premier") is called
+ * Then a list of matching players is returned
+ */
+ @Test
+ void findByLeague_matchingPlayersExist_returnsList() {
+ // Act
+ List actual = repository.findByLeagueContainingIgnoreCase("Premier");
+ // Assert
+ assertThat(actual).isNotEmpty()
+ .allMatch(player -> player.getLeague().toLowerCase().contains("premier"));
+ }
+
+ /**
+ * Given the database does not contain players in "nonexistentleague"
+ * When findByLeagueContainingIgnoreCase("nonexistentleague") is called
+ * Then an empty list is returned
+ */
+ @Test
+ void findByLeague_noMatches_returnsEmptyList() {
+ // Act
+ List actual = repository.findByLeagueContainingIgnoreCase("nonexistentleague");
+ // Assert
+ assertThat(actual).isEmpty();
+ }
+
+ /**
+ * Given the database contains Messi with squad number 10 (pre-seeded at ID 10)
+ * When findBySquadNumber(10) is called
+ * Then Messi's player record is returned
+ */
+ @Test
+ void findBySquadNumber_playerExists_returnsPlayer() {
+ // Act
+ Optional actual = repository.findBySquadNumber(10);
+ // Assert
+ assertThat(actual).isPresent();
+ assertThat(actual.get())
+ .extracting(Player::getSquadNumber, Player::getLastName)
+ .containsExactly(10, "Messi");
+ }
+
+ /**
+ * Given the database does not contain a player with squad number 99
+ * When findBySquadNumber(99) is called
+ * Then an empty Optional is returned
+ */
+ @Test
+ void findBySquadNumber_playerNotFound_returnsEmpty() {
+ // Act
+ Optional actual = repository.findBySquadNumber(99);
+ // Assert
+ assertThat(actual).isEmpty();
+ }
+}
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
deleted file mode 100644
index 930e213..0000000
--- a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/BooksServiceTests.java
+++ /dev/null
@@ -1,265 +0,0 @@
-package ar.com.nanotaboada.java.samples.spring.boot.test.services;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.modelmapper.ModelMapper;
-
-import ar.com.nanotaboada.java.samples.spring.boot.models.Book;
-import ar.com.nanotaboada.java.samples.spring.boot.models.BookDTO;
-import ar.com.nanotaboada.java.samples.spring.boot.repositories.BooksRepository;
-import ar.com.nanotaboada.java.samples.spring.boot.services.BooksService;
-import ar.com.nanotaboada.java.samples.spring.boot.test.BookDTOFakes;
-import ar.com.nanotaboada.java.samples.spring.boot.test.BookFakes;
-
-@DisplayName("CRUD Operations on Service")
-@ExtendWith(MockitoExtension.class)
-class BooksServiceTests {
-
- @Mock
- private BooksRepository booksRepositoryMock;
-
- @Mock
- private ModelMapper modelMapperMock;
-
- @InjectMocks
- private BooksService booksService;
-
- /*
- * -------------------------------------------------------------------------
- * Create
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenCreate_whenRepositoryExistsByIdReturnsTrue_thenRepositoryNeverSaveBookAndResultIsFalse() {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneInvalid();
- Mockito
- .when(booksRepositoryMock.existsById(anyString()))
- .thenReturn(true);
- // Act
- boolean result = booksService.create(bookDTO);
- // Assert
- verify(booksRepositoryMock, never()).save(any(Book.class));
- verify(modelMapperMock, never()).map(bookDTO, Book.class);
- assertThat(result).isFalse();
- }
-
- @Test
- void givenCreate_whenRepositoryExistsByIdReturnsFalse_thenRepositorySaveBookAndResultIsTrue() {
- // Arrange
- Book book = BookFakes.createOneValid();
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- Mockito
- .when(booksRepositoryMock.existsById(anyString()))
- .thenReturn(false);
- Mockito
- .when(modelMapperMock.map(bookDTO, Book.class))
- .thenReturn(book);
- // Act
- boolean result = booksService.create(bookDTO);
- // Assert
- verify(booksRepositoryMock, times(1)).save(any(Book.class));
- verify(modelMapperMock, times(1)).map(bookDTO, Book.class);
- assertThat(result).isTrue();
- }
-
- /*
- * -------------------------------------------------------------------------
- * Retrieve
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenRetrieveByIsbn_whenRepositoryFindByIdReturnsBook_thenResultIsEqualToBook() {
- // Arrange
- Book book = BookFakes.createOneValid();
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- Mockito
- .when(booksRepositoryMock.findByIsbn(anyString()))
- .thenReturn(Optional.of(book));
- Mockito
- .when(modelMapperMock.map(book, BookDTO.class))
- .thenReturn(bookDTO);
- // Act
- BookDTO result = booksService.retrieveByIsbn(bookDTO.getIsbn());
- // Assert
- verify(booksRepositoryMock, times(1)).findByIsbn(anyString());
- verify(modelMapperMock, times(1)).map(book, BookDTO.class);
- assertThat(result).usingRecursiveComparison().isEqualTo(bookDTO);
- }
-
- @Test
- void givenRetrieveByIsbn_whenRepositoryFindByIdReturnsEmpty_thenResultIsNull() {
- // Arrange
- String isbn = "9781484242216";
- Mockito
- .when(booksRepositoryMock.findByIsbn(anyString()))
- .thenReturn(Optional.empty());
- // Act
- BookDTO result = booksService.retrieveByIsbn(isbn);
- // Assert
- verify(booksRepositoryMock, times(1)).findByIsbn(anyString());
- verify(modelMapperMock, never()).map(any(Book.class), any(BookDTO.class));
- assertThat(result).isNull();
- }
-
- @Test
- void givenRetrieveAll_whenRepositoryFindAllReturnsBooks_thenResultIsEqualToBooks() {
- // Arrange
- List books = BookFakes.createManyValid();
- List bookDTOs = BookDTOFakes.createManyValid();
- Mockito
- .when(booksRepositoryMock.findAll())
- .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.retrieveAll();
- // Assert
- verify(booksRepositoryMock, times(1)).findAll();
- for (Book book : books) {
- verify(modelMapperMock, times(1)).map(book, BookDTO.class);
- }
- assertThat(result).usingRecursiveComparison().isEqualTo(bookDTOs);
- }
-
- /*
- * -------------------------------------------------------------------------
- * Update
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenUpdate_whenRepositoryExistsByIdReturnsTrue_thenRepositorySaveBookAndResultIsTrue() {
- // Arrange
- Book book = BookFakes.createOneValid();
- BookDTO bookDTO = BookDTOFakes.createOneValid();
-
- Mockito
- .when(booksRepositoryMock.existsById(anyString()))
- .thenReturn(true);
- Mockito
- .when(modelMapperMock.map(bookDTO, Book.class))
- .thenReturn(book);
- // Act
- boolean result = booksService.update(bookDTO);
- // Assert
- verify(booksRepositoryMock, times(1)).save(any(Book.class));
- verify(modelMapperMock, times(1)).map(bookDTO, Book.class);
- assertThat(result).isTrue();
- }
-
- @Test
- void givenUpdate_whenRepositoryExistsByIdReturnsFalse_thenRepositoryNeverSaveBookAndResultIsFalse() {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- Mockito
- .when(booksRepositoryMock.existsById(anyString()))
- .thenReturn(false);
- // Act
- boolean result = booksService.update(bookDTO);
- // Assert
- verify(booksRepositoryMock, never()).save(any(Book.class));
- verify(modelMapperMock, never()).map(bookDTO, Book.class);
- assertThat(result).isFalse();
- }
-
- /*
- * -------------------------------------------------------------------------
- * Delete
- * -------------------------------------------------------------------------
- */
-
- @Test
- void givenDelete_whenRepositoryExistsByIdReturnsFalse_thenRepositoryNeverDeleteBookAndResultIsFalse() {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneInvalid();
- Mockito
- .when(booksRepositoryMock.existsById(anyString()))
- .thenReturn(false);
- // Act
- boolean result = booksService.delete(bookDTO.getIsbn());
- // Assert
- verify(booksRepositoryMock, never()).deleteById(anyString());
- verify(modelMapperMock, never()).map(bookDTO, Book.class);
- assertThat(result).isFalse();
- }
-
- @Test
- void givenDelete_whenRepositoryExistsByIdReturnsTrue_thenRepositoryDeleteBookAndResultIsTrue() {
- // Arrange
- BookDTO bookDTO = BookDTOFakes.createOneValid();
- Mockito
- .when(booksRepositoryMock.existsById(anyString()))
- .thenReturn(true);
- // Act
- boolean result = booksService.delete(bookDTO.getIsbn());
- // Assert
- verify(booksRepositoryMock, times(1)).deleteById(anyString());
- 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();
- }
-}
diff --git a/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java
new file mode 100644
index 0000000..9a40917
--- /dev/null
+++ b/src/test/java/ar/com/nanotaboada/java/samples/spring/boot/test/services/PlayersServiceTests.java
@@ -0,0 +1,427 @@
+package ar.com.nanotaboada.java.samples.spring.boot.test.services;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.modelmapper.ModelMapper;
+import org.springframework.dao.DataIntegrityViolationException;
+
+import ar.com.nanotaboada.java.samples.spring.boot.models.Player;
+import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO;
+import ar.com.nanotaboada.java.samples.spring.boot.repositories.PlayersRepository;
+import ar.com.nanotaboada.java.samples.spring.boot.services.PlayersService;
+import ar.com.nanotaboada.java.samples.spring.boot.test.PlayerDTOFakes;
+import ar.com.nanotaboada.java.samples.spring.boot.test.PlayerFakes;
+
+@DisplayName("CRUD Operations on Service")
+@ExtendWith(MockitoExtension.class)
+class PlayersServiceTests {
+
+ @Mock
+ private PlayersRepository playersRepositoryMock;
+
+ @Mock
+ private ModelMapper modelMapperMock;
+
+ @InjectMocks
+ private PlayersService playersService;
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Create
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Given the repository does not contain a player with the same squad number
+ * When create() is called with valid player data
+ * Then the player is saved and a PlayerDTO is returned
+ */
+ @Test
+ void create_noConflict_returnsPlayerDTO() {
+ // Arrange
+ Player player = PlayerFakes.createOneValid();
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ Mockito
+ .when(playersRepositoryMock.findBySquadNumber(playerDTO.getSquadNumber()))
+ .thenReturn(Optional.empty()); // No conflict
+ Mockito
+ .when(modelMapperMock.map(playerDTO, Player.class))
+ .thenReturn(player);
+ Mockito
+ .when(playersRepositoryMock.save(any(Player.class)))
+ .thenReturn(player);
+ Mockito
+ .when(modelMapperMock.map(player, PlayerDTO.class))
+ .thenReturn(playerDTO);
+ // Act
+ PlayerDTO result = playersService.create(playerDTO);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findBySquadNumber(playerDTO.getSquadNumber());
+ verify(playersRepositoryMock, times(1)).save(any(Player.class));
+ verify(modelMapperMock, times(1)).map(playerDTO, Player.class);
+ verify(modelMapperMock, times(1)).map(player, PlayerDTO.class);
+ assertThat(result).isEqualTo(playerDTO);
+ }
+
+ /**
+ * Given the repository finds an existing player with the same squad number
+ * When create() is called
+ * Then null is returned (conflict detected)
+ */
+ @Test
+ void create_squadNumberExists_returnsNull() {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ Player existingPlayer = PlayerFakes.createAll().stream()
+ .filter(player -> player.getSquadNumber() == 10)
+ .findFirst()
+ .orElseThrow();
+ Mockito
+ .when(playersRepositoryMock.findBySquadNumber(playerDTO.getSquadNumber()))
+ .thenReturn(Optional.of(existingPlayer));
+ // Act
+ PlayerDTO result = playersService.create(playerDTO);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findBySquadNumber(playerDTO.getSquadNumber());
+ verify(playersRepositoryMock, never()).save(any(Player.class));
+ assertThat(result).isNull();
+ }
+
+ /**
+ * Given a race condition occurs where another request creates the same squad number
+ * When create() is called and save() throws DataIntegrityViolationException
+ * Then null is returned (conflict detected via exception)
+ */
+ @Test
+ void create_raceConditionOnSave_returnsNull() {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ Player player = PlayerFakes.createOneValid();
+ Mockito
+ .when(playersRepositoryMock.findBySquadNumber(playerDTO.getSquadNumber()))
+ .thenReturn(Optional.empty()); // No conflict initially
+ Mockito
+ .when(modelMapperMock.map(playerDTO, Player.class))
+ .thenReturn(player);
+ Mockito
+ .when(playersRepositoryMock.save(any(Player.class)))
+ .thenThrow(new DataIntegrityViolationException("Unique constraint violation"));
+ // Act
+ PlayerDTO result = playersService.create(playerDTO);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findBySquadNumber(playerDTO.getSquadNumber());
+ verify(playersRepositoryMock, times(1)).save(any(Player.class));
+ assertThat(result).isNull();
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Retrieve
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Given the repository finds an existing player with ID 1 (Damián Martínez)
+ * When retrieveById(1) is called
+ * Then the corresponding PlayerDTO is returned
+ */
+ @Test
+ void retrieveById_playerExists_returnsPlayerDTO() {
+ // Arrange
+ Player player = PlayerFakes.createOneForUpdate();
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneForUpdate();
+ Mockito
+ .when(playersRepositoryMock.findById(1L))
+ .thenReturn(Optional.of(player));
+ Mockito
+ .when(modelMapperMock.map(player, PlayerDTO.class))
+ .thenReturn(playerDTO);
+ // Act
+ PlayerDTO result = playersService.retrieveById(1L);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findById(1L);
+ verify(modelMapperMock, times(1)).map(player, PlayerDTO.class);
+ assertThat(result).isEqualTo(playerDTO);
+ }
+
+ /**
+ * Given the repository does not find a player with ID 999
+ * When retrieveById(999) is called
+ * Then null is returned
+ */
+ @Test
+ void retrieveById_playerNotFound_returnsNull() {
+ // Arrange
+ Mockito
+ .when(playersRepositoryMock.findById(anyLong()))
+ .thenReturn(Optional.empty());
+ // Act
+ PlayerDTO result = playersService.retrieveById(999L);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findById(anyLong());
+ verify(modelMapperMock, never()).map(any(Player.class), any());
+ assertThat(result).isNull();
+ }
+
+ /**
+ * Given the repository returns all 26 players
+ * When retrieveAll() is called
+ * Then a list of 26 PlayerDTOs is returned
+ */
+ @Test
+ void retrieveAll_playersExist_returnsListOfPlayerDTOs() {
+ // Arrange
+ List players = PlayerFakes.createAll();
+ List playerDTOs = PlayerDTOFakes.createAll();
+ Mockito
+ .when(playersRepositoryMock.findAll())
+ .thenReturn(players);
+ // Mock modelMapper to convert each player correctly
+ for (int i = 0; i < players.size(); i++) {
+ Mockito
+ .when(modelMapperMock.map(players.get(i), PlayerDTO.class))
+ .thenReturn(playerDTOs.get(i));
+ }
+ // Act
+ List result = playersService.retrieveAll();
+ // Assert
+ verify(playersRepositoryMock, times(1)).findAll();
+ assertThat(result).hasSize(26);
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Search
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Given the repository returns 7 players matching "Premier" league
+ * When searchByLeague("Premier") is called
+ * Then a list of 7 PlayerDTOs is returned
+ */
+ @Test
+ void searchByLeague_matchingPlayersExist_returnsListOfPlayerDTOs() {
+ // Arrange
+ List players = PlayerFakes.createAll().stream()
+ .filter(p -> p.getLeague().contains("Premier"))
+ .toList();
+ List playerDTOs = PlayerDTOFakes.createAll().stream()
+ .filter(p -> p.getLeague().contains("Premier"))
+ .toList();
+ Mockito
+ .when(playersRepositoryMock.findByLeagueContainingIgnoreCase(any()))
+ .thenReturn(players);
+ // Mock modelMapper to convert each player correctly
+ for (int i = 0; i < players.size(); i++) {
+ Mockito
+ .when(modelMapperMock.map(players.get(i), PlayerDTO.class))
+ .thenReturn(playerDTOs.get(i));
+ }
+ // Act
+ List result = playersService.searchByLeague("Premier");
+ // Assert
+ verify(playersRepositoryMock, times(1)).findByLeagueContainingIgnoreCase(any());
+ assertThat(result).hasSize(7);
+ }
+
+ /**
+ * Given the repository returns an empty list for "NonexistentLeague"
+ * When searchByLeague("NonexistentLeague") is called
+ * Then an empty list is returned
+ */
+ @Test
+ void searchByLeague_noMatches_returnsEmptyList() {
+ // Arrange
+ Mockito
+ .when(playersRepositoryMock.findByLeagueContainingIgnoreCase(any()))
+ .thenReturn(List.of());
+ // Act
+ List result = playersService.searchByLeague("NonexistentLeague");
+ // Assert
+ verify(playersRepositoryMock, times(1)).findByLeagueContainingIgnoreCase(any());
+ verify(modelMapperMock, never()).map(any(Player.class), any());
+ assertThat(result).isEmpty();
+ }
+
+ /**
+ * Given the repository finds Messi with squad number 10
+ * When searchBySquadNumber(10) is called
+ * Then the corresponding PlayerDTO is returned
+ */
+ @Test
+ void searchBySquadNumber_playerExists_returnsPlayerDTO() {
+ // Arrange
+ Player player = PlayerFakes.createAll().stream()
+ .filter(p -> p.getSquadNumber() == 10)
+ .findFirst()
+ .orElseThrow();
+ PlayerDTO playerDTO = PlayerDTOFakes.createAll().stream()
+ .filter(p -> p.getSquadNumber() == 10)
+ .findFirst()
+ .orElseThrow();
+ Mockito
+ .when(playersRepositoryMock.findBySquadNumber(10))
+ .thenReturn(Optional.of(player));
+ Mockito
+ .when(modelMapperMock.map(player, PlayerDTO.class))
+ .thenReturn(playerDTO);
+ // Act
+ PlayerDTO result = playersService.searchBySquadNumber(10);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findBySquadNumber(10);
+ verify(modelMapperMock, times(1)).map(player, PlayerDTO.class);
+ assertThat(result).isEqualTo(playerDTO);
+ assertThat(result.getSquadNumber()).isEqualTo(10);
+ }
+
+ /**
+ * Given the repository does not find a player with squad number 99
+ * When searchBySquadNumber(99) is called
+ * Then null is returned
+ */
+ @Test
+ void searchBySquadNumber_playerNotFound_returnsNull() {
+ // Arrange
+ Mockito
+ .when(playersRepositoryMock.findBySquadNumber(99))
+ .thenReturn(Optional.empty());
+ // Act
+ PlayerDTO result = playersService.searchBySquadNumber(99);
+ // Assert
+ verify(playersRepositoryMock, times(1)).findBySquadNumber(99);
+ verify(modelMapperMock, never()).map(any(Player.class), any());
+ assertThat(result).isNull();
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Update
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Given the repository confirms player with ID 1 exists
+ * When update() is called with updated data (Damián→Emiliano Martínez)
+ * Then the player is saved and true is returned
+ */
+ @Test
+ void update_playerExists_savesAndReturnsTrue() {
+ // Arrange
+ Player playerUpdated = PlayerFakes.createOneUpdated();
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneUpdated();
+ Mockito
+ .when(playersRepositoryMock.existsById(1L))
+ .thenReturn(true);
+ Mockito
+ .when(modelMapperMock.map(playerDTO, Player.class))
+ .thenReturn(playerUpdated);
+ // Act
+ boolean result = playersService.update(playerDTO);
+ // Assert
+ verify(playersRepositoryMock, times(1)).existsById(1L);
+ verify(playersRepositoryMock, times(1)).save(any(Player.class));
+ verify(modelMapperMock, times(1)).map(playerDTO, Player.class);
+ assertThat(result).isTrue();
+ }
+
+ /**
+ * Given the repository confirms player with ID 999 does not exist
+ * When update() is called
+ * Then false is returned without saving
+ */
+ @Test
+ void update_playerNotFound_returnsFalse() {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ playerDTO.setId(999L);
+ Mockito
+ .when(playersRepositoryMock.existsById(999L))
+ .thenReturn(false);
+ // Act
+ boolean result = playersService.update(playerDTO);
+ // Assert
+ verify(playersRepositoryMock, times(1)).existsById(999L);
+ verify(playersRepositoryMock, never()).save(any(Player.class));
+ verify(modelMapperMock, never()).map(playerDTO, Player.class);
+ assertThat(result).isFalse();
+ }
+
+ /**
+ * Given a PlayerDTO has null ID
+ * When update() is called
+ * Then false is returned without checking repository or saving
+ */
+ @Test
+ void update_nullId_returnsFalse() {
+ // Arrange
+ PlayerDTO playerDTO = PlayerDTOFakes.createOneValid();
+ playerDTO.setId(null);
+ // Act
+ boolean result = playersService.update(playerDTO);
+ // Assert
+ verify(playersRepositoryMock, never()).existsById(any());
+ verify(playersRepositoryMock, never()).save(any(Player.class));
+ verify(modelMapperMock, never()).map(any(), any());
+ assertThat(result).isFalse();
+ }
+
+ /*
+ * -----------------------------------------------------------------------------------------------------------------------
+ * Delete
+ * -----------------------------------------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Given the repository confirms player with ID 21 exists (Alejandro Gómez)
+ * When delete(21) is called
+ * Then the player is deleted and true is returned
+ */
+ @Test
+ void delete_playerExists_deletesAndReturnsTrue() {
+ // Arrange
+ Mockito
+ .when(playersRepositoryMock.existsById(21L))
+ .thenReturn(true);
+ // Act
+ boolean result = playersService.delete(21L);
+ // Assert
+ verify(playersRepositoryMock, times(1)).existsById(21L);
+ verify(playersRepositoryMock, times(1)).deleteById(21L);
+ assertThat(result).isTrue();
+ }
+
+ /**
+ * Given the repository confirms player with ID 999 does not exist
+ * When delete(999) is called
+ * Then false is returned without deleting
+ */
+ @Test
+ void delete_playerNotFound_returnsFalse() {
+ // Arrange
+ Mockito
+ .when(playersRepositoryMock.existsById(999L))
+ .thenReturn(false);
+ // Act
+ boolean result = playersService.delete(999L);
+ // Assert
+ verify(playersRepositoryMock, times(1)).existsById(999L);
+ verify(playersRepositoryMock, never()).deleteById(anyLong());
+ assertThat(result).isFalse();
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 4cdc0f5..7e0c762 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,11 +1,17 @@
# Test Configuration
-# Uses H2 in-memory database for fast test execution
+# Uses SQLite in-memory database for fast test execution
+# Maintains compatibility with production (same database engine)
+# Database is initialized with ddl.sql and dml.sql (all 26 players except Leandro Paredes)
-# H2 Database Configuration (Test Only)
-spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
-spring.datasource.driver-class-name=org.h2.Driver
-spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
-spring.jpa.hibernate.ddl-auto=create-drop
+# SQLite In-Memory Database Configuration (Test Only)
+spring.datasource.url=jdbc:sqlite::memory:
+spring.datasource.driver-class-name=org.sqlite.JDBC
+spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
+spring.jpa.hibernate.ddl-auto=none
+spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+spring.sql.init.mode=always
+spring.sql.init.schema-locations=classpath:ddl.sql
+spring.sql.init.data-locations=classpath:dml.sql
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true
diff --git a/src/test/resources/ddl.sql b/src/test/resources/ddl.sql
new file mode 100644
index 0000000..ed4ac22
--- /dev/null
+++ b/src/test/resources/ddl.sql
@@ -0,0 +1,19 @@
+-- Test Database Schema (DDL - Data Definition Language)
+-- SQLite in-memory database for testing
+-- Matches production schema exactly (compatibility guaranteed)
+
+DROP TABLE IF EXISTS players;
+
+CREATE TABLE players (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ firstName TEXT NOT NULL,
+ middleName TEXT,
+ lastName TEXT NOT NULL,
+ dateOfBirth TEXT NOT NULL,
+ squadNumber INTEGER NOT NULL UNIQUE,
+ position TEXT NOT NULL,
+ abbrPosition TEXT NOT NULL,
+ team TEXT NOT NULL,
+ league TEXT NOT NULL,
+ starting11 INTEGER NOT NULL
+);
diff --git a/src/test/resources/dml.sql b/src/test/resources/dml.sql
new file mode 100644
index 0000000..13f8558
--- /dev/null
+++ b/src/test/resources/dml.sql
@@ -0,0 +1,37 @@
+-- Test Database Data (DML - Data Manipulation Language)
+-- Contains all 26 players from production database EXCEPT Leandro Paredes (ID 19)
+-- Leandro Paredes will be created during tests (for POST/create operations)
+-- Damián Emiliano Martínez (ID 1) will be updated during tests
+-- Alejandro Gómez (ID 21) will be deleted during tests
+
+-- Starting 11 (IDs 1-11, excluding 19 if applicable)
+INSERT INTO players (id, firstName, middleName, lastName, dateOfBirth, squadNumber, position, abbrPosition, team, league, starting11) VALUES
+(1, 'Damián', 'Emiliano', 'Martínez', '1992-09-02T00:00:00.000Z', 23, 'Goalkeeper', 'GK', 'Aston Villa FC', 'Premier League', 1),
+(2, 'Nahuel', NULL, 'Molina', '1998-04-06T00:00:00.000Z', 26, 'Right-Back', 'RB', 'Atlético Madrid', 'La Liga', 1),
+(3, 'Cristian', 'Gabriel', 'Romero', '1998-04-27T00:00:00.000Z', 13, 'Centre-Back', 'CB', 'Tottenham Hotspur', 'Premier League', 1),
+(4, 'Nicolás', 'Hernán Gonzalo', 'Otamendi', '1988-02-12T00:00:00.000Z', 19, 'Centre-Back', 'CB', 'SL Benfica', 'Liga Portugal', 1),
+(5, 'Nicolás', 'Alejandro', 'Tagliafico', '1992-08-31T00:00:00.000Z', 3, 'Left-Back', 'LB', 'Olympique Lyon', 'Ligue 1', 1),
+(6, 'Ángel', 'Fabián', 'Di María', '1988-02-14T00:00:00.000Z', 11, 'Right Winger', 'RW', 'SL Benfica', 'Liga Portugal', 1),
+(7, 'Rodrigo', 'Javier', 'de Paul', '1994-05-24T00:00:00.000Z', 7, 'Central Midfield', 'CM', 'Atlético Madrid', 'La Liga', 1),
+(8, 'Enzo', 'Jeremías', 'Fernández', '2001-01-17T00:00:00.000Z', 24, 'Central Midfield', 'CM', 'Chelsea FC', 'Premier League', 1),
+(9, 'Alexis', NULL, 'Mac Allister', '1998-12-24T00:00:00.000Z', 20, 'Central Midfield', 'CM', 'Liverpool FC', 'Premier League', 1),
+(10, 'Lionel', 'Andrés', 'Messi', '1987-06-24T00:00:00.000Z', 10, 'Right Winger', 'RW', 'Inter Miami CF', 'Major League Soccer', 1),
+(11, 'Julián', NULL, 'Álvarez', '2000-01-31T00:00:00.000Z', 9, 'Centre-Forward', 'CF', 'Manchester City', 'Premier League', 1);
+
+-- Substitutes (IDs 12-26, excluding 19)
+INSERT INTO players (id, firstName, middleName, lastName, dateOfBirth, squadNumber, position, abbrPosition, team, league, starting11) VALUES
+(12, 'Franco', 'Daniel', 'Armani', '1986-10-16T00:00:00.000Z', 1, 'Goalkeeper', 'GK', 'River Plate', 'Copa de la Liga', 0),
+(13, 'Gerónimo', NULL, 'Rulli', '1992-05-20T00:00:00.000Z', 12, 'Goalkeeper', 'GK', 'Ajax Amsterdam', 'Eredivisie', 0),
+(14, 'Juan', 'Marcos', 'Foyth', '1998-01-12T00:00:00.000Z', 2, 'Right-Back', 'RB', 'Villarreal', 'La Liga', 0),
+(15, 'Gonzalo', 'Ariel', 'Montiel', '1997-01-01T00:00:00.000Z', 4, 'Right-Back', 'RB', 'Nottingham Forest', 'Premier League', 0),
+(16, 'Germán', 'Alejo', 'Pezzella', '1991-06-27T00:00:00.000Z', 6, 'Centre-Back', 'CB', 'Real Betis Balompié', 'La Liga', 0),
+(17, 'Marcos', 'Javier', 'Acuña', '1991-10-28T00:00:00.000Z', 8, 'Left-Back', 'LB', 'Sevilla FC', 'La Liga', 0),
+(18, 'Lisandro', NULL, 'Martínez', '1998-01-18T00:00:00.000Z', 25, 'Centre-Back', 'CB', 'Manchester United', 'Premier League', 0),
+-- ID 19 (Leandro Paredes) intentionally skipped - will be created during tests
+(20, 'Exequiel', 'Alejandro', 'Palacios', '1998-10-05T00:00:00.000Z', 14, 'Central Midfield', 'CM', 'Bayer 04 Leverkusen', 'Bundesliga', 0),
+(21, 'Alejandro', 'Darío', 'Gómez', '1988-02-15T00:00:00.000Z', 17, 'Left Winger', 'LW', 'AC Monza', 'Serie A', 0),
+(22, 'Guido', NULL, 'Rodríguez', '1994-04-12T00:00:00.000Z', 18, 'Defensive Midfield', 'DM', 'Real Betis Balompié', 'La Liga', 0),
+(23, 'Ángel', 'Martín', 'Correa', '1995-03-09T00:00:00.000Z', 15, 'Right Winger', 'RW', 'Atlético Madrid', 'La Liga', 0),
+(24, 'Thiago', 'Ezequiel', 'Almada', '2001-04-26T00:00:00.000Z', 16, 'Attacking Midfield', 'AM', 'Atlanta United FC', 'Major League Soccer', 0),
+(25, 'Paulo', 'Exequiel', 'Dybala', '1993-11-15T00:00:00.000Z', 21, 'Second Striker', 'SS', 'AS Roma', 'Serie A', 0),
+(26, 'Lautaro', 'Javier', 'Martínez', '1997-08-22T00:00:00.000Z', 22, 'Centre-Forward', 'CF', 'Inter Milan', 'Serie A', 0);
diff --git a/storage/books-sqlite3.db b/storage/books-sqlite3.db
deleted file mode 100644
index b138738..0000000
Binary files a/storage/books-sqlite3.db and /dev/null differ
diff --git a/storage/players-sqlite3.db b/storage/players-sqlite3.db
new file mode 100644
index 0000000..14a1d8e
Binary files /dev/null and b/storage/players-sqlite3.db differ