diff --git a/.claude/commands/pre-commit.md b/.claude/commands/pre-commit.md index f856769..4a5cad0 100644 --- a/.claude/commands/pre-commit.md +++ b/.claude/commands/pre-commit.md @@ -3,13 +3,12 @@ Before running the checklist, run `git fetch origin`. If the current branch is b Run the pre-commit checklist for this project: 1. Update `CHANGELOG.md` `[Unreleased]` section — add an entry under the appropriate subsection (Added / Changed / Fixed / Removed) describing the changes made, referencing the issue number. -2. Run `./mvnw clean install` — must succeed with no compilation warnings and all tests passing. -3. Remind me to open `target/site/jacoco/index.html` to verify coverage after the build completes. -4. If Docker is running, run `docker compose build` — must succeed with no +2. Run `./mvnw clean install` — must succeed with no compilation warnings, all tests passing, and the JaCoCo check reporting `All coverage checks have been met.` (80% instruction and branch coverage enforced by the build). +3. If Docker is running, run `docker compose build` — must succeed with no errors. Skip this step with a note if Docker Desktop is not running. -5. If `coderabbit` CLI is installed, run `coderabbit review --type uncommitted --prompt-only`: +4. If `coderabbit` CLI is installed, run `coderabbit review --type uncommitted --prompt-only`: - If actionable/serious findings are reported, stop and address them before proposing the commit. - If only nitpick-level findings, report them and continue to the commit proposal. - If `coderabbit` is not installed, skip this step with a note. -Run steps 1–4, report the results clearly, then run step 5 (CodeRabbit review) if available, then propose a branch name and commit message for my approval using the format `type(scope): description (#issue)` (max 80 chars; types: `feat` `fix` `chore` `docs` `test` `refactor` `ci` `perf`). Do not create the branch or commit until I explicitly confirm. +Run steps 1–3, report the results clearly, then run step 4 (CodeRabbit review) if available, then propose a branch name and commit message for my approval using the format `type(scope): description (#issue)` (max 80 chars; types: `feat` `fix` `chore` `docs` `test` `refactor` `ci` `perf`). Do not create the branch or commit until I explicitly confirm. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0627af7..c5ad73f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,23 @@ Release names follow the **historic football clubs** naming convention (A–Z): ## [Unreleased] +### Changed + +- `Player` entity: `id` demoted from `@Id Long` to surrogate `UUID` (non-PK, + `updatable=false`, `unique`, generated via `@PrePersist`); `squadNumber` + promoted to `@Id Integer` (natural key) (#268) +- `PlayerDTO`: `id` type changed from `Long` to `UUID` (#268) +- `PlayersRepository`: keyed on `Integer` (squad number as PK); added + `findById(UUID)` overload for admin/internal lookup (#268) +- `PlayersService`: `retrieveById` now accepts `UUID`; `update` keyed on + `Integer squadNumber`; `delete` renamed to `deleteBySquadNumber(Integer)` (#268) +- `PUT /players/{squadNumber}` and `DELETE /players/{squadNumber}`: path + variable changed from `Long id` to `Integer squadNumber` (#268) +- `GET /players/{id}`: path variable changed from `Long` to `UUID` (admin use) (#268) +- `storage/players-sqlite3.db`: schema migrated to `id VARCHAR(36) NOT NULL UNIQUE`, + `squadNumber INTEGER PRIMARY KEY`; 25 players preserved with generated UUIDs (#268) +- `ddl.sql` and `dml.sql`: test schema and seed data updated for new structure (#268) + ### Added - `.sonarcloud.properties`: SonarCloud Automatic Analysis configuration — diff --git a/pom.xml b/pom.xml index 122e6f0..2b5ba25 100644 --- a/pom.xml +++ b/pom.xml @@ -310,6 +310,31 @@ report + + check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.80 + + + BRANCH + COVEREDRATIO + 0.80 + + + + + + diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java index 90f6418..979c218 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java @@ -4,6 +4,7 @@ import java.net.URI; import java.util.List; +import java.util.UUID; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -37,12 +38,12 @@ *

Base Path:

* * *

Response Codes:

@@ -74,12 +75,8 @@ public class PlayersController { /** * Creates a new player resource. *

- * Validates the request body and creates a new player in the database. Returns a 201 Created response with a Location - * header pointing to the new resource. - *

- *

- * Conflict Detection: If a player with the same squad number already exists, returns 409 Conflict. - * Squad numbers must be unique (jersey numbers like Messi's #10). + * Validates the request body and creates a new player. Returns 201 Created with a Location + * header pointing to the new resource (UUID-based path). *

* * @param playerDTO the player data to create (validated with JSR-380 constraints) @@ -115,9 +112,6 @@ public ResponseEntity post(@RequestBody @Valid PlayerDTO playerDTO) { /** * Retrieves all players in the squad. - *

- * Returns the complete Argentina 2022 FIFA World Cup squad (26 players). - *

* * @return 200 OK with array of all players (empty array if none found) */ @@ -132,18 +126,18 @@ public ResponseEntity> getAll() { } /** - * Retrieves a single player by their unique identifier. + * Retrieves a single player by their surrogate UUID (admin/internal use only). * - * @param id the unique identifier of the player + * @param id the UUID surrogate key of the player * @return 200 OK with player data, or 404 Not Found if player doesn't exist */ @GetMapping("/players/{id}") - @Operation(summary = "Retrieves a player by ID") + @Operation(summary = "Retrieves a player by UUID (admin/internal use)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO.class))), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) - public ResponseEntity getById(@PathVariable Long id) { + public ResponseEntity getById(@PathVariable UUID id) { PlayerDTO playerDTO = playersService.retrieveById(id); return (playerDTO != null) ? ResponseEntity.status(HttpStatus.OK).body(playerDTO) @@ -151,9 +145,8 @@ public ResponseEntity getById(@PathVariable Long id) { } /** - * Retrieves a player by their squad number (unique identifier). + * Retrieves a player by their squad number. *

- * Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup similar to getById(). * Example: {@code /players/squadnumber/10} returns Lionel Messi *

* @@ -199,30 +192,29 @@ public ResponseEntity> searchByLeague(@PathVariable String leagu */ /** - * Updates an existing player resource (full update). + * Updates an existing player resource (full update) identified by squad number. *

- * Performs a complete replacement of the player entity. The ID in the path must match the ID in the request body. + * Performs a complete replacement of the player entity. The squad number in the path + * must match the squad number in the request body (if provided). *

* - * @param id the unique identifier of the player to update + * @param squadNumber the squad number (natural key) of the player to update * @param playerDTO the complete player data (must pass validation) - * @return 204 No Content if successful, 404 Not Found if player doesn't exist, or 400 Bad Request if validation fails or - * ID mismatch + * @return 204 No Content if successful, 404 Not Found if player doesn't exist, or 400 Bad Request if validation fails */ - @PutMapping("/players/{id}") - @Operation(summary = "Updates (entirely) a player by ID") + @PutMapping("/players/{squadNumber}") + @Operation(summary = "Updates (entirely) a player by squad number") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "No Content", content = @Content), @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) - public ResponseEntity put(@PathVariable Long id, @RequestBody @Valid PlayerDTO playerDTO) { - // Ensure path ID matches body ID - if (playerDTO.getId() != null && !playerDTO.getId().equals(id)) { + public ResponseEntity put(@PathVariable Integer squadNumber, @RequestBody @Valid PlayerDTO playerDTO) { + if (playerDTO.getSquadNumber() != null && !playerDTO.getSquadNumber().equals(squadNumber)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } - playerDTO.setId(id); // Set ID from path to ensure consistency - boolean updated = playersService.update(playerDTO); + playerDTO.setSquadNumber(squadNumber); + boolean updated = playersService.update(squadNumber, playerDTO); return (updated) ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); @@ -235,19 +227,19 @@ public ResponseEntity put(@PathVariable Long id, @RequestBody @Valid Playe */ /** - * Deletes a player resource by their unique identifier. + * Deletes a player resource by their squad number. * - * @param id the unique identifier of the player to delete + * @param squadNumber the squad number of the player to delete * @return 204 No Content if successful, or 404 Not Found if player doesn't exist */ - @DeleteMapping("/players/{id}") - @Operation(summary = "Deletes a player by ID") + @DeleteMapping("/players/{squadNumber}") + @Operation(summary = "Deletes a player by squad number") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "No Content", content = @Content), @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) }) - public ResponseEntity delete(@PathVariable Long id) { - boolean deleted = playersService.delete(id); + public ResponseEntity delete(@PathVariable Integer squadNumber) { + boolean deleted = playersService.deleteBySquadNumber(squadNumber); return (deleted) ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/Player.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/Player.java index 1a47dbb..89400e1 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/Player.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/Player.java @@ -1,6 +1,7 @@ package ar.com.nanotaboada.java.samples.spring.boot.models; import java.time.LocalDate; +import java.util.UUID; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -28,9 +29,9 @@ * *

Key Features:

*
    - *
  • Auto-generated ID using IDENTITY strategy
  • - *
  • ISO-8601 date storage for SQLite compatibility - * ({@link IsoDateConverter})
  • + *
  • UUID primary key — generated at application level via {@code GenerationType.UUID}
  • + *
  • Squad number natural key — unique domain identifier, used as path variable for mutations
  • + *
  • ISO-8601 date storage for SQLite compatibility ({@link IsoDateConverter})
  • *
  • JSON serialization support for LocalDate fields
  • *
* @@ -44,9 +45,22 @@ @NoArgsConstructor @AllArgsConstructor public class Player { + + /** + * Primary key — UUID generated at application level. + */ @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id", nullable = false, updatable = false, columnDefinition = "VARCHAR(36)") + private UUID id; + + /** + * Natural key — unique domain identifier, path variable for PUT and DELETE. + * Squad number (jersey number) is unique per team and stable. + */ + @Column(name = "squadNumber", nullable = false, unique = true, updatable = false) + private Integer squadNumber; + private String firstName; private String middleName; private String lastName; @@ -59,16 +73,6 @@ public class Player { @Convert(converter = IsoDateConverter.class) private LocalDate dateOfBirth; - /** - * Squad number (jersey number) - unique natural key. - *

- * Used for player lookups via /players/search/squadnumber/{squadNumber}. - * Database constraint enforces uniqueness. - *

- */ - @Column(unique = true) - private Integer squadNumber; - private String position; private String abbrPosition; private String team; diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerDTO.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerDTO.java index 40fede9..00181e8 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerDTO.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerDTO.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import java.util.UUID; + import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Past; @@ -46,7 +48,7 @@ */ @Data public class PlayerDTO { - private Long id; + private UUID id; @NotBlank private String firstName; private String middleName; diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java index 34a281b..35f98a6 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -12,56 +13,39 @@ * Spring Data JPA Repository for {@link Player} entities. *

* Provides data access methods for the {@code players} table using Spring Data's repository abstraction. - * Extends {@link JpaRepository} for CRUD operations, batch operations, and query methods. + * Extends {@link JpaRepository} for CRUD operations keyed on the UUID primary key. *

* *

Provided Methods:

*
    - *
  • Inherited from JpaRepository: save, findAll (returns List), findById, delete, flush, etc.
  • - *
  • Custom Query Methods: League search with case-insensitive wildcard matching
  • - *
  • Derived Queries: findBySquadNumber (method name conventions)
  • - *
- * - *

Query Strategies:

- *
    - *
  • Derived Queries: Spring Data derives queries from method names (findBySquadNumber, - * findByLeagueContainingIgnoreCase)
  • + *
  • Inherited from JpaRepository: save, findAll, findById(UUID), existsById, deleteById, etc.
  • + *
  • Derived Queries: findBySquadNumber, findByLeagueContainingIgnoreCase
  • *
* * @see Player * @see org.springframework.data.jpa.repository.JpaRepository - * @see Query - * Creation from Method Names * @since 4.0.2025 */ @Repository -public interface PlayersRepository extends JpaRepository { +public interface PlayersRepository extends JpaRepository { /** * Finds a player by their squad number (exact match). *

- * This is a derived query method - Spring Data JPA generates the query automatically. - * Squad numbers are jersey numbers that users recognize (e.g., Messi is #10). - * This demonstrates Spring Data's method name query derivation with a natural key. + * Squad numbers are unique jersey numbers (e.g., Messi is #10). + * Used as the natural key for mutation endpoints (PUT, DELETE). *

* - * @param squadNumber the squad number to search for (jersey number, typically - * 1-99) + * @param squadNumber the squad number to search for (jersey number, typically 1-99) * @return an Optional containing the player if found, empty Optional otherwise */ Optional findBySquadNumber(Integer squadNumber); /** * Finds players by league name using case-insensitive wildcard matching. - *

- * This method uses Spring Data's derived query mechanism to perform partial matching. - * For example, searching for "Premier" will match "Premier League". - *

* * @param league the league name to search for (partial matches allowed) - * @return a list of players whose league name contains the search term (empty - * list if none found) + * @return a list of players whose league name contains the search term */ List findByLeagueContainingIgnoreCase(String league); } 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 index 763b501..e9c3fb7 100644 --- 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 @@ -1,6 +1,7 @@ package ar.com.nanotaboada.java.samples.spring.boot.services; import java.util.List; +import java.util.UUID; import org.modelmapper.ModelMapper; import org.springframework.cache.annotation.CacheEvict; @@ -65,29 +66,26 @@ public class PlayersService { /** * 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. + * Converts the PlayerDTO to a Player entity, persists it (UUID generated via + * {@code GenerationType.UUID}), and returns the saved player with its assigned UUID. *

*

- * 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. + * Conflict Detection: Checks if a player with the same squad number already exists. + * 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 + * @return the created player with generated UUID, or null if squad number already exists */ @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 + return null; } try { @@ -97,8 +95,6 @@ public PlayerDTO create(PlayerDTO playerDTO) { 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; } @@ -112,13 +108,8 @@ public PlayerDTO create(PlayerDTO playerDTO) { /** * 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() { @@ -129,34 +120,29 @@ public List retrieveAll() { } /** - * Retrieves a player by their unique identifier. + * Retrieves a player by their UUID primary key. *

- * 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. + * Uses caching to improve performance. Null results are not cached. *

* - * @param id the unique identifier of the player (must not be null) + * @param id the UUID primary key (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) { + public PlayerDTO retrieveById(UUID id) { return playersRepository.findById(id) .map(this::mapFrom) .orElse(null); } /** - * Retrieves a player by their squad number (unique identifier). + * Retrieves a player by their squad number. *

- * Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup by unique identifier, - * similar to retrieveById(). Results are cached to improve performance. + * Squad numbers are unique jersey numbers (e.g., Messi is #10). Results are cached. *

* * @param squadNumber the squad number to retrieve (jersey number, typically 1-99) * @return the player DTO if found, null otherwise - * @see org.springframework.cache.annotation.Cacheable */ @Cacheable(value = "players", key = "'squad-' + #squadNumber", unless = "#result == null") public PlayerDTO retrieveBySquadNumber(Integer squadNumber) { @@ -173,10 +159,6 @@ public PlayerDTO retrieveBySquadNumber(Integer squadNumber) { /** * 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) @@ -195,30 +177,33 @@ public List searchByLeague(String league) { */ /** - * Updates an existing player's information. + * Updates an existing player identified by their squad number. *

- * 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. + * Looks up the existing player by squad number to retrieve the UUID primary key, + * maps the DTO to an entity, preserves the UUID, and saves. Returns false if not found. *

* - * @param playerDTO the player data to update (must include a valid ID) + * @param squadNumber the squad number (natural key) of the player to update + * @param playerDTO the player data to update * @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()); + public boolean update(Integer squadNumber, PlayerDTO playerDTO) { + log.debug("Updating player with squad number: {}", squadNumber); - 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; - } + return playersRepository.findBySquadNumber(squadNumber) + .map(existing -> { + Player player = mapFrom(playerDTO); + player.setId(existing.getId()); + playersRepository.save(player); + log.info("Player updated successfully - Squad Number: {}", squadNumber); + return true; + }) + .orElseGet(() -> { + log.warn("Cannot update player - squad number {} not found", squadNumber); + return false; + }); } /* @@ -228,29 +213,30 @@ public boolean update(PlayerDTO playerDTO) { */ /** - * Deletes a player by their unique identifier. + * Deletes a player by their squad number. *

- * 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. + * Looks up the player by squad number to retrieve the UUID primary key, then deletes by UUID. + * Returns false if the player doesn't exist. *

* - * @param id the unique identifier of the player to delete (must not be null) + * @param squadNumber the squad number 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); + public boolean deleteBySquadNumber(Integer squadNumber) { + log.debug("Deleting player with squad number: {}", squadNumber); - 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; - } + return playersRepository.findBySquadNumber(squadNumber) + .map(existing -> { + playersRepository.deleteById(existing.getId()); + log.info("Player deleted successfully - Squad Number: {}", squadNumber); + return true; + }) + .orElseGet(() -> { + log.warn("Cannot delete player - squad number {} not found", squadNumber); + return false; + }); } private PlayerDTO mapFrom(Player player) { 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 index 8cadd71..195300f 100644 --- 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 @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.util.Arrays; import java.util.List; +import java.util.UUID; import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO; @@ -12,6 +13,10 @@ private PlayerDTOFakes() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } + private static UUID uuid(int index) { + return UUID.fromString(String.format("00000000-0000-0000-0000-%012d", index)); + } + /** * Leandro Paredes - Test data for CREATE operations * @@ -19,16 +24,16 @@ private PlayerDTOFakes() { * - 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) + * Note: Not pre-seeded in test DB (squad number 5 slot is empty) */ public static PlayerDTO createOneValid() { PlayerDTO playerDTO = new PlayerDTO(); - playerDTO.setId(null); // Will be auto-generated as 19 + playerDTO.setId(null); // Will be generated by @PrePersist + playerDTO.setSquadNumber(5); 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"); @@ -38,22 +43,20 @@ public static PlayerDTO createOneValid() { } /** - * Damián Martínez - Player ID 1 BEFORE update + * Damián Martínez - Player squad number 23 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) + * - Service tests: Mock expected data for playersService.retrieveById(uuid) + * - Controller tests: Mock expected data for GET /players/{id} */ public static PlayerDTO createOneForUpdate() { PlayerDTO playerDTO = new PlayerDTO(); - playerDTO.setId(1L); + playerDTO.setId(uuid(1)); + playerDTO.setSquadNumber(23); 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"); @@ -63,11 +66,7 @@ public static PlayerDTO createOneForUpdate() { } /** - * 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 + * Emiliano Martínez - Expected result AFTER updating player squad number 23 * * Update changes: * - firstName: "Damián" → "Emiliano" @@ -75,12 +74,12 @@ public static PlayerDTO createOneForUpdate() { */ public static PlayerDTO createOneUpdated() { PlayerDTO playerDTO = new PlayerDTO(); - playerDTO.setId(1L); + playerDTO.setId(uuid(1)); + playerDTO.setSquadNumber(23); 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"); @@ -92,20 +91,16 @@ public static PlayerDTO createOneUpdated() { /** * 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.setId(null); + playerDTO.setSquadNumber(-1); // Invalid (must be positive) 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) @@ -116,87 +111,50 @@ public static PlayerDTO createOneInvalid() { /** * 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), + createPlayerDTO(uuid(1), 23, "Damián", "Emiliano", "Martínez", LocalDate.of(1992, 9, 2), "Goalkeeper", "GK", "Aston Villa FC", "Premier League", true), + createPlayerDTO(uuid(2), 26, "Nahuel", null, "Molina", LocalDate.of(1998, 4, 6), "Right-Back", "RB", "Atlético Madrid", "La Liga", true), + createPlayerDTO(uuid(3), 13, "Cristian", "Gabriel", "Romero", LocalDate.of(1998, 4, 27), "Centre-Back", "CB", "Tottenham Hotspur", "Premier League", true), + createPlayerDTO(uuid(4), 19, "Nicolás", "Hernán Gonzalo", "Otamendi", LocalDate.of(1988, 2, 12), "Centre-Back", "CB", "SL Benfica", "Liga Portugal", true), + createPlayerDTO(uuid(5), 3, "Nicolás", "Alejandro", "Tagliafico", LocalDate.of(1992, 8, 31), "Left-Back", "LB", "Olympique Lyon", "Ligue 1", true), + createPlayerDTO(uuid(6), 11, "Ángel", "Fabián", "Di María", LocalDate.of(1988, 2, 14), "Right Winger", "RW", "SL Benfica", "Liga Portugal", true), + createPlayerDTO(uuid(7), 7, "Rodrigo", "Javier", "de Paul", LocalDate.of(1994, 5, 24), "Central Midfield", "CM", "Atlético Madrid", "La Liga", true), + createPlayerDTO(uuid(8), 24, "Enzo", "Jeremías", "Fernández", LocalDate.of(2001, 1, 17), "Central Midfield", "CM", "Chelsea FC", "Premier League", true), + createPlayerDTO(uuid(9), 20, "Alexis", null, "Mac Allister", LocalDate.of(1998, 12, 24), "Central Midfield", "CM", "Liverpool FC", "Premier League", true), + createPlayerDTO(uuid(10), 10, "Lionel", "Andrés", "Messi", LocalDate.of(1987, 6, 24), "Right Winger", "RW", "Inter Miami CF", "Major League Soccer", true), + createPlayerDTO(uuid(11), 9, "Julián", null, "Álvarez", LocalDate.of(2000, 1, 31), "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)); + createPlayerDTO(uuid(12), 1, "Franco", "Daniel", "Armani", LocalDate.of(1986, 10, 16), "Goalkeeper", "GK", "River Plate", "Copa de la Liga", false), + createPlayerDTO(uuid(13), 12, "Gerónimo", null, "Rulli", LocalDate.of(1992, 5, 20), "Goalkeeper", "GK", "Ajax Amsterdam", "Eredivisie", false), + createPlayerDTO(uuid(14), 2, "Juan", "Marcos", "Foyth", LocalDate.of(1998, 1, 12), "Right-Back", "RB", "Villarreal", "La Liga", false), + createPlayerDTO(uuid(15), 4, "Gonzalo", "Ariel", "Montiel", LocalDate.of(1997, 1, 1), "Right-Back", "RB", "Nottingham Forest", "Premier League", false), + createPlayerDTO(uuid(16), 6, "Germán", "Alejo", "Pezzella", LocalDate.of(1991, 6, 27), "Centre-Back", "CB", "Real Betis Balompié", "La Liga", false), + createPlayerDTO(uuid(17), 8, "Marcos", "Javier", "Acuña", LocalDate.of(1991, 10, 28), "Left-Back", "LB", "Sevilla FC", "La Liga", false), + createPlayerDTO(uuid(18), 25, "Lisandro", null, "Martínez", LocalDate.of(1998, 1, 18), "Centre-Back", "CB", "Manchester United", "Premier League", false), + // Leandro Paredes (squad number 5) - created during tests + createPlayerDTO(uuid(19), 5, "Leandro", "Daniel", "Paredes", LocalDate.of(1994, 6, 29), "Defensive Midfield", "DM", "AS Roma", "Serie A", false), + createPlayerDTO(uuid(20), 14, "Exequiel", "Alejandro", "Palacios", LocalDate.of(1998, 10, 5), "Central Midfield", "CM", "Bayer 04 Leverkusen", "Bundesliga", false), + createPlayerDTO(uuid(21), 17, "Alejandro", "Darío", "Gómez", LocalDate.of(1988, 2, 15), "Left Winger", "LW", "AC Monza", "Serie A", false), + createPlayerDTO(uuid(22), 18, "Guido", null, "Rodríguez", LocalDate.of(1994, 4, 12), "Defensive Midfield", "DM", "Real Betis Balompié", "La Liga", false), + createPlayerDTO(uuid(23), 15, "Ángel", "Martín", "Correa", LocalDate.of(1995, 3, 9), "Right Winger", "RW", "Atlético Madrid", "La Liga", false), + createPlayerDTO(uuid(24), 16, "Thiago", "Ezequiel", "Almada", LocalDate.of(2001, 4, 26), "Attacking Midfield", "AM", "Atlanta United FC", "Major League Soccer", false), + createPlayerDTO(uuid(25), 21, "Paulo", "Exequiel", "Dybala", LocalDate.of(1993, 11, 15), "Second Striker", "SS", "AS Roma", "Serie A", false), + createPlayerDTO(uuid(26), 22, "Lautaro", "Javier", "Martínez", LocalDate.of(1997, 8, 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) { + private static PlayerDTO createPlayerDTO(UUID id, Integer squadNumber, String firstName, String middleName, + String lastName, LocalDate dateOfBirth, String position, String abbrPosition, + String team, String league, Boolean starting11) { PlayerDTO playerDTO = new PlayerDTO(); playerDTO.setId(id); + playerDTO.setSquadNumber(squadNumber); playerDTO.setFirstName(firstName); playerDTO.setMiddleName(middleName); playerDTO.setLastName(lastName); playerDTO.setDateOfBirth(dateOfBirth); - playerDTO.setSquadNumber(squadNumber); playerDTO.setPosition(position); playerDTO.setAbbrPosition(abbrPosition); playerDTO.setTeam(team); 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 index 5eea56c..f4639d9 100644 --- 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 @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.util.Arrays; import java.util.List; +import java.util.UUID; import ar.com.nanotaboada.java.samples.spring.boot.models.Player; @@ -12,24 +13,28 @@ private PlayerFakes() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } + private static UUID uuid(int index) { + return UUID.fromString(String.format("00000000-0000-0000-0000-%012d", index)); + } + /** * Leandro Paredes - Test data for CREATE operations * * Usage: - * - Repository tests: Insert into real in-memory DB (gets ID 19) + * - Repository tests: Insert into real in-memory DB * - 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) + * Note: Not pre-seeded in test DB (squad number 5 slot is empty) */ public static Player createOneValid() { Player player = new Player(); - player.setId(null); // Will be auto-generated as 19 + player.setId(null); // Will be generated by @PrePersist + player.setSquadNumber(5); 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"); @@ -39,22 +44,22 @@ public static Player createOneValid() { } /** - * Damián Martínez - Player ID 1 BEFORE update + * Damián Martínez - Player squad number 23 BEFORE update * * Usage: - * - Service tests: Mock expected data for playersService.retrieveById(1L) - * - Controller tests: Mock expected data for GET /players/1 + * - Service tests: Mock expected data for playersService.retrieveById(uuid) + * - Controller tests: Mock expected data for GET /players/{id} * * Note: Repository tests query DB directly (pre-seeded in dml.sql) */ public static Player createOneForUpdate() { Player player = new Player(); - player.setId(1L); + player.setId(uuid(1)); + player.setSquadNumber(23); 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"); @@ -64,26 +69,20 @@ public static Player createOneForUpdate() { } /** - * 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 + * Emiliano Martínez - Expected result AFTER updating player squad number 23 * * 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.setId(uuid(1)); + player.setSquadNumber(23); 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"); @@ -94,21 +93,15 @@ public static Player createOneUpdated() { /** * 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.setId(null); + player.setSquadNumber(-1); // Invalid (must be positive) 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) @@ -119,87 +112,50 @@ public static Player createOneInvalid() { /** * 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), + createPlayer(uuid(1), 23, "Damián", "Emiliano", "Martínez", LocalDate.of(1992, 9, 2), "Goalkeeper", "GK", "Aston Villa FC", "Premier League", true), + createPlayer(uuid(2), 26, "Nahuel", null, "Molina", LocalDate.of(1998, 4, 6), "Right-Back", "RB", "Atlético Madrid", "La Liga", true), + createPlayer(uuid(3), 13, "Cristian", "Gabriel", "Romero", LocalDate.of(1998, 4, 27), "Centre-Back", "CB", "Tottenham Hotspur", "Premier League", true), + createPlayer(uuid(4), 19, "Nicolás", "Hernán Gonzalo", "Otamendi", LocalDate.of(1988, 2, 12), "Centre-Back", "CB", "SL Benfica", "Liga Portugal", true), + createPlayer(uuid(5), 3, "Nicolás", "Alejandro", "Tagliafico", LocalDate.of(1992, 8, 31), "Left-Back", "LB", "Olympique Lyon", "Ligue 1", true), + createPlayer(uuid(6), 11, "Ángel", "Fabián", "Di María", LocalDate.of(1988, 2, 14), "Right Winger", "RW", "SL Benfica", "Liga Portugal", true), + createPlayer(uuid(7), 7, "Rodrigo", "Javier", "de Paul", LocalDate.of(1994, 5, 24), "Central Midfield", "CM", "Atlético Madrid", "La Liga", true), + createPlayer(uuid(8), 24, "Enzo", "Jeremías", "Fernández", LocalDate.of(2001, 1, 17), "Central Midfield", "CM", "Chelsea FC", "Premier League", true), + createPlayer(uuid(9), 20, "Alexis", null, "Mac Allister", LocalDate.of(1998, 12, 24), "Central Midfield", "CM", "Liverpool FC", "Premier League", true), + createPlayer(uuid(10), 10, "Lionel", "Andrés", "Messi", LocalDate.of(1987, 6, 24), "Right Winger", "RW", "Inter Miami CF", "Major League Soccer", true), + createPlayer(uuid(11), 9, "Julián", null, "Álvarez", LocalDate.of(2000, 1, 31), "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)); + createPlayer(uuid(12), 1, "Franco", "Daniel", "Armani", LocalDate.of(1986, 10, 16), "Goalkeeper", "GK", "River Plate", "Copa de la Liga", false), + createPlayer(uuid(13), 12, "Gerónimo", null, "Rulli", LocalDate.of(1992, 5, 20), "Goalkeeper", "GK", "Ajax Amsterdam", "Eredivisie", false), + createPlayer(uuid(14), 2, "Juan", "Marcos", "Foyth", LocalDate.of(1998, 1, 12), "Right-Back", "RB", "Villarreal", "La Liga", false), + createPlayer(uuid(15), 4, "Gonzalo", "Ariel", "Montiel", LocalDate.of(1997, 1, 1), "Right-Back", "RB", "Nottingham Forest", "Premier League", false), + createPlayer(uuid(16), 6, "Germán", "Alejo", "Pezzella", LocalDate.of(1991, 6, 27), "Centre-Back", "CB", "Real Betis Balompié", "La Liga", false), + createPlayer(uuid(17), 8, "Marcos", "Javier", "Acuña", LocalDate.of(1991, 10, 28), "Left-Back", "LB", "Sevilla FC", "La Liga", false), + createPlayer(uuid(18), 25, "Lisandro", null, "Martínez", LocalDate.of(1998, 1, 18), "Centre-Back", "CB", "Manchester United", "Premier League", false), + // Leandro Paredes (squad number 5) - created during tests + createPlayer(uuid(19), 5, "Leandro", "Daniel", "Paredes", LocalDate.of(1994, 6, 29), "Defensive Midfield", "DM", "AS Roma", "Serie A", false), + createPlayer(uuid(20), 14, "Exequiel", "Alejandro", "Palacios", LocalDate.of(1998, 10, 5), "Central Midfield", "CM", "Bayer 04 Leverkusen", "Bundesliga", false), + createPlayer(uuid(21), 17, "Alejandro", "Darío", "Gómez", LocalDate.of(1988, 2, 15), "Left Winger", "LW", "AC Monza", "Serie A", false), + createPlayer(uuid(22), 18, "Guido", null, "Rodríguez", LocalDate.of(1994, 4, 12), "Defensive Midfield", "DM", "Real Betis Balompié", "La Liga", false), + createPlayer(uuid(23), 15, "Ángel", "Martín", "Correa", LocalDate.of(1995, 3, 9), "Right Winger", "RW", "Atlético Madrid", "La Liga", false), + createPlayer(uuid(24), 16, "Thiago", "Ezequiel", "Almada", LocalDate.of(2001, 4, 26), "Attacking Midfield", "AM", "Atlanta United FC", "Major League Soccer", false), + createPlayer(uuid(25), 21, "Paulo", "Exequiel", "Dybala", LocalDate.of(1993, 11, 15), "Second Striker", "SS", "AS Roma", "Serie A", false), + createPlayer(uuid(26), 22, "Lautaro", "Javier", "Martínez", LocalDate.of(1997, 8, 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) { + private static Player createPlayer(UUID id, Integer squadNumber, String firstName, String middleName, + String lastName, LocalDate dateOfBirth, String position, String abbrPosition, + String team, String league, Boolean starting11) { Player player = new Player(); player.setId(id); + player.setSquadNumber(squadNumber); player.setFirstName(firstName); player.setMiddleName(middleName); player.setLastName(lastName); player.setDateOfBirth(dateOfBirth); - player.setSquadNumber(squadNumber); player.setPosition(position); player.setAbbrPosition(abbrPosition); player.setTeam(team); 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 index a31f1f6..74e763e 100644 --- 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 @@ -2,12 +2,13 @@ import static org.assertj.core.api.BDDAssertions.then; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyInt; 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.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -77,7 +78,8 @@ void givenValidPlayer_whenPost_thenReturnsCreated() // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); PlayerDTO savedDTO = PlayerDTOFakes.createOneValid(); - savedDTO.setId(19L); // Simulating auto-generated ID + UUID savedUuid = UUID.fromString("00000000-0000-0000-0000-000000000019"); + savedDTO.setId(savedUuid); String content = objectMapper.writeValueAsString(dto); Mockito .when(playersServiceMock.create(any(PlayerDTO.class))) @@ -95,7 +97,7 @@ void givenValidPlayer_whenPost_thenReturnsCreated() verify(playersServiceMock, times(1)).create(any(PlayerDTO.class)); then(response.getStatus()).isEqualTo(HttpStatus.CREATED.value()); then(response.getHeader(HttpHeaders.LOCATION)).isNotNull(); - then(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/19"); + then(response.getHeader(HttpHeaders.LOCATION)).contains(PATH + "/" + savedUuid); } /** @@ -189,7 +191,7 @@ void givenPlayersExist_whenGetAll_thenReturnsOkWithAllPlayers() /** * Given a player exists - * When requesting that player by ID + * When requesting that player by UUID * Then response status is 200 OK and the player data is returned */ @Test @@ -197,11 +199,12 @@ void givenPlayerExists_whenGetById_thenReturnsOk() throws Exception { // Given PlayerDTO expected = PlayerDTOFakes.createOneForUpdate(); + UUID id = expected.getId(); Mockito - .when(playersServiceMock.retrieveById(1L)) + .when(playersServiceMock.retrieveById(id)) .thenReturn(expected); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .get(PATH + "/{id}", 1L); + .get(PATH + "/{id}", id); // When MockHttpServletResponse response = application .perform(request) @@ -211,23 +214,23 @@ void givenPlayerExists_whenGetById_thenReturnsOk() PlayerDTO actual = objectMapper.readValue(content, PlayerDTO.class); // Then then(response.getContentType()).contains("application/json"); - verify(playersServiceMock, times(1)).retrieveById(1L); + verify(playersServiceMock, times(1)).retrieveById(id); then(response.getStatus()).isEqualTo(HttpStatus.OK.value()); then(actual).usingRecursiveComparison().isEqualTo(expected); } /** - * Given a player with a specific ID does not exist - * When requesting that player by ID + * Given a player with a specific UUID does not exist + * When requesting that player by UUID * Then response status is 404 Not Found */ @Test void givenPlayerDoesNotExist_whenGetById_thenReturnsNotFound() throws Exception { // Given - Long id = 999L; + UUID id = UUID.randomUUID(); Mockito - .when(playersServiceMock.retrieveById(anyLong())) + .when(playersServiceMock.retrieveById(any(UUID.class))) .thenReturn(null); MockHttpServletRequestBuilder request = MockMvcRequestBuilders .get(PATH + "/{id}", id); @@ -237,7 +240,7 @@ void givenPlayerDoesNotExist_whenGetById_thenReturnsNotFound() .andReturn() .getResponse(); // Then - verify(playersServiceMock, times(1)).retrieveById(anyLong()); + verify(playersServiceMock, times(1)).retrieveById(any(UUID.class)); then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -371,7 +374,7 @@ void givenNoPlayersExist_whenSearchByLeague_thenReturnsOk() /** * Given a player exists and valid update data is provided - * When updating that player + * When updating that player by squad number * Then response status is 204 No Content */ @Test @@ -379,13 +382,13 @@ void givenPlayerExists_whenPut_thenReturnsNoContent() throws Exception { // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); - dto.setId(1L); // Set ID for update operation + Integer squadNumber = dto.getSquadNumber(); String content = objectMapper.writeValueAsString(dto); Mockito - .when(playersServiceMock.update(any(PlayerDTO.class))) + .when(playersServiceMock.update(squadNumber, dto)) .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", dto.getId()) + .put(PATH + "/{squadNumber}", squadNumber) .content(content) .contentType(MediaType.APPLICATION_JSON); // When @@ -394,12 +397,12 @@ void givenPlayerExists_whenPut_thenReturnsNoContent() .andReturn() .getResponse(); // Then - verify(playersServiceMock, times(1)).update(any(PlayerDTO.class)); + verify(playersServiceMock, times(1)).update(anyInt(), any(PlayerDTO.class)); then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } /** - * Given a player with the provided ID does not exist + * Given a player with the provided squad number does not exist * When attempting to update that player * Then response status is 404 Not Found */ @@ -408,13 +411,13 @@ void givenPlayerDoesNotExist_whenPut_thenReturnsNotFound() throws Exception { // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); - dto.setId(999L); // Set ID for update operation + Integer squadNumber = dto.getSquadNumber(); String content = objectMapper.writeValueAsString(dto); Mockito - .when(playersServiceMock.update(any(PlayerDTO.class))) + .when(playersServiceMock.update(anyInt(), any(PlayerDTO.class))) .thenReturn(false); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", dto.getId()) + .put(PATH + "/{squadNumber}", squadNumber) .content(content) .contentType(MediaType.APPLICATION_JSON); // When @@ -423,7 +426,7 @@ void givenPlayerDoesNotExist_whenPut_thenReturnsNotFound() .andReturn() .getResponse(); // Then - verify(playersServiceMock, times(1)).update(any(PlayerDTO.class)); + verify(playersServiceMock, times(1)).update(anyInt(), any(PlayerDTO.class)); then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -439,7 +442,7 @@ void givenInvalidPlayer_whenPut_thenReturnsBadRequest() PlayerDTO dto = PlayerDTOFakes.createOneInvalid(); String content = objectMapper.writeValueAsString(dto); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", dto.getId()) + .put(PATH + "/{squadNumber}", 1) .content(content) .contentType(MediaType.APPLICATION_JSON); // When @@ -448,25 +451,25 @@ void givenInvalidPlayer_whenPut_thenReturnsBadRequest() .andReturn() .getResponse(); // Then - verify(playersServiceMock, never()).update(any(PlayerDTO.class)); + verify(playersServiceMock, never()).update(anyInt(), any(PlayerDTO.class)); then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /** - * Given the path ID does not match the body ID + * Given the path squad number does not match the body squad number * When attempting to update a player * Then response status is 400 Bad Request and service is never called */ @Test - void givenIdMismatch_whenPut_thenReturnsBadRequest() + void givenSquadNumberMismatch_whenPut_thenReturnsBadRequest() throws Exception { // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); - dto.setId(999L); // Body has different ID - Long pathId = 1L; // Path has different ID + dto.setSquadNumber(999); // Body has different squad number + Integer pathSquadNumber = 5; // Path has different squad number String content = objectMapper.writeValueAsString(dto); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", pathId) + .put(PATH + "/{squadNumber}", pathSquadNumber) .content(content) .contentType(MediaType.APPLICATION_JSON); // When @@ -475,28 +478,24 @@ void givenIdMismatch_whenPut_thenReturnsBadRequest() .andReturn() .getResponse(); // Then - verify(playersServiceMock, never()).update(any(PlayerDTO.class)); + verify(playersServiceMock, never()).update(anyInt(), any(PlayerDTO.class)); then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /** - * Given the body ID is null (ID only in path) - * When updating a player - * Then the ID is set from the path and the update proceeds normally + * Given the body squad number is null + * When attempting to update a player + * Then response status is 400 Bad Request (squad number is required) */ @Test - void givenNullBodyId_whenPut_thenSetsIdFromPath() + void givenNullBodySquadNumber_whenPut_thenReturnsBadRequest() throws Exception { // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); - dto.setId(null); // Body has null ID - Long pathId = 1L; + dto.setSquadNumber(null); // Body has null squad number (violates @NotNull) String content = objectMapper.writeValueAsString(dto); - Mockito - .when(playersServiceMock.update(any(PlayerDTO.class))) - .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .put(PATH + "/{id}", pathId) + .put(PATH + "/{squadNumber}", 5) .content(content) .contentType(MediaType.APPLICATION_JSON); // When @@ -505,8 +504,8 @@ void givenNullBodyId_whenPut_thenSetsIdFromPath() .andReturn() .getResponse(); // Then - verify(playersServiceMock, times(1)).update(any(PlayerDTO.class)); - then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + verify(playersServiceMock, never()).update(anyInt(), any(PlayerDTO.class)); + then(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } /* @@ -517,50 +516,51 @@ void givenNullBodyId_whenPut_thenSetsIdFromPath() /** * Given a player exists - * When deleting that player by ID + * When deleting that player by squad number * Then response status is 204 No Content */ @Test void givenPlayerExists_whenDelete_thenReturnsNoContent() throws Exception { // Given + Integer squadNumber = 17; Mockito - .when(playersServiceMock.delete(1L)) + .when(playersServiceMock.deleteBySquadNumber(squadNumber)) .thenReturn(true); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete(PATH + "/{id}", 1L); + .delete(PATH + "/{squadNumber}", squadNumber); // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); // Then - verify(playersServiceMock, times(1)).delete(1L); + verify(playersServiceMock, times(1)).deleteBySquadNumber(squadNumber); then(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); } /** - * Given a player with a specific ID does not exist - * When attempting to delete that player by ID + * Given a player with a specific squad number does not exist + * When attempting to delete that player * Then response status is 404 Not Found */ @Test void givenPlayerDoesNotExist_whenDelete_thenReturnsNotFound() throws Exception { // Given - Long id = 999L; + Integer squadNumber = 999; Mockito - .when(playersServiceMock.delete(id)) + .when(playersServiceMock.deleteBySquadNumber(squadNumber)) .thenReturn(false); MockHttpServletRequestBuilder request = MockMvcRequestBuilders - .delete(PATH + "/{id}", id); + .delete(PATH + "/{squadNumber}", squadNumber); // When MockHttpServletResponse response = application .perform(request) .andReturn() .getResponse(); // Then - verify(playersServiceMock, times(1)).delete(id); + verify(playersServiceMock, times(1)).deleteBySquadNumber(squadNumber); then(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); } } 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 index 8ee6b04..fe6c20e 100644 --- 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 @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -27,30 +28,31 @@ class PlayersRepositoryTests { private PlayersRepository repository; /** - * Given a player exists in the database - * When findById() is called with that player's ID + * Given a player is saved to the database + * When findById() is called with the player's UUID surrogate key * Then the player is returned */ @Test void givenPlayerExists_whenFindById_thenReturnsPlayer() { // Given Player expected = repository.save(PlayerFakes.createOneValid()); + UUID savedUuid = expected.getId(); // When - Optional actual = repository.findById(expected.getId()); + Optional actual = repository.findById(savedUuid); // Then then(actual).isPresent(); then(actual.get()).usingRecursiveComparison().isEqualTo(expected); } /** - * Given the database does not contain a player with a specific ID - * When querying by that ID + * Given the database does not contain a player with a specific UUID + * When querying by that UUID * Then an empty Optional is returned */ @Test void givenPlayerDoesNotExist_whenFindById_thenReturnsEmpty() { // Given - Long nonExistentId = 999L; + UUID nonExistentId = UUID.randomUUID(); // When Optional actual = repository.findById(nonExistentId); // Then 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 index 4ae52d9..7727c73 100644 --- 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 @@ -2,13 +2,13 @@ import static org.assertj.core.api.BDDAssertions.then; 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 java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -58,7 +58,7 @@ void givenNoExistingPlayer_whenCreate_thenReturnsPlayerDTO() { PlayerDTO expected = PlayerDTOFakes.createOneValid(); Mockito .when(playersRepositoryMock.findBySquadNumber(expected.getSquadNumber())) - .thenReturn(Optional.empty()); // No conflict + .thenReturn(Optional.empty()); Mockito .when(modelMapperMock.map(expected, Player.class)) .thenReturn(entity); @@ -115,7 +115,7 @@ void givenRaceCondition_whenCreate_thenReturnsNull() { Player entity = PlayerFakes.createOneValid(); Mockito .when(playersRepositoryMock.findBySquadNumber(dto.getSquadNumber())) - .thenReturn(Optional.empty()); // No conflict initially + .thenReturn(Optional.empty()); Mockito .when(modelMapperMock.map(dto, Player.class)) .thenReturn(entity); @@ -149,7 +149,6 @@ void givenAllPlayersExist_whenRetrieveAll_thenReturns26Players() { Mockito .when(playersRepositoryMock.findAll()) .thenReturn(entities); - // Mock modelMapper to convert each player correctly for (int i = 0; i < entities.size(); i++) { Mockito .when(modelMapperMock.map(entities.get(i), PlayerDTO.class)) @@ -163,8 +162,8 @@ void givenAllPlayersExist_whenRetrieveAll_thenReturns26Players() { } /** - * Given a player exists with a specific ID - * When retrieving that player by ID + * Given a player exists with a specific UUID + * When retrieving that player by UUID * Then the corresponding PlayerDTO is returned */ @Test @@ -172,36 +171,37 @@ void givenPlayerExists_whenRetrieveById_thenReturnsPlayerDTO() { // Given Player entity = PlayerFakes.createOneForUpdate(); PlayerDTO expected = PlayerDTOFakes.createOneForUpdate(); + UUID id = entity.getId(); Mockito - .when(playersRepositoryMock.findById(1L)) + .when(playersRepositoryMock.findById(id)) .thenReturn(Optional.of(entity)); Mockito .when(modelMapperMock.map(entity, PlayerDTO.class)) .thenReturn(expected); // When - PlayerDTO actual = playersService.retrieveById(1L); + PlayerDTO actual = playersService.retrieveById(id); // Then - verify(playersRepositoryMock, times(1)).findById(1L); + verify(playersRepositoryMock, times(1)).findById(id); verify(modelMapperMock, times(1)).map(entity, PlayerDTO.class); then(actual).isEqualTo(expected); } /** - * Given no player exists with a specific ID - * When retrieving by that ID + * Given no player exists with a specific UUID + * When retrieving by that UUID * Then null is returned */ @Test void givenPlayerDoesNotExist_whenRetrieveById_thenReturnsNull() { // Given - Long id = 999L; + UUID id = UUID.randomUUID(); Mockito - .when(playersRepositoryMock.findById(anyLong())) + .when(playersRepositoryMock.findById(id)) .thenReturn(Optional.empty()); // When PlayerDTO actual = playersService.retrieveById(id); // Then - verify(playersRepositoryMock, times(1)).findById(anyLong()); + verify(playersRepositoryMock, times(1)).findById(id); verify(modelMapperMock, never()).map(any(Player.class), any()); then(actual).isNull(); } @@ -282,7 +282,6 @@ void givenPlayersExist_whenSearchByLeague_thenReturns7Players() { Mockito .when(playersRepositoryMock.findByLeagueContainingIgnoreCase(any())) .thenReturn(entities); - // Mock modelMapper to convert each player correctly for (int i = 0; i < entities.size(); i++) { Mockito .when(modelMapperMock.map(entities.get(i), PlayerDTO.class)) @@ -326,7 +325,7 @@ void givenNoPlayersExist_whenSearchByLeague_thenReturnsEmptyList() { /** * Given a player exists - * When update() is called with modified player data + * When update() is called with the player's squad number and modified data * Then the player is updated and true is returned */ @Test @@ -334,23 +333,24 @@ void givenPlayerExists_whenUpdate_thenReturnsTrue() { // Given Player entity = PlayerFakes.createOneUpdated(); PlayerDTO dto = PlayerDTOFakes.createOneUpdated(); + Integer squadNumber = dto.getSquadNumber(); Mockito - .when(playersRepositoryMock.existsById(1L)) - .thenReturn(true); + .when(playersRepositoryMock.findBySquadNumber(squadNumber)) + .thenReturn(Optional.of(entity)); Mockito .when(modelMapperMock.map(dto, Player.class)) .thenReturn(entity); // When - boolean actual = playersService.update(dto); + boolean actual = playersService.update(squadNumber, dto); // Then - verify(playersRepositoryMock, times(1)).existsById(1L); + verify(playersRepositoryMock, times(1)).findBySquadNumber(squadNumber); verify(playersRepositoryMock, times(1)).save(any(Player.class)); verify(modelMapperMock, times(1)).map(dto, Player.class); then(actual).isTrue(); } /** - * Given no player exists with the specified ID + * Given no player exists with the specified squad number * When update() is called * Then false is returned without saving */ @@ -358,33 +358,34 @@ void givenPlayerExists_whenUpdate_thenReturnsTrue() { void givenPlayerDoesNotExist_whenUpdate_thenReturnsFalse() { // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); - dto.setId(999L); + Integer squadNumber = 999; Mockito - .when(playersRepositoryMock.existsById(999L)) - .thenReturn(false); + .when(playersRepositoryMock.findBySquadNumber(squadNumber)) + .thenReturn(Optional.empty()); // When - boolean actual = playersService.update(dto); + boolean actual = playersService.update(squadNumber, dto); // Then - verify(playersRepositoryMock, times(1)).existsById(999L); + verify(playersRepositoryMock, times(1)).findBySquadNumber(squadNumber); verify(playersRepositoryMock, never()).save(any(Player.class)); verify(modelMapperMock, never()).map(dto, Player.class); then(actual).isFalse(); } /** - * Given a PlayerDTO has null ID + * Given a null squad number is passed * When update() is called - * Then false is returned without checking repository or saving + * Then false is returned without hitting the repository */ @Test - void givenNullId_whenUpdate_thenReturnsFalse() { + void givenNullSquadNumber_whenUpdate_thenReturnsFalse() { // Given PlayerDTO dto = PlayerDTOFakes.createOneValid(); - dto.setId(null); + Mockito + .when(playersRepositoryMock.findBySquadNumber(null)) + .thenReturn(Optional.empty()); // When - boolean actual = playersService.update(dto); + boolean actual = playersService.update(null, dto); // Then - verify(playersRepositoryMock, never()).existsById(any()); verify(playersRepositoryMock, never()).save(any(Player.class)); verify(modelMapperMock, never()).map(any(), any()); then(actual).isFalse(); @@ -398,39 +399,45 @@ void givenNullId_whenUpdate_thenReturnsFalse() { /** * Given a player exists - * When deleting that player - * Then the player is deleted and true is returned + * When deleting that player by squad number + * Then the player is deleted by UUID and true is returned */ @Test void givenPlayerExists_whenDelete_thenReturnsTrue() { // Given + Integer squadNumber = 17; + Player entity = PlayerFakes.createAll().stream() + .filter(p -> squadNumber.equals(p.getSquadNumber())) + .findFirst() + .orElseThrow(); Mockito - .when(playersRepositoryMock.existsById(21L)) - .thenReturn(true); + .when(playersRepositoryMock.findBySquadNumber(squadNumber)) + .thenReturn(Optional.of(entity)); // When - boolean actual = playersService.delete(21L); + boolean actual = playersService.deleteBySquadNumber(squadNumber); // Then - verify(playersRepositoryMock, times(1)).existsById(21L); - verify(playersRepositoryMock, times(1)).deleteById(21L); + verify(playersRepositoryMock, times(1)).findBySquadNumber(squadNumber); + verify(playersRepositoryMock, times(1)).deleteById(entity.getId()); then(actual).isTrue(); } /** - * Given no player exists with a specific ID + * Given no player exists with a specific squad number * When attempting to delete that player * Then false is returned without deleting */ @Test void givenPlayerDoesNotExist_whenDelete_thenReturnsFalse() { // Given + Integer squadNumber = 999; Mockito - .when(playersRepositoryMock.existsById(999L)) - .thenReturn(false); + .when(playersRepositoryMock.findBySquadNumber(squadNumber)) + .thenReturn(Optional.empty()); // When - boolean actual = playersService.delete(999L); + boolean actual = playersService.deleteBySquadNumber(squadNumber); // Then - verify(playersRepositoryMock, times(1)).existsById(999L); - verify(playersRepositoryMock, never()).deleteById(anyLong()); + verify(playersRepositoryMock, times(1)).findBySquadNumber(squadNumber); + verify(playersRepositoryMock, never()).deleteById(any()); then(actual).isFalse(); } } diff --git a/src/test/resources/ddl.sql b/src/test/resources/ddl.sql index ed4ac22..cd73b2a 100644 --- a/src/test/resources/ddl.sql +++ b/src/test/resources/ddl.sql @@ -5,15 +5,15 @@ 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 + id VARCHAR(36) PRIMARY KEY, + squadNumber INTEGER NOT NULL UNIQUE, + firstName TEXT NOT NULL, + middleName TEXT, + lastName TEXT NOT NULL, + dateOfBirth TEXT NOT NULL, + 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 index 13f8558..978216d 100644 --- a/src/test/resources/dml.sql +++ b/src/test/resources/dml.sql @@ -1,37 +1,36 @@ -- Test Database Data (DML - Data Manipulation Language) --- Contains all 26 players from production database EXCEPT Leandro Paredes (ID 19) +-- Contains all 26 players from production database EXCEPT Leandro Paredes (squadNumber 5) -- 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 +-- Damián Emiliano Martínez (squadNumber 23) will be updated during tests +-- Alejandro Gómez (squadNumber 17) 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); +-- Starting 11 (id is PRIMARY KEY, squadNumber is UNIQUE) +INSERT INTO players (id, squadNumber, firstName, middleName, lastName, dateOfBirth, position, abbrPosition, team, league, starting11) VALUES +('00000000-0000-0000-0000-000000000001', 23, 'Damián', 'Emiliano', 'Martínez', '1992-09-02T00:00:00.000Z', 'Goalkeeper', 'GK', 'Aston Villa FC', 'Premier League', 1), +('00000000-0000-0000-0000-000000000002', 26, 'Nahuel', NULL, 'Molina', '1998-04-06T00:00:00.000Z', 'Right-Back', 'RB', 'Atlético Madrid', 'La Liga', 1), +('00000000-0000-0000-0000-000000000003', 13, 'Cristian', 'Gabriel', 'Romero', '1998-04-27T00:00:00.000Z', 'Centre-Back', 'CB', 'Tottenham Hotspur', 'Premier League', 1), +('00000000-0000-0000-0000-000000000004', 19, 'Nicolás', 'Hernán Gonzalo', 'Otamendi', '1988-02-12T00:00:00.000Z', 'Centre-Back', 'CB', 'SL Benfica', 'Liga Portugal', 1), +('00000000-0000-0000-0000-000000000005', 3, 'Nicolás', 'Alejandro', 'Tagliafico', '1992-08-31T00:00:00.000Z', 'Left-Back', 'LB', 'Olympique Lyon', 'Ligue 1', 1), +('00000000-0000-0000-0000-000000000006', 11, 'Ángel', 'Fabián', 'Di María', '1988-02-14T00:00:00.000Z', 'Right Winger', 'RW', 'SL Benfica', 'Liga Portugal', 1), +('00000000-0000-0000-0000-000000000007', 7, 'Rodrigo', 'Javier', 'de Paul', '1994-05-24T00:00:00.000Z', 'Central Midfield', 'CM', 'Atlético Madrid', 'La Liga', 1), +('00000000-0000-0000-0000-000000000008', 24, 'Enzo', 'Jeremías', 'Fernández', '2001-01-17T00:00:00.000Z', 'Central Midfield', 'CM', 'Chelsea FC', 'Premier League', 1), +('00000000-0000-0000-0000-000000000009', 20, 'Alexis', NULL, 'Mac Allister', '1998-12-24T00:00:00.000Z', 'Central Midfield', 'CM', 'Liverpool FC', 'Premier League', 1), +('00000000-0000-0000-0000-000000000010', 10, 'Lionel', 'Andrés', 'Messi', '1987-06-24T00:00:00.000Z', 'Right Winger', 'RW', 'Inter Miami CF', 'Major League Soccer', 1), +('00000000-0000-0000-0000-000000000011', 9, 'Julián', NULL, 'Álvarez', '2000-01-31T00:00:00.000Z', '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); +-- Substitutes (squad number 5 intentionally skipped - Leandro Paredes will be created during tests) +INSERT INTO players (id, squadNumber, firstName, middleName, lastName, dateOfBirth, position, abbrPosition, team, league, starting11) VALUES +('00000000-0000-0000-0000-000000000012', 1, 'Franco', 'Daniel', 'Armani', '1986-10-16T00:00:00.000Z', 'Goalkeeper', 'GK', 'River Plate', 'Copa de la Liga', 0), +('00000000-0000-0000-0000-000000000013', 12, 'Gerónimo', NULL, 'Rulli', '1992-05-20T00:00:00.000Z', 'Goalkeeper', 'GK', 'Ajax Amsterdam', 'Eredivisie', 0), +('00000000-0000-0000-0000-000000000014', 2, 'Juan', 'Marcos', 'Foyth', '1998-01-12T00:00:00.000Z', 'Right-Back', 'RB', 'Villarreal', 'La Liga', 0), +('00000000-0000-0000-0000-000000000015', 4, 'Gonzalo', 'Ariel', 'Montiel', '1997-01-01T00:00:00.000Z', 'Right-Back', 'RB', 'Nottingham Forest', 'Premier League', 0), +('00000000-0000-0000-0000-000000000016', 6, 'Germán', 'Alejo', 'Pezzella', '1991-06-27T00:00:00.000Z', 'Centre-Back', 'CB', 'Real Betis Balompié', 'La Liga', 0), +('00000000-0000-0000-0000-000000000017', 8, 'Marcos', 'Javier', 'Acuña', '1991-10-28T00:00:00.000Z', 'Left-Back', 'LB', 'Sevilla FC', 'La Liga', 0), +('00000000-0000-0000-0000-000000000018', 25, 'Lisandro', NULL, 'Martínez', '1998-01-18T00:00:00.000Z', 'Centre-Back', 'CB', 'Manchester United', 'Premier League', 0), +('00000000-0000-0000-0000-000000000020', 14, 'Exequiel', 'Alejandro', 'Palacios', '1998-10-05T00:00:00.000Z', 'Central Midfield', 'CM', 'Bayer 04 Leverkusen', 'Bundesliga', 0), +('00000000-0000-0000-0000-000000000021', 17, 'Alejandro', 'Darío', 'Gómez', '1988-02-15T00:00:00.000Z', 'Left Winger', 'LW', 'AC Monza', 'Serie A', 0), +('00000000-0000-0000-0000-000000000022', 18, 'Guido', NULL, 'Rodríguez', '1994-04-12T00:00:00.000Z', 'Defensive Midfield', 'DM', 'Real Betis Balompié', 'La Liga', 0), +('00000000-0000-0000-0000-000000000023', 15, 'Ángel', 'Martín', 'Correa', '1995-03-09T00:00:00.000Z', 'Right Winger', 'RW', 'Atlético Madrid', 'La Liga', 0), +('00000000-0000-0000-0000-000000000024', 16, 'Thiago', 'Ezequiel', 'Almada', '2001-04-26T00:00:00.000Z', 'Attacking Midfield', 'AM', 'Atlanta United FC', 'Major League Soccer', 0), +('00000000-0000-0000-0000-000000000025', 21, 'Paulo', 'Exequiel', 'Dybala', '1993-11-15T00:00:00.000Z', 'Second Striker', 'SS', 'AS Roma', 'Serie A', 0), +('00000000-0000-0000-0000-000000000026', 22, 'Lautaro', 'Javier', 'Martínez', '1997-08-22T00:00:00.000Z', 'Centre-Forward', 'CF', 'Inter Milan', 'Serie A', 0); diff --git a/storage/players-sqlite3.db b/storage/players-sqlite3.db index 14a1d8e..d7a8987 100644 Binary files a/storage/players-sqlite3.db and b/storage/players-sqlite3.db differ