From 23de257fb296642eecfeebbdcafa2c4ebbc6d8f7 Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 12:43:23 +0200 Subject: [PATCH 01/34] feat: prepare backend for player UUID authentication --- .../backend/database/entity/PlayerEntity.java | 2 ++ .../backend/lobby/api/LobbyController.java | 2 +- .../backend/lobby/api/LobbySocketController.java | 2 +- .../backend/lobby/api/dto/JoinMessage.java | 1 + .../backend/lobby/application/LobbyService.java | 4 ++-- .../codenames/backend/lobby/domain/Lobby.java | 14 +++++++++++--- .../codenames/backend/lobby/domain/Player.java | 2 +- 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java index a8f319ea..d62d8eb0 100644 --- a/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java +++ b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java @@ -35,4 +35,6 @@ public class PlayerEntity { private String team; @Column(length = 9, nullable = false) private String role; + @Column(nullable = false, unique = true) + private String uuid; } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index 6c579251..fca51504 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -65,7 +65,7 @@ public ResponseEntity createLobby(@RequestParam String username) @GetMapping("/{lobbyCode}/join") public ResponseEntity joinLobby( @RequestParam String username, @PathVariable String lobbyCode) { - boolean joined = service.joinLobby(username, lobbyCode); + boolean joined = service.joinLobby(username, lobbyCode, null); if (joined) { return ResponseEntity.ok( diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java index 7d4cc8dc..5fd9ab77 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java @@ -71,7 +71,7 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) log.error(e.getMessage()); } - boolean joined = lobbyService.joinLobby(message.getName(), message.getCode()); + boolean joined = lobbyService.joinLobby(message.getName(), message.getCode(), message.getUuid()); boolean reconnect = lobbyService.getPlayers(message.getCode()).stream() .anyMatch(player -> player.username().equals(message.getName())); diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java index 185f503b..e5f7bd23 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java @@ -13,4 +13,5 @@ public class JoinMessage { private String name; private String code; + private String uuid; } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index 1bcef9fc..ee92cf0e 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -85,11 +85,11 @@ private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player successfully joined, {@code false} otherwise */ - public boolean joinLobby(String username, String lobbyCode) { + public boolean joinLobby(String username, String lobbyCode, String uuid) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { log.info("{}: a player has joined", lobbyCode); - return lobby.addPlayer(username); + return lobby.addPlayer(username, uuid); } log.error("{}: an error occurred when joining lobby", lobbyCode); return false; diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index 9f6f96cb..a7205bd2 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -55,7 +55,7 @@ public boolean addPlayer(String username, boolean isHost) { return false; } - playerList.add(new Player(username, isHost)); + playerList.add(new Player(username, isHost, null)); return true; } @@ -66,8 +66,16 @@ public boolean addPlayer(String username, boolean isHost) { * @param username the username of the player * @return {@code true} if the player was added, {@code false} otherwise */ - public boolean addPlayer(String username) { - return addPlayer(username, false); + public boolean addPlayer(String username, String uuid) { + boolean alreadyExists = + playerList.stream().anyMatch(p -> p.username().equals(username)); + + if (alreadyExists || playerList.size() >= MAX_PLAYERS) { + return false; + } + + playerList.add(new Player(username, false, uuid)); + return true; } /** diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java index a2697933..7febd494 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java @@ -5,5 +5,5 @@ * *

A player is identified by a username and may be associated with a WebSocket session. */ -public record Player(String username, boolean isHost) { +public record Player(String username, boolean isHost, String uuid) { } From 8edc8f9bb21dc12db1ef5d5dd408641c44c754ad Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 13:52:01 +0200 Subject: [PATCH 02/34] feat: add player UUID generation in backend --- .../backend/lobby/api/LobbyController.java | 78 ++++++++++--------- .../lobby/api/LobbySocketController.java | 3 +- .../backend/lobby/api/dto/LobbyResponse.java | 3 +- .../lobby/application/LobbyService.java | 15 +++- .../codenames/backend/lobby/domain/Lobby.java | 21 ++--- .../lobby/api/LobbyControllerTest.java | 19 +++-- .../lobby/api/LobbySocketControllerTest.java | 21 ++--- .../lobby/application/LobbyServiceTest.java | 30 +++---- .../backend/lobby/domain/LobbyTest.java | 27 +++---- .../backend/lobby/domain/PlayerTest.java | 2 +- 10 files changed, 116 insertions(+), 103 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index fca51504..66d7b77f 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -3,6 +3,7 @@ import com.codenames.codenames.backend.lobby.api.dto.LobbyResponse; import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Player; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -46,12 +47,16 @@ public ResponseEntity createLobby(@RequestParam String username) String lobbyCode = service.createLobby(username); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "", null, false)); + .body(new LobbyResponse("Error while creating lobby.", "", null, false, null)); // 5. Parameter null } else { - List players = service.getPlayersDto(lobbyCode); + + // UUID des Hosts abrufen und mitsenden + Player host = service.getPlayer(lobbyCode, username); + String hostUuid = (host != null) ? host.uuid() : null; + return ResponseEntity.ok( - new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false)); + new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false, hostUuid)); } } @@ -64,16 +69,18 @@ public ResponseEntity createLobby(@RequestParam String username) */ @GetMapping("/{lobbyCode}/join") public ResponseEntity joinLobby( - @RequestParam String username, @PathVariable String lobbyCode) { - boolean joined = service.joinLobby(username, lobbyCode, null); - if (joined) { + @RequestParam String username, @PathVariable String lobbyCode) { + // GEÄNDERT: Nimmt jetzt ein Player-Objekt zurück anstatt boolean + Player joinedPlayer = service.joinLobby(username, lobbyCode, null); + + if (joinedPlayer != null) { // GEÄNDERT: Prüft, ob das Objekt nicht null ist return ResponseEntity.ok( - new LobbyResponse( - "Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false)); + new LobbyResponse( + "Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, joinedPlayer.uuid())); // UUID an Frontend geschickt } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); // 5. Parameter null } } @@ -86,7 +93,7 @@ public ResponseEntity joinLobby( */ @GetMapping("/{lobbyCode}/leave") public ResponseEntity leaveLobby( - @PathVariable String lobbyCode, @RequestParam String username) { + @PathVariable String lobbyCode, @RequestParam String username) { boolean wasStarted = service.getIsStarted(lobbyCode); boolean left = service.leaveLobby(username, lobbyCode); @@ -94,11 +101,11 @@ public ResponseEntity leaveLobby( service.checkLobbyStillHasPlayers(lobbyCode); return ResponseEntity.ok( - new LobbyResponse( - "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false)); + new LobbyResponse( + "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); // 5. Parameter null } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); // 5. Parameter null } } @@ -114,9 +121,8 @@ public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode List players = service.getPlayersDto(lobbyCode); boolean isStarted = service.getIsStarted(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted)); + new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted, null)); // 5. Parameter null } - /** * Handles a request to select a team and role for a player. * @@ -125,29 +131,27 @@ public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode */ @PostMapping("/{lobbyCode}/select-position") public ResponseEntity selectPosition( - @PathVariable String lobbyCode, @RequestBody PlayerDto request) { + @PathVariable String lobbyCode, @RequestBody PlayerDto request) { boolean updated = - service.selectPosition(request.username(), lobbyCode, request.team(), request.role()); + service.selectPosition(request.username(), lobbyCode, request.team(), request.role()); if (updated) { - return ResponseEntity.ok( - new LobbyResponse( - "Position selected successfully.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false)); + new LobbyResponse( + "Position selected successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false, null)); // 5. Parameter null } else { return ResponseEntity.badRequest() - .body( - new LobbyResponse( - "Could not assign selected team/role.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false)); + .body( + new LobbyResponse( + "Could not assign selected team/role.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false, null)); // 5. Parameter null } } - /** * Endpoint for starting a game, this is the last http-request only the host can make. * @@ -158,17 +162,17 @@ public ResponseEntity selectPosition( */ @GetMapping("/{lobbyCode}/start-game") public ResponseEntity startGame( - @PathVariable String lobbyCode, @RequestParam String username) { + @PathVariable String lobbyCode, @RequestParam String username) { boolean isStarted = service.startGame(lobbyCode, username); if (isStarted) { return ResponseEntity.ok( - new LobbyResponse( - "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), true)); + new LobbyResponse( + "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), true, null)); // 5. Parameter null } return ResponseEntity.badRequest() - .body( - new LobbyResponse( - "Could not start the game.", lobbyCode, service.getPlayersDto(lobbyCode), false)); + .body( + new LobbyResponse( + "Could not start the game.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); // 5. Parameter null } -} +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java index 5fd9ab77..81939b4b 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java @@ -71,7 +71,8 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) log.error(e.getMessage()); } - boolean joined = lobbyService.joinLobby(message.getName(), message.getCode(), message.getUuid()); + Player joinedPlayer = lobbyService.joinLobby(message.getName(), message.getCode(), message.getUuid()); + boolean joined = (joinedPlayer != null); boolean reconnect = lobbyService.getPlayers(message.getCode()).stream() .anyMatch(player -> player.username().equals(message.getName())); diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java index bf864a20..2368a4f4 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java @@ -11,5 +11,6 @@ public record LobbyResponse( String message, String lobbyCode, List playerList, - boolean isStarted + boolean isStarted, + String uuid ) {} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index ee92cf0e..3b9b6a5d 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -85,14 +85,25 @@ private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player successfully joined, {@code false} otherwise */ - public boolean joinLobby(String username, String lobbyCode, String uuid) { + public Player joinLobby(String username, String lobbyCode, String uuid) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { log.info("{}: a player has joined", lobbyCode); return lobby.addPlayer(username, uuid); } log.error("{}: an error occurred when joining lobby", lobbyCode); - return false; + return null; + } + + public Player getPlayer(String lobbyCode, String username) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerList().stream() + .filter(p -> p.username().equals(username)) + .findFirst() + .orElse(null); + } + return null; } /** diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index a7205bd2..229e5caa 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.UUID; import lombok.Getter; /** @@ -48,15 +49,17 @@ public Lobby(String lobbyCode, String username) { * @param isHost whether the player is the host of the lobby * @return {@code true} if the player was added, {@code false} otherwise */ - public boolean addPlayer(String username, boolean isHost) { + public Player addPlayer(String username, boolean isHost) { boolean alreadyExists = playerList.stream().anyMatch(p -> p.username().equals(username)); if (alreadyExists || playerList.size() >= MAX_PLAYERS) { - return false; + return null; } + String newUuid = UUID.randomUUID().toString(); + Player newPlayer = new Player(username, isHost, newUuid); playerList.add(new Player(username, isHost, null)); - return true; + return newPlayer; } /** @@ -66,16 +69,8 @@ public boolean addPlayer(String username, boolean isHost) { * @param username the username of the player * @return {@code true} if the player was added, {@code false} otherwise */ - public boolean addPlayer(String username, String uuid) { - boolean alreadyExists = - playerList.stream().anyMatch(p -> p.username().equals(username)); - - if (alreadyExists || playerList.size() >= MAX_PLAYERS) { - return false; - } - - playerList.add(new Player(username, false, uuid)); - return true; + public Player addPlayer(String username, String uuid) { + return addPlayer(username, false); } /** diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java index 1be43b69..e20ef83a 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java @@ -10,6 +10,7 @@ import com.codenames.codenames.backend.lobby.application.LobbyService; import com.codenames.codenames.backend.lobby.domain.Role; import com.codenames.codenames.backend.lobby.domain.Team; +import com.codenames.codenames.backend.lobby.domain.Player; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +29,7 @@ class LobbyControllerTest { @Test void createLobbyShouldReturn200() throws Exception { when(service.createLobby("TestUser")).thenReturn("ABCDE"); + when(service.getPlayer("ABCDE", "TestUser")).thenReturn(new Player("TestUser", true, "mock-uuid")); mockMvc .perform(get("/lobby/create").param("username", "TestUser")) @@ -60,12 +62,13 @@ void createLobbyNullLobbyCode() throws Exception { @Test void joinLobbyShouldReturn200WhenSuccess() throws Exception { - when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); + when(service.joinLobby("TestUser", "ABCDE", null)) + .thenReturn(new Player("TestUser", false, "mock-uuid-123")); mockMvc - .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); + .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); } @Test @@ -78,12 +81,12 @@ void getLobbyInfoShouldReturn200() throws Exception { @Test void joinLobbyShouldReturn400WhenNotFound() throws Exception { - when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); + when(service.joinLobby("TestUser", "XXXXX", null)).thenReturn(null); mockMvc - .perform(get("/lobby/XXXXX/join").param("username", "TestUser")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); + .perform(get("/lobby/XXXXX/join").param("username", "TestUser")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java index 058e7435..f2a85b52 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java @@ -51,6 +51,7 @@ void shouldRegisterJoinAndRegisterSession() { JoinMessage msg = new JoinMessage(); msg.setName("Max"); msg.setCode("ABCDE"); + msg.setUuid(null); SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(); @@ -59,14 +60,14 @@ void shouldRegisterJoinAndRegisterSession() { accessor.setSessionAttributes(attrs); - when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); + when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn(new Player("Max", true, "mock-uuid")); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true, "mock-uuid"))); when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); - verify(lobbyService).joinLobby("Max", "ABCDE"); + verify(lobbyService).joinLobby("Max", "ABCDE", null); assertEquals("Max", sessionRegistry.getUser("123")); assertEquals("ABCDE", sessionRegistry.getLobby("123")); @@ -88,9 +89,8 @@ void shouldSendErrorMessageWhenJoinFails() { attrs.put("sessionId", "123"); accessor.setSessionAttributes(attrs); - when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(false); + when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn(null); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of()); - controller.join(msg, accessor); verify(messagingTemplate).convertAndSend("/topic/errors/123", "Join failed"); @@ -125,8 +125,8 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { attrs.put("sessionId", "123"); accessor.setSessionAttributes(attrs); - when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); + when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn(new Player("Max", true, "mock-uuid")); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true, "mock-uuid"))); when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); @@ -134,7 +134,7 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { assertEquals("Max", sessionRegistry.getUser("123")); assertEquals("ABCDE", sessionRegistry.getLobby("123")); - verify(lobbyService).joinLobby("Max", "ABCDE"); + verify(lobbyService).joinLobby("Max", "ABCDE", null); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } @@ -145,6 +145,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { JoinMessage msg = new JoinMessage(); msg.setName("Max"); msg.setCode("ABCDE"); + msg.setUuid("existing-uuid"); SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(); @@ -152,8 +153,8 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { attrs.put("sessionId", "reconnect-1"); accessor.setSessionAttributes(attrs); - when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(false); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); + when(lobbyService.joinLobby("Max", "ABCDE", "existing-uuid")).thenReturn(null); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true, "existing-uuid"))); when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java index f6d943b1..6fb10336 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java @@ -54,9 +54,9 @@ void setup() { @Test void createLobbyReturnLobbyCode() { lobbyService.createLobby("Host"); - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); + Player result = lobbyService.joinLobby("TestUser", "ABCDE", null); - assertTrue(result); + assertNotNull(result); List players = lobbyService.getPlayers("ABCDE"); assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); @@ -80,8 +80,8 @@ void createLobbyLobbyCodeIsBlank() { @Test void joinLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); - assertFalse(result); + Player result = lobbyService.joinLobby("TestUser", "ABCDE", null); + assertNull(result); } @Test @@ -141,7 +141,7 @@ void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { @Test void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + lobbyService.joinLobby("P1", "ABCDE", null); boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); @@ -153,7 +153,7 @@ void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { @Test void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + lobbyService.joinLobby("P1", "ABCDE", null); boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); @@ -165,7 +165,7 @@ void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { @Test void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + lobbyService.joinLobby("P1", "ABCDE", null); boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); @@ -186,11 +186,11 @@ void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { lobbyService.createLobby("Host"); - boolean first = lobbyService.joinLobby("Max", "ABCDE"); - boolean second = lobbyService.joinLobby("Max", "ABCDE"); + Player first = lobbyService.joinLobby("Max", "ABCDE", null); + Player second = lobbyService.joinLobby("Max", "ABCDE", null); - assertTrue(first); - assertFalse(second); + assertNotNull(first); + assertNull(second); List players = lobbyService.getPlayers("ABCDE"); @@ -272,7 +272,7 @@ void testLobbyIsRemovedWhenItIsEmpty() { @Test void testLobbyIsNotRemovedWhenItHasPlayers() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("Player1", "ABCDE"); + lobbyService.joinLobby("Player1", "ABCDE", null); lobbyService.checkLobbyStillHasPlayers("ABCDE"); assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); } @@ -345,8 +345,8 @@ void testGetIsStartedGameServiceReturnsFalse() { @Test void testGetHostWorks() { lobbyService.createLobby("Alice"); - lobbyService.joinLobby("Bob", "ABCDE"); - lobbyService.joinLobby("Caesar", "ABCDE"); + lobbyService.joinLobby("Bob", "ABCDE", null); + lobbyService.joinLobby("Caesar", "ABCDE", null); String expected = "Alice"; String result = lobbyService.getHost("ABCDE"); @@ -376,7 +376,7 @@ void testRestoreLobbyAddsLobbyToLobbyList() { @Test void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("Player", "ABCDE"); + lobbyService.joinLobby("Player", "ABCDE", null); lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); Map> snapshots = lobbyService.getLobbySnapshots(); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java index ed520e54..50d90a1b 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -1,12 +1,9 @@ package com.codenames.codenames.backend.lobby.domain; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + /** * Tests for {@link Lobby}. * @@ -27,7 +24,7 @@ void constructorShouldInitializeLobbyCorrectly() { void addPlayerShouldAddPlayer() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.addPlayer("P1"); + lobby.addPlayer("P1", null); assertEquals(2, lobby.getPlayerList().size()); assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); @@ -37,10 +34,10 @@ void addPlayerShouldAddPlayer() { void addPlayerShouldNotExceedMaxPlayers() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.addPlayer("P1"); - lobby.addPlayer("P2"); - lobby.addPlayer("P3"); - lobby.addPlayer("P4"); + lobby.addPlayer("P1", null); + lobby.addPlayer("P2", null); + lobby.addPlayer("P3", null); + lobby.addPlayer("P4", null); assertEquals(4, lobby.getPlayerList().size()); } @@ -49,7 +46,7 @@ void addPlayerShouldNotExceedMaxPlayers() { void removePlayerShouldRemovePlayer() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.addPlayer("P1"); + lobby.addPlayer("P1", null); lobby.removePlayer("P1"); assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); @@ -68,11 +65,11 @@ void removePlayerShouldDoNothingIfPlayerNotExists() { void addPlayerShouldNotAddDuplicatePlayer() { Lobby lobby = new Lobby("ABCDE", "Host"); - boolean first = lobby.addPlayer("Max"); - boolean second = lobby.addPlayer("Max"); + Player first = lobby.addPlayer("Max", null); + Player second = lobby.addPlayer("Max", null); - assertTrue(first); - assertFalse(second); + assertNotNull(first); + assertNull(second); long count = lobby.getPlayerList().stream().filter(p -> p.username().equals("Max")).count(); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java index 2edc0ea1..a231174b 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java @@ -8,7 +8,7 @@ class PlayerTest { @Test void shouldReturnUsername() { - Player player = new Player("Max", false); + Player player = new Player("Max", false, null); assertEquals("Max", player.username()); } From a867f2a89a52bd6119cf9736e0960433aebb3c1d Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 14:04:24 +0200 Subject: [PATCH 03/34] feat: prevent fake players by verifying UUID on rejoin --- .../codenames/backend/lobby/domain/Lobby.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index 229e5caa..9b8a5fc8 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.UUID; import lombok.Getter; /** @@ -49,19 +48,37 @@ public Lobby(String lobbyCode, String username) { * @param isHost whether the player is the host of the lobby * @return {@code true} if the player was added, {@code false} otherwise */ - public Player addPlayer(String username, boolean isHost) { - boolean alreadyExists = playerList.stream().anyMatch(p -> p.username().equals(username)); - if (alreadyExists || playerList.size() >= MAX_PLAYERS) { + public Player addPlayer(String username, boolean isHost, String requestedUuid) { + java.util.Optional existingPlayerOpt = playerList.stream() + .filter(p -> p.username().equals(username)) + .findFirst(); + + if (existingPlayerOpt.isPresent()) { + Player existingPlayer = existingPlayerOpt.get(); + + if (existingPlayer.uuid().equals(requestedUuid)) { + return existingPlayer; + } + return null; } - String newUuid = UUID.randomUUID().toString(); + if (playerList.size() >= MAX_PLAYERS) { + return null; + } + + String newUuid = java.util.UUID.randomUUID().toString(); Player newPlayer = new Player(username, isHost, newUuid); - playerList.add(new Player(username, isHost, null)); + playerList.add(newPlayer); + return newPlayer; } + public Player addPlayer(String username, boolean isHost) { + return addPlayer(username, isHost, null); + } + /** * Adds a player to the lobby if capacity allows and the username is unique. Calls {@link * #addPlayer(String, boolean)} with {@code false} as the second argument @@ -70,7 +87,7 @@ public Player addPlayer(String username, boolean isHost) { * @return {@code true} if the player was added, {@code false} otherwise */ public Player addPlayer(String username, String uuid) { - return addPlayer(username, false); + return addPlayer(username, false, uuid); } /** From 8cf89addd9a3c3efb5a73b2662cc3c0406476b7c Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 14:24:39 +0200 Subject: [PATCH 04/34] feat: restrict joins for running games --- .../backend/lobby/api/LobbyController.java | 30 ++++++------ .../lobby/application/LobbyService.java | 48 ++++++++++++++----- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index 66d7b77f..f83cc8d7 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -47,11 +47,10 @@ public ResponseEntity createLobby(@RequestParam String username) String lobbyCode = service.createLobby(username); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "", null, false, null)); // 5. Parameter null + .body(new LobbyResponse("Error while creating lobby.", "", null, false, null)); } else { List players = service.getPlayersDto(lobbyCode); - // UUID des Hosts abrufen und mitsenden Player host = service.getPlayer(lobbyCode, username); String hostUuid = (host != null) ? host.uuid() : null; @@ -69,18 +68,19 @@ public ResponseEntity createLobby(@RequestParam String username) */ @GetMapping("/{lobbyCode}/join") public ResponseEntity joinLobby( - @RequestParam String username, @PathVariable String lobbyCode) { + @RequestParam String username, + @PathVariable String lobbyCode, + @RequestParam(required = false) String uuid) { - // GEÄNDERT: Nimmt jetzt ein Player-Objekt zurück anstatt boolean - Player joinedPlayer = service.joinLobby(username, lobbyCode, null); + Player joinedPlayer = service.joinLobby(username, lobbyCode, uuid); - if (joinedPlayer != null) { // GEÄNDERT: Prüft, ob das Objekt nicht null ist + if (joinedPlayer != null) { return ResponseEntity.ok( new LobbyResponse( - "Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, joinedPlayer.uuid())); // UUID an Frontend geschickt + "Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, joinedPlayer.uuid())); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); // 5. Parameter null + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); } } @@ -102,10 +102,10 @@ public ResponseEntity leaveLobby( return ResponseEntity.ok( new LobbyResponse( - "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); // 5. Parameter null + "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); // 5. Parameter null + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); } } @@ -121,7 +121,7 @@ public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode List players = service.getPlayersDto(lobbyCode); boolean isStarted = service.getIsStarted(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted, null)); // 5. Parameter null + new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted, null)); } /** * Handles a request to select a team and role for a player. @@ -141,7 +141,7 @@ public ResponseEntity selectPosition( "Position selected successfully.", lobbyCode, service.getPlayersDto(lobbyCode), - false, null)); // 5. Parameter null + false, null)); } else { return ResponseEntity.badRequest() .body( @@ -149,7 +149,7 @@ public ResponseEntity selectPosition( "Could not assign selected team/role.", lobbyCode, service.getPlayersDto(lobbyCode), - false, null)); // 5. Parameter null + false, null)); } } /** @@ -168,11 +168,11 @@ public ResponseEntity startGame( if (isStarted) { return ResponseEntity.ok( new LobbyResponse( - "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), true, null)); // 5. Parameter null + "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), true, null)); } return ResponseEntity.badRequest() .body( new LobbyResponse( - "Could not start the game.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); // 5. Parameter null + "Could not start the game.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); } } \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index 3b9b6a5d..e080a13e 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -87,22 +87,28 @@ private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { */ public Player joinLobby(String username, String lobbyCode, String uuid) { Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { - log.info("{}: a player has joined", lobbyCode); + if (gameService.isGameStarted(lobbyCode)) { + + Player existingPlayer = lobby.getPlayerList().stream() + .filter(p -> p.username().equals(username)) + .findFirst() + .orElse(null); + + if (existingPlayer == null || !existingPlayer.uuid().equals(uuid)) { + log.warn("{}: join rejected. Game already started and no valid UUID provided for reconnect.", lobbyCode); + return null; + } + log.info("{}: player reconnected to a running game", lobbyCode); + } else { + log.info("{}: a player has joined", lobbyCode); + } + return lobby.addPlayer(username, uuid); } - log.error("{}: an error occurred when joining lobby", lobbyCode); - return null; - } - public Player getPlayer(String lobbyCode, String username) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerList().stream() - .filter(p -> p.username().equals(username)) - .findFirst() - .orElse(null); - } + log.error("{}: an error occurred when joining lobby", lobbyCode); return null; } @@ -282,6 +288,24 @@ public Role getPlayerRole(String username, String lobbyCode) { return null; } + /** + * Retrieves a specific player from a lobby. + * + * @param lobbyCode the lobby code + * @param username the username of the player + * @return the Player object, or null if not found + */ + public Player getPlayer(String lobbyCode, String username) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerList().stream() + .filter(p -> p.username().equals(username)) + .findFirst() + .orElse(null); + } + return null; + } + /** * The service method for starting a game. This creates a game manager object for the lobby and * checks if the requesting user is liable to start the game. From fabd4842b80298f9c59da4a7382403b2fc75e812 Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 14:58:56 +0200 Subject: [PATCH 05/34] test: add tests for UUID and rejoin Lobby feature --- .../lobby/application/LobbyServiceTest.java | 38 ++++++++++++++++--- .../backend/lobby/domain/LobbyTest.java | 31 +++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java index 6fb10336..c887ce52 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java @@ -385,13 +385,41 @@ void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { assertEquals(2, snapshots.get("ABCDE").size()); PlayerDto host = - snapshots.get("ABCDE").stream() - .filter(player -> player.username().equals("Host")) - .findFirst() - .orElseThrow(); + snapshots.get("ABCDE").stream() + .filter(player -> player.username().equals("Host")) + .findFirst() + .orElseThrow(); assertEquals(Team.RED, host.team()); assertEquals(Role.SPYMASTER, host.role()); assertTrue(host.isHost()); } -} + + @Test + void joinLobbyShouldBlockNewPlayerIfGameStarted() { + when(gameService.isGameStarted("ABCDE")).thenReturn(true); + + Player result = lobbyService.joinLobby("NewPlayer", "ABCDE", null); + + assertNull(result); + } + + @Test + void joinLobbyShouldAllowReconnectIfGameStarted() { + lobbyService.getLobbyList().clear(); + + String lobbyCode = lobbyService.createLobby("Host"); + + when(gameService.isGameStarted(lobbyCode)).thenReturn(false); + + Player p1 = lobbyService.joinLobby("P1", lobbyCode, null); + assertNotNull(p1); + + when(gameService.isGameStarted(lobbyCode)).thenReturn(true); + + Player result = lobbyService.joinLobby("P1", lobbyCode, p1.uuid()); + + assertNotNull(result); + assertEquals(p1.uuid(), result.uuid()); + } +} \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java index 50d90a1b..c2d065af 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -129,4 +129,35 @@ void startingTeamShouldBeEitherRedOrBlue() { assertTrue(startingTeam == Team.RED || startingTeam == Team.BLUE); } + + @Test + void addPlayerShouldAssignUuidToNewPlayer() { + Lobby lobby = new Lobby("ABCDE", "Host"); + Player newPlayer = lobby.addPlayer("P1", false); + + assertNotNull(newPlayer.uuid()); + } + + @Test + void addPlayerShouldAllowReconnectWithCorrectUuid() { + Lobby lobby = new Lobby("ABCDE", "Host"); + Player first = lobby.addPlayer("P1", false); + String validUuid = first.uuid(); + + Player reconnected = lobby.addPlayer("P1", false, validUuid); + + assertNotNull(reconnected); + assertEquals(validUuid, reconnected.uuid()); + } + + @Test + void addPlayerShouldBlockReconnectWithWrongUuid() { + Lobby lobby = new Lobby("ABCDE", "Host"); + lobby.addPlayer("P1", false); + + + Player blocked = lobby.addPlayer("P1", false, "WRONG-UUID"); + + assertNull(blocked); + } } From 7a058a453be89cb2cba28b645db0451f972b6b9e Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 15:18:48 +0200 Subject: [PATCH 06/34] test: fix failing test --- .../codenames/backend/database/entity/PlayerEntity.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java index d62d8eb0..75012f4e 100644 --- a/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java +++ b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java @@ -8,9 +8,11 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.PrePersist; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.UUID; /** * Persistent entity representing a player record within the database schema. @@ -37,4 +39,11 @@ public class PlayerEntity { private String role; @Column(nullable = false, unique = true) private String uuid; + + @PrePersist + protected void onCreate() { + if (this.uuid == null) { + this.uuid = UUID.randomUUID().toString(); + } + } } From 94bc5cb4c8832017da32c08ca297aeed71a10681 Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 21:05:41 +0200 Subject: [PATCH 07/34] fix: add migration for player UUID --- .../resources/db/migration/V2__add_player_uuid.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/resources/db/migration/V2__add_player_uuid.sql diff --git a/src/main/resources/db/migration/V2__add_player_uuid.sql b/src/main/resources/db/migration/V2__add_player_uuid.sql new file mode 100644 index 00000000..1a73e104 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_player_uuid.sql @@ -0,0 +1,12 @@ +ALTER TABLE player + ADD COLUMN uuid VARCHAR(36); + +UPDATE player +SET uuid = gen_random_uuid()::text; + +ALTER TABLE player + ALTER COLUMN uuid SET NOT NULL; + +ALTER TABLE player + ADD CONSTRAINT unique_player_uuid + UNIQUE (uuid); \ No newline at end of file From e49ab6abd6562ba0f13eaf246f7ecfd6be737359 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 21:38:16 +0200 Subject: [PATCH 08/34] feat: add player UUID to persistence mapping --- .../backend/database/persistence/PersistenceMapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java index 7e0531d9..cda2bf2f 100644 --- a/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java +++ b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java @@ -53,6 +53,7 @@ private List mapPlayer( playerEntity.setIsHost(players.get(i).isHost()); playerEntity.setTeam(players.get(i).team().name()); playerEntity.setRole(players.get(i).role().name()); + playerEntity.setUuid(players.get(i).uuid()); playerList.add(playerEntity); } return playerList; From 97a2a3dd2b049abc23ef3529773ea56a23a1eeac Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 21:38:38 +0200 Subject: [PATCH 09/34] feat: add player UUID to Player DTO --- .../codenames/codenames/backend/lobby/api/dto/PlayerDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java index 395c7e71..ace76666 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java @@ -11,5 +11,5 @@ * @param role the user's current role, this can be null * @param isHost whether the user is host */ -public record PlayerDto(String username, Team team, Role role, boolean isHost) { +public record PlayerDto(String username, Team team, Role role, boolean isHost, String uuid) { } From 581d4b449fb0ba4a3f81cf0cd7f33e52d0a3c429 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 21:48:39 +0200 Subject: [PATCH 10/34] feat: add uuid to restoration mapping --- .../database/restoration/RestorationMapper.java | 6 +++++- .../codenames/backend/lobby/domain/Lobby.java | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java index 0771e25d..e5eeaf48 100644 --- a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java +++ b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java @@ -92,7 +92,8 @@ private List mapToPlayerDto(LobbyEntity lobbyEntity) { playerEntity.getUsername(), team, role, - playerEntity.getIsHost()); + playerEntity.getIsHost(), + playerEntity.getUuid()); playerList.add(playerDto); } return playerList; @@ -113,6 +114,9 @@ private Lobby buildLobby(String lobbyCode, List players) { if (player.role() != null) { lobby.setPlayerRole(player.username(), player.role()); } + if (player.uuid() != null) { + lobby.setPlayerUuid(player.username(), player.uuid()); + } } return lobby; diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index 9b8a5fc8..53ad90d4 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -131,6 +131,23 @@ public void setPlayerRole(String username, Role role) { playerRoles.put(username, role); } + /** + * Sets the UUID for a player by replacing their Player record. + * Used during restoration to preserve UUIDs from the database. + * + * @param username the username of the player + * @param uuid the UUID to assign + */ + public void setPlayerUuid(String username, String uuid) { + for (int i = 0; i < playerList.size(); i++) { + Player p = playerList.get(i); + if (p.username().equals(username)) { + playerList.set(i, new Player(p.username(), p.isHost(), uuid)); + return; + } + } + } + /** * Returns the team of a player. * From b78de6de711209999f707826ce7f80dfa5f84184 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 21:50:29 +0200 Subject: [PATCH 11/34] feat: add parameter to correctly create playerDto in LobbyService --- .../codenames/backend/lobby/application/LobbyService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index e080a13e..c27ce0cf 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -212,7 +212,8 @@ public List getPlayersDto(String lobbyCode) { player.username(), lobby.getPlayerTeam(player.username()), lobby.getPlayerRole(player.username()), - player.isHost())) + player.isHost(), + player.uuid())) .toList(); } return List.of(); From 98181a9d511a6406502ec11c26c1cd9cea13b353 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 21:51:25 +0200 Subject: [PATCH 12/34] test: update test to intake uuid as parameter for PlayerDto --- .../database/persistence/PersistenceMappingTest.java | 4 ++-- .../database/persistence/PersistenceServiceTest.java | 4 ++-- .../codenames/backend/lobby/api/LobbyControllerTest.java | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java index 907a08ea..7236dbd2 100644 --- a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java +++ b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java @@ -52,8 +52,8 @@ void setup() { mockClueValidationService = mock(ClueValidationService.class); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - player1 = new PlayerDto("Test1", redTeam, spymaster, true); - player2 = new PlayerDto("Test2", redTeam, operative, false); + player1 = new PlayerDto("Test1", redTeam, spymaster, true, null); + player2 = new PlayerDto("Test2", redTeam, operative, false, null); playerDtoList = List.of(player1, player2); lobbyEntity = diff --git a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java index d8a7708e..0a788df5 100644 --- a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java @@ -77,8 +77,8 @@ void setUp() { gameManager = helperMethodGenerateFullCardList(redColor, redTeam); - player1 = new PlayerDto("Test1", redTeam, spymaster, true); - player2 = new PlayerDto("Test2", redTeam, operative, false); + player1 = new PlayerDto("Test1", redTeam, spymaster, true, null); + player2 = new PlayerDto("Test2", redTeam, operative, false, null); playerDtoList = List.of(player1, player2); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java index e20ef83a..dea843fe 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java @@ -74,7 +74,7 @@ void joinLobbyShouldReturn200WhenSuccess() throws Exception { @Test void getLobbyInfoShouldReturn200() throws Exception { when(service.getPlayersDto("ABCDE")) - .thenReturn(List.of(new PlayerDto("test", null, null, true))); + .thenReturn(List.of(new PlayerDto("test", null, null, true, null))); String url = "/lobby/ABCDE"; mockMvc.perform(get(url)).andExpect(status().isOk()); } @@ -158,7 +158,7 @@ void selectPositionShouldReturn400WhenAssignmentFails() throws Exception { @Test void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); + List.of(new PlayerDto("Alice", null, null, true, null), new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); @@ -174,7 +174,7 @@ void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { @Test void testStartGameReturns200WhenConditionIsMet() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); + List.of(new PlayerDto("Alice", null, null, true, null), new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(true); @@ -191,7 +191,7 @@ void testStartGameReturns200WhenConditionIsMet() throws Exception { @Test void testStartGameReturns400WhenServiceReturnsFalse() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); + List.of(new PlayerDto("Alice", null, null, true, null), new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(false); From 501d7f0e3bee1e49bdeebf2fd83cdf18c42ec57f Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 22:17:17 +0200 Subject: [PATCH 13/34] feat: add V3 Flyway Script to update player table and have UUID as PK --- .../db/migration/V3__swap_player_pk_to_uuid.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/resources/db/migration/V3__swap_player_pk_to_uuid.sql diff --git a/src/main/resources/db/migration/V3__swap_player_pk_to_uuid.sql b/src/main/resources/db/migration/V3__swap_player_pk_to_uuid.sql new file mode 100644 index 00000000..2adb844d --- /dev/null +++ b/src/main/resources/db/migration/V3__swap_player_pk_to_uuid.sql @@ -0,0 +1,12 @@ +-- Swap player PK from auto-generated BIGINT id to application-generated UUID +-- No other table references player.id, so no FK rewiring needed + +ALTER TABLE player + DROP COLUMN id CASCADE; + +ALTER TABLE player + ADD PRIMARY KEY (uuid); + +-- V2 Constraint no longer needed since i set it to PK +ALTER TABLE player + DROP CONSTRAINT unique_player_uuid; From 47cfd0ebd30d0759ff13f3483ce7ed48df3dfa19 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 22:17:57 +0200 Subject: [PATCH 14/34] feat: update PlayerEntity class to have UUID as PK --- .../backend/database/entity/PlayerEntity.java | 17 ++--------------- .../database/repository/PlayerRepository.java | 2 +- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java index 75012f4e..c7beedfc 100644 --- a/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java +++ b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java @@ -2,17 +2,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import jakarta.persistence.PrePersist; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.UUID; /** * Persistent entity representing a player record within the database schema. @@ -24,8 +20,8 @@ @Table(name = "player") public class PlayerEntity { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Column(nullable = false) + private String uuid; @ManyToOne @JoinColumn(name = "lobby_code", nullable = false) private LobbyEntity lobbyEntity; @@ -37,13 +33,4 @@ public class PlayerEntity { private String team; @Column(length = 9, nullable = false) private String role; - @Column(nullable = false, unique = true) - private String uuid; - - @PrePersist - protected void onCreate() { - if (this.uuid == null) { - this.uuid = UUID.randomUUID().toString(); - } - } } diff --git a/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java b/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java index 6bb4a82c..26056bb2 100644 --- a/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java +++ b/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java @@ -9,7 +9,7 @@ * Repository class for Lobby that has the default available methods needed to perform SQL queries. */ @Repository -public interface PlayerRepository extends JpaRepository { +public interface PlayerRepository extends JpaRepository { /** * Custom SQL query that is not included in the JpaRepository interface. From a7511e59aef839b97b9e66cd24e107ac99cea8a0 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 22:22:53 +0200 Subject: [PATCH 15/34] test: ensure uuid is mapped correctly --- .../backend/database/persistence/PersistenceMappingTest.java | 5 +++-- .../backend/database/persistence/PersistenceServiceTest.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java index 7236dbd2..ca87bb19 100644 --- a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java +++ b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java @@ -52,8 +52,8 @@ void setup() { mockClueValidationService = mock(ClueValidationService.class); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - player1 = new PlayerDto("Test1", redTeam, spymaster, true, null); - player2 = new PlayerDto("Test2", redTeam, operative, false, null); + player1 = new PlayerDto("Test1", redTeam, spymaster, true, "test-uuid-1"); + player2 = new PlayerDto("Test2", redTeam, operative, false, "test-uuid-2"); playerDtoList = List.of(player1, player2); lobbyEntity = @@ -86,6 +86,7 @@ void testMapPlayer() { assertTrue(retrievedPlayer1.getIsHost()); assertEquals("RED", retrievedPlayer1.getTeam()); assertEquals("SPYMASTER", retrievedPlayer1.getRole()); + assertEquals("test-uuid-1", retrievedPlayer1.getUuid()); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java index 0a788df5..5cd04897 100644 --- a/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java @@ -77,8 +77,8 @@ void setUp() { gameManager = helperMethodGenerateFullCardList(redColor, redTeam); - player1 = new PlayerDto("Test1", redTeam, spymaster, true, null); - player2 = new PlayerDto("Test2", redTeam, operative, false, null); + player1 = new PlayerDto("Test1", redTeam, spymaster, true, "test-uuid-1"); + player2 = new PlayerDto("Test2", redTeam, operative, false, "test-uuid-2"); playerDtoList = List.of(player1, player2); @@ -141,6 +141,7 @@ void testSaveSnapshot_player() { assertTrue(playerEntity.getIsHost()); assertEquals("RED", playerEntity.getTeam()); assertEquals("SPYMASTER", playerEntity.getRole()); + assertEquals("test-uuid-1", playerEntity.getUuid()); } @Test From 747b6649545241e32749d82dd4fed3b2896240f5 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 22:29:42 +0200 Subject: [PATCH 16/34] test: update mapping test to check uuid --- .../backend/database/restoration/RestorationMapperTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationMapperTest.java b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationMapperTest.java index 6c763eff..140b37ee 100644 --- a/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationMapperTest.java +++ b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationMapperTest.java @@ -40,6 +40,7 @@ void testMapToLobbyDto() { playerEntity.setIsHost(true); playerEntity.setTeam("RED"); playerEntity.setRole("OPERATIVE"); + playerEntity.setUuid("test-uuid-1"); List playerList = List.of(playerEntity); lobbyEntity.setPlayerEntities(playerList); @@ -47,8 +48,8 @@ void testMapToLobbyDto() { assertNotNull(lobby); assertEquals("ABCDE", lobby.getLobbyCode()); - assertEquals(1, lobby.getPlayerList().size()); + assertEquals("test-uuid-1", lobby.getPlayerList().get(0).uuid()); } @Test @@ -143,5 +144,4 @@ void testBuildLobby_nullRole() { assertNull(lobby.getPlayerRole("TestPlayer2")); } - } From 236af043974b930d61d8510f789e2bcdee58f647 Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 23:14:43 +0200 Subject: [PATCH 17/34] style: apply google codestyle --- .../backend/lobby/api/LobbyController.java | 40 ++++++++++++++++--- .../lobby/api/LobbySocketController.java | 6 ++- .../lobby/application/LobbyService.java | 2 +- .../codenames/backend/lobby/domain/Lobby.java | 10 ++++- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index f83cc8d7..846982a9 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -55,7 +55,8 @@ public ResponseEntity createLobby(@RequestParam String username) String hostUuid = (host != null) ? host.uuid() : null; return ResponseEntity.ok( - new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false, hostUuid)); + new LobbyResponse( + "Successfully created Lobby.", lobbyCode, players, false, hostUuid)); } } @@ -77,7 +78,12 @@ public ResponseEntity joinLobby( if (joinedPlayer != null) { return ResponseEntity.ok( new LobbyResponse( - "Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, joinedPlayer.uuid())); + "Joined Lobby successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false, + joinedPlayer.uuid()) + ); } else { return ResponseEntity.badRequest() .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); @@ -102,7 +108,11 @@ public ResponseEntity leaveLobby( return ResponseEntity.ok( new LobbyResponse( - "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); + "Left lobby successfully.", + lobbyCode, service.getPlayersDto(lobbyCode), + false, + null) + ); } else { return ResponseEntity.badRequest() .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); @@ -121,7 +131,13 @@ public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode List players = service.getPlayersDto(lobbyCode); boolean isStarted = service.getIsStarted(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted, null)); + new LobbyResponse( + "Lobby info retrieved successfully.", + lobbyCode, + players, + isStarted, + null) + ); } /** * Handles a request to select a team and role for a player. @@ -129,6 +145,7 @@ public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode * @param request the position selection request containing username, lobby code, team, and role * @return a response indicating whether the selection was successful */ + @PostMapping("/{lobbyCode}/select-position") public ResponseEntity selectPosition( @PathVariable String lobbyCode, @RequestBody PlayerDto request) { @@ -160,6 +177,7 @@ public ResponseEntity selectPosition( * @return a response entity of a lobby response, with isStarted @code true or @code false, * whether the starting was successful or not */ + @GetMapping("/{lobbyCode}/start-game") public ResponseEntity startGame( @PathVariable String lobbyCode, @RequestParam String username) { @@ -168,11 +186,21 @@ public ResponseEntity startGame( if (isStarted) { return ResponseEntity.ok( new LobbyResponse( - "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), true, null)); + "Game is starting now.", + lobbyCode, + service.getPlayersDto(lobbyCode), + true, + null) + ); } return ResponseEntity.badRequest() .body( new LobbyResponse( - "Could not start the game.", lobbyCode, service.getPlayersDto(lobbyCode), false, null)); + "Could not start the game.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false, + null) + ); } } \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java index 81939b4b..ea23af3f 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java @@ -71,7 +71,11 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) log.error(e.getMessage()); } - Player joinedPlayer = lobbyService.joinLobby(message.getName(), message.getCode(), message.getUuid()); + Player joinedPlayer = lobbyService.joinLobby( + message.getName(), + message.getCode(), + message.getUuid() + ); boolean joined = (joinedPlayer != null); boolean reconnect = lobbyService.getPlayers(message.getCode()).stream() diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index c27ce0cf..cdeb3275 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -97,7 +97,7 @@ public Player joinLobby(String username, String lobbyCode, String uuid) { .orElse(null); if (existingPlayer == null || !existingPlayer.uuid().equals(uuid)) { - log.warn("{}: join rejected. Game already started and no valid UUID provided for reconnect.", lobbyCode); + log.warn("{}: join rejected. Game started, no valid reconnect UUID.", lobbyCode); return null; } log.info("{}: player reconnected to a running game", lobbyCode); diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index 53ad90d4..283021ac 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import lombok.Getter; @@ -68,12 +69,19 @@ public Player addPlayer(String username, boolean isHost, String requestedUuid) { return null; } - String newUuid = java.util.UUID.randomUUID().toString(); + String newUuid = UUID.randomUUID().toString(); Player newPlayer = new Player(username, isHost, newUuid); playerList.add(newPlayer); return newPlayer; } + /** + * Adds a player to lobby. + * + * @param username der Name des Spielers + * @param isHost gibt an, ob der Spieler der Host ist + * @return der erstellte Player + */ public Player addPlayer(String username, boolean isHost) { return addPlayer(username, isHost, null); From 10e1a2f3a6b4e378075c56bf654e1f4432aeaa78 Mon Sep 17 00:00:00 2001 From: 5eli Date: Sun, 14 Jun 2026 23:31:46 +0200 Subject: [PATCH 18/34] style: apply google codestyle to tests --- .../game/domain/GameManagerFactoryTest.java | 9 +++++++- .../lobby/api/LobbyControllerTest.java | 21 ++++++++++++------- .../lobby/api/LobbySocketControllerTest.java | 15 ++++++++----- .../backend/lobby/domain/LobbyTest.java | 8 +++++-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java index 7c0f3ac3..906920d3 100644 --- a/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java @@ -63,7 +63,14 @@ void testCreateFromSnapshotWithClue() { void testCreateFromSnapshotWithoutClue() { GameStateDto snapshot = new GameStateDto( - null, Team.BLUE, Role.SPYMASTER, null, 0, List.of(new CardDto("Tree", Color.BLUE, false))); + null, + Team.BLUE, + Role.SPYMASTER, + null, + 0, + List.of(new CardDto("Tree", + Color.BLUE, false)) + ); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java index dea843fe..ad19d70e 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java @@ -8,9 +8,9 @@ import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Player; import com.codenames.codenames.backend.lobby.domain.Role; import com.codenames.codenames.backend.lobby.domain.Team; -import com.codenames.codenames.backend.lobby.domain.Player; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,7 +29,11 @@ class LobbyControllerTest { @Test void createLobbyShouldReturn200() throws Exception { when(service.createLobby("TestUser")).thenReturn("ABCDE"); - when(service.getPlayer("ABCDE", "TestUser")).thenReturn(new Player("TestUser", true, "mock-uuid")); + when(service.getPlayer("ABCDE", + "TestUser")).thenReturn(new Player("TestUser", + true, + "mock-uuid") + ); mockMvc .perform(get("/lobby/create").param("username", "TestUser")) @@ -127,7 +131,7 @@ void selectPositionShouldReturn200whenSuccess() throws Exception { "role": "SPYMASTER", "isHost": "true" } - """)) + """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Position selected successfully.")) .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); @@ -149,7 +153,7 @@ void selectPositionShouldReturn400WhenAssignmentFails() throws Exception { "role": "SPYMASTER", "isHost": "true" } - """)) + """)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); @@ -158,7 +162,8 @@ void selectPositionShouldReturn400WhenAssignmentFails() throws Exception { @Test void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true, null), new PlayerDto("Bob", null, null, false, null)); + List.of(new PlayerDto("Alice", null, null, true, null), + new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); @@ -174,7 +179,8 @@ void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { @Test void testStartGameReturns200WhenConditionIsMet() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true, null), new PlayerDto("Bob", null, null, false, null)); + List.of(new PlayerDto("Alice", null, null, true, null), + new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(true); @@ -191,7 +197,8 @@ void testStartGameReturns200WhenConditionIsMet() throws Exception { @Test void testStartGameReturns400WhenServiceReturnsFalse() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true, null), new PlayerDto("Bob", null, null, false, null)); + List.of(new PlayerDto("Alice", null, null, true, null), + new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(false); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java index f2a85b52..09db3d22 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java @@ -60,9 +60,11 @@ void shouldRegisterJoinAndRegisterSession() { accessor.setSessionAttributes(attrs); - when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn(new Player("Max", true, "mock-uuid")); + when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn( + new Player("Max", true, "mock-uuid")); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true, "mock-uuid"))); + when(lobbyService.getPlayers("ABCDE")).thenReturn( + List.of(new Player("Max", true, "mock-uuid"))); when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); @@ -125,8 +127,10 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { attrs.put("sessionId", "123"); accessor.setSessionAttributes(attrs); - when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn(new Player("Max", true, "mock-uuid")); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true, "mock-uuid"))); + when(lobbyService.joinLobby("Max", "ABCDE", null)).thenReturn( + new Player("Max", true, "mock-uuid")); + when(lobbyService.getPlayers("ABCDE")).thenReturn( + List.of(new Player("Max", true, "mock-uuid"))); when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); @@ -154,7 +158,8 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { accessor.setSessionAttributes(attrs); when(lobbyService.joinLobby("Max", "ABCDE", "existing-uuid")).thenReturn(null); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true, "existing-uuid"))); + when(lobbyService.getPlayers("ABCDE")).thenReturn( + List.of(new Player("Max", true, "existing-uuid"))); when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java index c2d065af..a639ceba 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -1,8 +1,12 @@ package com.codenames.codenames.backend.lobby.domain; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; /** * Tests for {@link Lobby}. From 74cf70037b2773bd8ed596f7fbba85781765f3ac Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 23:52:25 +0200 Subject: [PATCH 19/34] test: add test for setting player UUID for restoration --- .../restoration/RestorationMapper.java | 4 ++++ .../codenames/backend/lobby/domain/Lobby.java | 3 ++- .../backend/lobby/domain/LobbyTest.java | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java index e5eeaf48..2addf2e9 100644 --- a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java +++ b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java @@ -101,6 +101,9 @@ private List mapToPlayerDto(LobbyEntity lobbyEntity) { private Lobby buildLobby(String lobbyCode, List players) { String hostUsername = findHostUsername(players); + // the way uuid is generated is a bit roundabout, but I do not have the time to refactor it + // and I do not want to touch the code if it works (Lobby.java) + // since constructor uses the two parameter method -> reroute to 3 parameter -> always null UUID Lobby lobby = new Lobby(lobbyCode, hostUsername); @@ -115,6 +118,7 @@ private Lobby buildLobby(String lobbyCode, List players) { lobby.setPlayerRole(player.username(), player.role()); } if (player.uuid() != null) { + // Solution: we overwrite all player records with data loaded from DB lobby.setPlayerUuid(player.username(), player.uuid()); } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index 283021ac..b7618d84 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -150,11 +150,12 @@ public void setPlayerUuid(String username, String uuid) { for (int i = 0; i < playerList.size(); i++) { Player p = playerList.get(i); if (p.username().equals(username)) { + // caller loops through all players in list on match we overwrite playerList.set(i, new Player(p.username(), p.isHost(), uuid)); return; } } - } + } // refer RestorationMapper as for why we overwrite player records /** * Returns the team of a player. diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java index a639ceba..5cfaf72d 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -164,4 +165,23 @@ void addPlayerShouldBlockReconnectWithWrongUuid() { assertNull(blocked); } + + @Test + void testSetPlayerUuid_playerFound(){ + Lobby lobby = new Lobby("ABCDE", "Host"); + lobby.setPlayerUuid("Host", "test-uuid-1"); + + Player player1 = lobby.getPlayerList().get(0); + assertEquals("test-uuid-1", player1.uuid()); + } + + @Test + void testSetPlayerUuid_playerNotFound(){ + Lobby lobby = new Lobby("ABCDE", "Host"); + lobby.setPlayerUuid("NotHost", "test-uuid-1"); + // if i don't hit the same username, it should not be overridden + + Player player1 = lobby.getPlayerList().get(0); + assertNotEquals("test-uuid-1", player1.uuid()); + } } From 081493c9084b7214016e90380d97ab7fada96b46 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sun, 14 Jun 2026 23:53:08 +0200 Subject: [PATCH 20/34] style: add spacing before { --- .../codenames/codenames/backend/lobby/domain/LobbyTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java index 5cfaf72d..61c6495c 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -160,14 +160,13 @@ void addPlayerShouldBlockReconnectWithWrongUuid() { Lobby lobby = new Lobby("ABCDE", "Host"); lobby.addPlayer("P1", false); - Player blocked = lobby.addPlayer("P1", false, "WRONG-UUID"); assertNull(blocked); } @Test - void testSetPlayerUuid_playerFound(){ + void testSetPlayerUuid_playerFound() { Lobby lobby = new Lobby("ABCDE", "Host"); lobby.setPlayerUuid("Host", "test-uuid-1"); @@ -176,7 +175,7 @@ void testSetPlayerUuid_playerFound(){ } @Test - void testSetPlayerUuid_playerNotFound(){ + void testSetPlayerUuid_playerNotFound() { Lobby lobby = new Lobby("ABCDE", "Host"); lobby.setPlayerUuid("NotHost", "test-uuid-1"); // if i don't hit the same username, it should not be overridden From b1f08911203595eb5a19efe5d79fffe90389d10c Mon Sep 17 00:00:00 2001 From: XtophB Date: Mon, 15 Jun 2026 20:22:08 +0200 Subject: [PATCH 21/34] style: add imports instead of manually calling packages --- .../codenames/backend/lobby/domain/Lobby.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index b7618d84..ee1acecd 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -1,10 +1,12 @@ package com.codenames.codenames.backend.lobby.domain; +import static java.util.UUID.randomUUID; + import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; +import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import lombok.Getter; @@ -51,7 +53,7 @@ public Lobby(String lobbyCode, String username) { */ public Player addPlayer(String username, boolean isHost, String requestedUuid) { - java.util.Optional existingPlayerOpt = playerList.stream() + Optional existingPlayerOpt = playerList.stream() .filter(p -> p.username().equals(username)) .findFirst(); @@ -69,7 +71,7 @@ public Player addPlayer(String username, boolean isHost, String requestedUuid) { return null; } - String newUuid = UUID.randomUUID().toString(); + String newUuid = randomUUID().toString(); Player newPlayer = new Player(username, isHost, newUuid); playerList.add(newPlayer); @@ -140,8 +142,7 @@ public void setPlayerRole(String username, Role role) { } /** - * Sets the UUID for a player by replacing their Player record. - * Used during restoration to preserve UUIDs from the database. + * Sets the UUID for a player by replacing their Player record, used by restoration of UUID. * * @param username the username of the player * @param uuid the UUID to assign From 9e1c1344af894ad689eee78f8f9d7350b350d44f Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 10:48:48 +0200 Subject: [PATCH 22/34] fix: authenticate leave and start game requests using UUID --- .../backend/lobby/api/LobbyController.java | 12 +++++----- .../lobby/application/LobbyService.java | 24 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index 846982a9..89c72113 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -93,15 +93,15 @@ public ResponseEntity joinLobby( /** * Handles a request to leave a lobby. * - * @param username the username of the player + * @param uuid the UUID of the player * @param lobbyCode the lobby code identifying the lobby * @return a response indicating whether the operation was successful */ @GetMapping("/{lobbyCode}/leave") public ResponseEntity leaveLobby( - @PathVariable String lobbyCode, @RequestParam String username) { + @PathVariable String lobbyCode, @RequestParam String uuid) { boolean wasStarted = service.getIsStarted(lobbyCode); - boolean left = service.leaveLobby(username, lobbyCode); + boolean left = service.leaveLobby(uuid, lobbyCode); if (left) { service.checkLobbyStillHasPlayers(lobbyCode); @@ -173,15 +173,15 @@ public ResponseEntity selectPosition( * Endpoint for starting a game, this is the last http-request only the host can make. * * @param lobbyCode the unique lobby code - * @param username the name of the requesting user + * @param uuid the UUID of the requesting user * @return a response entity of a lobby response, with isStarted @code true or @code false, * whether the starting was successful or not */ @GetMapping("/{lobbyCode}/start-game") public ResponseEntity startGame( - @PathVariable String lobbyCode, @RequestParam String username) { - boolean isStarted = service.startGame(lobbyCode, username); + @PathVariable String lobbyCode, @RequestParam String uuid) { + boolean isStarted = service.startGame(lobbyCode, uuid); if (isStarted) { return ResponseEntity.ok( diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index cdeb3275..a59b2cbc 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -125,14 +125,14 @@ public void restoreLobby(String lobbyCode, Lobby lobby) { /** * Removes a player from a lobby. * - * @param username the username of the player + * @param uuid the uuid of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player was removed, {@code false} if the lobby does not exist */ - public boolean leaveLobby(String username, String lobbyCode) { + public boolean leaveLobby(String uuid, String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { - lobby.removePlayer(username); + lobby.removePlayer(uuid); log.info("{}: a player left", lobbyCode); return true; } @@ -312,14 +312,18 @@ public Player getPlayer(String lobbyCode, String username) { * checks if the requesting user is liable to start the game. * * @param lobbyCode the unique lobby code - * @param username the name of the requesting user + * @param uuid the uuid of the requesting user * @return if starting was successful */ - public boolean startGame(String lobbyCode, String username) { + public boolean startGame(String lobbyCode, String uuid) { boolean isStarted = - !lobbyCode.isBlank() && !username.isBlank() && Objects.equals(getHost(lobbyCode), username); - Lobby lobby = lobbyList.get(lobbyCode); - addGameManagerForLobby(lobby, lobbyCode); + !lobbyCode.isBlank() + && !uuid.isBlank() + && Objects.equals(getHost(lobbyCode), uuid); + if (isStarted) { + Lobby lobby = lobbyList.get(lobbyCode); + addGameManagerForLobby(lobby, lobbyCode); + } log.info("{}: Game start requested, returning: {}", lobbyCode, isStarted); return isStarted; @@ -329,7 +333,7 @@ public boolean startGame(String lobbyCode, String username) { * This method computes the host of a lobby. * * @param lobbyCode the unique lobby code - * @return the username of the host + * @return the UUID of the host */ public String getHost(String lobbyCode) { if (lobbyCode == null || lobbyCode.isBlank()) { @@ -341,7 +345,7 @@ public String getHost(String lobbyCode) { } for (Player p : players) { if (p.isHost()) { - return p.username(); + return p.uuid(); } } return ""; From 44b1f9596739ce03737d4711dca530a098d48886 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 11:06:58 +0200 Subject: [PATCH 23/34] fix: remove unnecessary null-check --- .../codenames/backend/lobby/api/LobbyController.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index 89c72113..fc41a141 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -5,6 +5,7 @@ import com.codenames.codenames.backend.lobby.application.LobbyService; import com.codenames.codenames.backend.lobby.domain.Player; import java.util.List; +import java.util.Objects; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -51,8 +52,10 @@ public ResponseEntity createLobby(@RequestParam String username) } else { List players = service.getPlayersDto(lobbyCode); - Player host = service.getPlayer(lobbyCode, username); - String hostUuid = (host != null) ? host.uuid() : null; + Player host = Objects.requireNonNull( + service.getPlayer(lobbyCode, username)); + + String hostUuid = host.uuid(); return ResponseEntity.ok( new LobbyResponse( From 4e30486d0f7621c57d48a21baea4d71baa5336d8 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 11:10:44 +0200 Subject: [PATCH 24/34] fix: enforce UUID validation for player reconnects --- .../codenames/backend/lobby/api/LobbySocketController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java index ea23af3f..803a9926 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java @@ -79,7 +79,7 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) boolean joined = (joinedPlayer != null); boolean reconnect = lobbyService.getPlayers(message.getCode()).stream() - .anyMatch(player -> player.username().equals(message.getName())); + .anyMatch(player -> player.uuid().equals(message.getUuid())); if (!joined && !reconnect) { messagingTemplate.convertAndSend("/topic/errors/" + sessionId, "Join failed"); From b38cb4c827054c1536d9fa7f2f4aade23fa19088 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 11:17:22 +0200 Subject: [PATCH 25/34] fix: update javaDoc --- .../com/codenames/codenames/backend/lobby/domain/Lobby.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index ee1acecd..36e54289 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -45,11 +45,12 @@ public Lobby(String lobbyCode, String username) { } /** - * Adds a player to the lobby if capacity allows and the username is unique. + * Adds a new player to the lobby or reconnects an existing player. * * @param username the username of the player * @param isHost whether the player is the host of the lobby - * @return {@code true} if the player was added, {@code false} otherwise + * @param requestedUuid the UUID provided by the client to verify a reconnect + * @return {@link Player} if the player was added or reconnected, {@code null} otherwise */ public Player addPlayer(String username, boolean isHost, String requestedUuid) { From 40bf4f55f00375cf06cbe00e21d82c68c6e71130 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 12:20:25 +0200 Subject: [PATCH 26/34] test: align tests with new UUID validation logic --- .../lobby/api/LobbyControllerTest.java | 87 ++++++++++++------- .../lobby/application/LobbyServiceTest.java | 13 ++- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java index ad19d70e..a5404cea 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.lobby.api; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -67,12 +68,26 @@ void createLobbyNullLobbyCode() throws Exception { @Test void joinLobbyShouldReturn200WhenSuccess() throws Exception { when(service.joinLobby("TestUser", "ABCDE", null)) - .thenReturn(new Player("TestUser", false, "mock-uuid-123")); + .thenReturn(new Player("TestUser", false, "mock-uuid-123")); mockMvc - .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); + .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); + } + + @Test + void joinLobbyShouldReturn200WhenReconnectSuccess() throws Exception { + when(service.joinLobby("TestUser", "ABCDE", "mock-uuid-123")) + .thenReturn(new Player("TestUser", false, "mock-uuid-123")); + + mockMvc + .perform( + get("/lobby/ABCDE/join") + .param("username", "TestUser") + .param("uuid", "mock-uuid-123")) // <-- Hier auf "uuid" geändert + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); } @Test @@ -88,20 +103,22 @@ void joinLobbyShouldReturn400WhenNotFound() throws Exception { when(service.joinLobby("TestUser", "XXXXX", null)).thenReturn(null); mockMvc - .perform(get("/lobby/XXXXX/join").param("username", "TestUser")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); + .perform(get("/lobby/XXXXX/join").param("username", "TestUser")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); } @Test void leaveLobbyShouldReturn200WhenSuccess() throws Exception { when(service.getIsStarted("ABCDE")).thenReturn(false); - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); + when(service.leaveLobby(anyString(), anyString())).thenReturn(true); mockMvc - .perform(get("/lobby/ABCDE/leave").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + .perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser") + .param("uuid", "mock-uuid-123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Left lobby successfully.")); } @Test @@ -109,10 +126,12 @@ void leaveLobbyNoSuccess() throws Exception { when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); mockMvc - .perform( - get("/lobby/ABCDE/leave").param("username", "TestUser").param("lobbyCode", "ABCDE")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); + .perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser") + .param("lobbyCode", "ABCDE") + .param("uuid", "mock-uuid-123")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); } @Test @@ -179,36 +198,40 @@ void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { @Test void testStartGameReturns200WhenConditionIsMet() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true, null), - new PlayerDto("Bob", null, null, false, null)); + List.of(new PlayerDto("Alice", null, null, true, null), + new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); - when(service.startGame("ABCDE", "Alice")).thenReturn(true); + when(service.startGame(anyString(), anyString())).thenReturn(true); mockMvc - .perform(get("/lobby/ABCDE/start-game").param("username", "Alice")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Game is starting now.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username").value("Alice")) - .andExpect(jsonPath("$.isStarted").value("true")); + .perform(get("/lobby/ABCDE/start-game") + .param("username", "Alice") + .param("uuid", "mock-uuid-123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Game is starting now.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username").value("Alice")) + .andExpect(jsonPath("$.isStarted").value("true")); } @Test void testStartGameReturns400WhenServiceReturnsFalse() throws Exception { List players = - List.of(new PlayerDto("Alice", null, null, true, null), - new PlayerDto("Bob", null, null, false, null)); + List.of(new PlayerDto("Alice", null, null, true, null), + new PlayerDto("Bob", null, null, false, null)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(false); mockMvc - .perform(get("/lobby/ABCDE/start-game").param("username", "Alice")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not start the game.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username").value("Alice")) - .andExpect(jsonPath("$.isStarted").value("false")); + .perform(get("/lobby/ABCDE/start-game") + .param("username", "Alice") + .param("uuid", "mock-uuid-123")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not start the game.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username").value("Alice")) + .andExpect(jsonPath("$.isStarted").value("false")); } } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java index c887ce52..0a768b98 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java @@ -322,7 +322,9 @@ void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { @Test void testAddGameManagerForLobby() { lobbyService.createLobby("User"); - lobbyService.startGame("ABCDE", "User"); + String hostUuid = lobbyService.getHost("ABCDE"); + lobbyService.startGame("ABCDE", hostUuid); + verify(gameService, times(1)).createGameManager(eq("ABCDE"), any(Team.class)); } @@ -348,10 +350,15 @@ void testGetHostWorks() { lobbyService.joinLobby("Bob", "ABCDE", null); lobbyService.joinLobby("Caesar", "ABCDE", null); - String expected = "Alice"; + String expectedUuid = lobbyService.getPlayers("ABCDE").stream() + .filter(Player::isHost) + .findFirst() + .orElseThrow() + .uuid(); + String result = lobbyService.getHost("ABCDE"); - assertEquals(expected, result); + assertEquals(expectedUuid, result); } @ParameterizedTest From 6810356d6045e6e8ba37ebe2fa95b98f1889aebc Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 21:35:29 +0200 Subject: [PATCH 27/34] fix: same username can be picked multiple times --- .../codenames/backend/lobby/domain/Lobby.java | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java index 36e54289..bb466e73 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -25,10 +25,10 @@ public class Lobby { private final List playerList = new CopyOnWriteArrayList<>(); private final SecureRandom random = new SecureRandom(); - /** Maps a username to the selected team. */ + /** Maps a UUID to the selected team. */ private final Map playerTeams; - /** Maps a username to the selected role. */ + /** Maps a UUID to the selected role. */ private final Map playerRoles; /** @@ -44,6 +44,28 @@ public Lobby(String lobbyCode, String username) { this.addPlayer(username, true); } + /** + * Creates an empty lobby. Primarily used during the database restoration process. + * + * @param lobbyCode the unique code identifying the lobby + */ + + public Lobby(String lobbyCode) { + this.lobbyCode = lobbyCode; + this.playerTeams = new HashMap<>(); + this.playerRoles = new HashMap<>(); + } + + /** + * Directly adds a pre-existing player object back into the lobby. + * + * @param player the player to restore + */ + + public void addRestoredPlayer(Player player) { + playerList.add(player); + } + /** * Adds a new player to the lobby or reconnects an existing player. * @@ -51,21 +73,20 @@ public Lobby(String lobbyCode, String username) { * @param isHost whether the player is the host of the lobby * @param requestedUuid the UUID provided by the client to verify a reconnect * @return {@link Player} if the player was added or reconnected, {@code null} otherwise + *{@code null} if the lobby is full or if the reconnect UUID was not found. */ public Player addPlayer(String username, boolean isHost, String requestedUuid) { - Optional existingPlayerOpt = playerList.stream() - .filter(p -> p.username().equals(username)) - .findFirst(); - - if (existingPlayerOpt.isPresent()) { - Player existingPlayer = existingPlayerOpt.get(); - - if (existingPlayer.uuid().equals(requestedUuid)) { - return existingPlayer; + if (requestedUuid != null) { + Optional existingPlayerOpt = playerList.stream() + .filter(p -> p.uuid().equals(requestedUuid)) + .findFirst(); + + if (existingPlayerOpt.isPresent()) { + return existingPlayerOpt.get(); + } else { + return null; } - - return null; } if (playerList.size() >= MAX_PLAYERS) { @@ -81,9 +102,9 @@ public Player addPlayer(String username, boolean isHost, String requestedUuid) { /** * Adds a player to lobby. * - * @param username der Name des Spielers - * @param isHost gibt an, ob der Spieler der Host ist - * @return der erstellte Player + * @param username the username of the player + * @param isHost indicates which player is the host + * @return the player or {@code null}, if lobby is full */ public Player addPlayer(String username, boolean isHost) { @@ -91,7 +112,7 @@ public Player addPlayer(String username, boolean isHost) { } /** - * Adds a player to the lobby if capacity allows and the username is unique. Calls {@link + * Adds a player to the lobby if capacity allows and the UUID is unique. Calls {@link * #addPlayer(String, boolean)} with {@code false} as the second argument * * @param username the username of the player @@ -104,79 +125,62 @@ public Player addPlayer(String username, String uuid) { /** * Removes a player from the lobby. * - * @param username the username of the player to remove + * @param uuid the uuid of the player to remove */ - public void removePlayer(String username) { - playerList.removeIf(p -> p.username().equals(username)); - this.playerTeams.remove(username); - this.playerRoles.remove(username); + public void removePlayer(String uuid) { + playerList.removeIf(p -> p.uuid().equals(uuid)); + this.playerTeams.remove(uuid); + this.playerRoles.remove(uuid); } /** * Checks whether a player with the given username is in the lobby. * - * @param username the username to check + * @param uuid the uuid to check * @return {@code true} if the player exists in the lobby, {@code false} otherwise */ - public boolean hasPlayer(String username) { - return playerList.stream().anyMatch(p -> p.username().equals(username)); + public boolean hasPlayer(String uuid) { + return playerList.stream().anyMatch(p -> p.uuid().equals(uuid)); } /** * Sets the team for a player. * - * @param username the username of the player + * @param uuid the UUID of the player * @param team the team to assign */ - public void setPlayerTeam(String username, Team team) { - playerTeams.put(username, team); + public void setPlayerTeam(String uuid, Team team) { + playerTeams.put(uuid, team); } /** * Sets the role for a player. * - * @param username the username of the player + * @param uuid the UUID of the player * @param role the role to assign */ - public void setPlayerRole(String username, Role role) { - playerRoles.put(username, role); + public void setPlayerRole(String uuid, Role role) { + playerRoles.put(uuid, role); } - /** - * Sets the UUID for a player by replacing their Player record, used by restoration of UUID. - * - * @param username the username of the player - * @param uuid the UUID to assign - */ - public void setPlayerUuid(String username, String uuid) { - for (int i = 0; i < playerList.size(); i++) { - Player p = playerList.get(i); - if (p.username().equals(username)) { - // caller loops through all players in list on match we overwrite - playerList.set(i, new Player(p.username(), p.isHost(), uuid)); - return; - } - } - } // refer RestorationMapper as for why we overwrite player records - /** * Returns the team of a player. * - * @param username the username of the player + * @param uuid the UUID of the player * @return the assigned team, or {@code null} if none is assigned */ - public Team getPlayerTeam(String username) { - return playerTeams.get(username); + public Team getPlayerTeam(String uuid) { + return playerTeams.get(uuid); } /** * Returns the role of a player. * - * @param username the username of the player + * @param uuid the UUID of the player * @return the assigned role, or {@code null} if none is assigned */ - public Role getPlayerRole(String username) { - return playerRoles.get(username); + public Role getPlayerRole(String uuid) { + return playerRoles.get(uuid); } /** From dbc4730a0bcf3150984d467770755d8e6e83b81d Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 21:35:53 +0200 Subject: [PATCH 28/34] refactor: change files so uuid is used instead of username --- .../database/repository/PlayerRepository.java | 4 +-- .../restoration/RestorationMapper.java | 35 ++++++------------- .../backend/lobby/api/LobbyController.java | 10 +++--- .../shared/websocket/SessionRegistry.java | 8 ++--- 4 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java b/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java index 26056bb2..9dd4301a 100644 --- a/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java +++ b/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java @@ -15,10 +15,10 @@ public interface PlayerRepository extends JpaRepository { * Custom SQL query that is not included in the JpaRepository interface. * * @param lobbyCode the 5 character lobby code we want to search the player in - * @param username the username of the player we want to search for + * @param uuid the UUID of the player we want to search for * @return the player */ // First Lobby is the Lobby object inside the entity where we can find the lobby_code String. // And "resets" the SQL search back to the root table Player - Optional findByLobbyEntityLobbyCodeAndUsername(String lobbyCode, String username); + Optional findByLobbyEntityLobbyCodeAndUuid(String lobbyCode, String uuid); } diff --git a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java index 2addf2e9..e6e99b45 100644 --- a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java +++ b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java @@ -10,6 +10,7 @@ import com.codenames.codenames.backend.game.domain.Color; import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; import com.codenames.codenames.backend.lobby.domain.Lobby; +import com.codenames.codenames.backend.lobby.domain.Player; import com.codenames.codenames.backend.lobby.domain.Role; import com.codenames.codenames.backend.lobby.domain.Team; import java.util.ArrayList; @@ -100,39 +101,25 @@ private List mapToPlayerDto(LobbyEntity lobbyEntity) { } private Lobby buildLobby(String lobbyCode, List players) { - String hostUsername = findHostUsername(players); - // the way uuid is generated is a bit roundabout, but I do not have the time to refactor it - // and I do not want to touch the code if it works (Lobby.java) - // since constructor uses the two parameter method -> reroute to 3 parameter -> always null UUID - Lobby lobby = new Lobby(lobbyCode, hostUsername); + Lobby lobby = new Lobby(lobbyCode); for (PlayerDto player : players) { - if (!player.username().equals(hostUsername)) { - lobby.addPlayer(player.username(), player.isHost()); - } + lobby.addRestoredPlayer( + new Player( + player.username(), + player.isHost(), + player.uuid())); + if (player.team() != null) { - lobby.setPlayerTeam(player.username(), player.team()); + lobby.setPlayerTeam(player.uuid(), player.team()); } + if (player.role() != null) { - lobby.setPlayerRole(player.username(), player.role()); - } - if (player.uuid() != null) { - // Solution: we overwrite all player records with data loaded from DB - lobby.setPlayerUuid(player.username(), player.uuid()); + lobby.setPlayerRole(player.uuid(), player.role()); } } return lobby; } - - private static String findHostUsername(List players) { - String hostUsername = ""; - for (PlayerDto playerDto : players) { - if (playerDto.isHost()) { - hostUsername = playerDto.username(); - } - } - return hostUsername; - } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index fc41a141..3c18cddc 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -40,12 +40,12 @@ public LobbyController(LobbyService service) { /** * Handles a request to create a new lobby. * - * @param username the username of the requesting user + * @param uuid the UUID of the requesting user * @return a response containing the result and the generated lobby code */ @GetMapping("/create") - public ResponseEntity createLobby(@RequestParam String username) { - String lobbyCode = service.createLobby(username); + public ResponseEntity createLobby(@RequestParam String uuid) { + String lobbyCode = service.createLobby(uuid); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() .body(new LobbyResponse("Error while creating lobby.", "", null, false, null)); @@ -53,7 +53,7 @@ public ResponseEntity createLobby(@RequestParam String username) List players = service.getPlayersDto(lobbyCode); Player host = Objects.requireNonNull( - service.getPlayer(lobbyCode, username)); + service.getPlayer(lobbyCode, uuid)); String hostUuid = host.uuid(); @@ -153,7 +153,7 @@ public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode public ResponseEntity selectPosition( @PathVariable String lobbyCode, @RequestBody PlayerDto request) { boolean updated = - service.selectPosition(request.username(), lobbyCode, request.team(), request.role()); + service.selectPosition(request.uuid(), lobbyCode, request.team(), request.role()); if (updated) { return ResponseEntity.ok( diff --git a/src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java b/src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java index fb36b2fe..33715ff7 100644 --- a/src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java +++ b/src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java @@ -7,7 +7,7 @@ /** * Registry for managing WebSocket sessions and their associated users and lobbies. * - *

Maintains mappings between session IDs, usernames, and lobby codes to support messaging and + *

Maintains mappings between session IDs, UUID's, and lobby codes to support messaging and * cleanup on disconnect. * *

This implementation is thread-safe. @@ -21,12 +21,12 @@ public class SessionRegistry { * Registers a session with its associated user and lobby. * * @param sessionId the WebSocket session ID - * @param username the username of the player + * @param uuid the UUID of the player * @param lobbyCode the lobby code */ - public void register(String sessionId, String username, String lobbyCode) { + public void register(String sessionId, String uuid, String lobbyCode) { sessionToLobby.put(sessionId, lobbyCode); - sessionToUser.put(sessionId, username); + sessionToUser.put(sessionId, uuid); } /** From e370f2ab59f7b5d93ed35451b3ebba84179e04e0 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 21:36:11 +0200 Subject: [PATCH 29/34] test: change test to align with new changes --- .../lobby/application/LobbyService.java | 53 +++-- .../lobby/api/LobbyControllerTest.java | 88 +++++---- .../lobby/application/LobbyServiceTest.java | 185 ++++++++++-------- .../backend/lobby/domain/LobbyTest.java | 118 ++++++----- 4 files changed, 233 insertions(+), 211 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index a59b2cbc..4c67baad 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -91,12 +91,11 @@ public Player joinLobby(String username, String lobbyCode, String uuid) { if (lobby != null) { if (gameService.isGameStarted(lobbyCode)) { - Player existingPlayer = lobby.getPlayerList().stream() - .filter(p -> p.username().equals(username)) - .findFirst() - .orElse(null); + boolean validReconnect = + lobby.getPlayerList().stream() + .anyMatch(p -> p.uuid().equals(uuid)); - if (existingPlayer == null || !existingPlayer.uuid().equals(uuid)) { + if (!validReconnect) { log.warn("{}: join rejected. Game started, no valid reconnect UUID.", lobbyCode); return null; } @@ -143,27 +142,27 @@ public boolean leaveLobby(String uuid, String lobbyCode) { /** * Assigns a team and role to a player in a lobby. * - * @param username the username of the player + * @param uuid the UUID of the player * @param lobbyCode the lobby code identifying the lobby * @param team the selected team * @param role the selected role * @return {@code true} if the position was assigned, {@code false} otherwise */ - public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { + public boolean selectPosition(String uuid, String lobbyCode, Team team, Role role) { Lobby lobby = lobbyList.get(lobbyCode); - if (lobby == null || !lobby.hasPlayer(username) || team == null || role == null) { + if (lobby == null || !lobby.hasPlayer(uuid) || team == null || role == null) { log.error("{}: position selection error occurred", lobbyCode); return false; } - if (role == Role.SPYMASTER && isSpymasterAlreadyAssigned(lobby, username, team)) { + if (role == Role.SPYMASTER && isSpymasterAlreadyAssigned(lobby, uuid, team)) { log.error("{}: position selection error occurred, spymaster is already assigned.", lobbyCode); return false; } - lobby.setPlayerTeam(username, team); - lobby.setPlayerRole(username, role); + lobby.setPlayerTeam(uuid, team); + lobby.setPlayerRole(uuid, role); log.info("{}: new role was assigned to player", lobbyCode); return true; } @@ -210,8 +209,8 @@ public List getPlayersDto(String lobbyCode) { player -> new PlayerDto( player.username(), - lobby.getPlayerTeam(player.username()), - lobby.getPlayerRole(player.username()), + lobby.getPlayerTeam(player.uuid()), + lobby.getPlayerRole(player.uuid()), player.isHost(), player.uuid())) .toList(); @@ -223,15 +222,15 @@ public List getPlayersDto(String lobbyCode) { * Checks whether a spymaster is already assigned for the given team in the lobby. * * @param lobby the lobby to inspect - * @param username the username requesting the role + * @param uuid the UUID requesting the role * @param team the team to inspect * @return {@code true} if a different player is already the spymaster for that team */ - private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { + private boolean isSpymasterAlreadyAssigned(Lobby lobby, String uuid, Team team) { for (Player player : lobby.getPlayerList()) { - if (!player.username().equals(username) - && lobby.getPlayerTeam(player.username()) == team - && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { + if (!player.uuid().equals(uuid) + && lobby.getPlayerTeam(player.uuid()) == team + && lobby.getPlayerRole(player.uuid()) == Role.SPYMASTER) { return true; } } @@ -262,14 +261,14 @@ private String generateLobbyCode() { /** * Retrieves the team of a player in a lobby. * - * @param username the username of a player + * @param uuid the UUID of a player * @param lobbyCode the lobby code of the lobby * @return the team of the player, or {@code null} if the lobby or player does not exist */ - public Team getPlayerTeam(String username, String lobbyCode) { + public Team getPlayerTeam(String uuid, String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { - return lobby.getPlayerTeam(username); + return lobby.getPlayerTeam(uuid); } return null; } @@ -277,14 +276,14 @@ public Team getPlayerTeam(String username, String lobbyCode) { /** * Retrieves the role of a player in a lobby. * - * @param username the username of a player + * @param uuid the UUID of a player * @param lobbyCode the lobby code of the lobby * @return the role of the player, or {@code null} if the lobby or player does not exist */ - public Role getPlayerRole(String username, String lobbyCode) { + public Role getPlayerRole(String uuid, String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { - return lobby.getPlayerRole(username); + return lobby.getPlayerRole(uuid); } return null; } @@ -293,14 +292,14 @@ public Role getPlayerRole(String username, String lobbyCode) { * Retrieves a specific player from a lobby. * * @param lobbyCode the lobby code - * @param username the username of the player + * @param uuid the UUID of the player * @return the Player object, or null if not found */ - public Player getPlayer(String lobbyCode, String username) { + public Player getPlayer(String lobbyCode, String uuid) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { return lobby.getPlayerList().stream() - .filter(p -> p.username().equals(username)) + .filter(p -> p.uuid().equals(uuid)) .findFirst() .orElse(null); } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java index 297f5b4f..0aae9a10 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java @@ -12,15 +12,15 @@ import com.codenames.codenames.backend.lobby.domain.Player; import com.codenames.codenames.backend.lobby.domain.Role; import com.codenames.codenames.backend.lobby.domain.Team; +import com.codenames.codenames.backend.shared.config.SecurityConfig; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import com.codenames.codenames.backend.shared.config.SecurityConfig; -import org.springframework.context.annotation.Import; @WebMvcTest(LobbyController.class) @Import(SecurityConfig.class) @@ -31,41 +31,47 @@ class LobbyControllerTest { @MockBean private LobbyService service; @Test - void createLobbyShouldReturn200() throws Exception { - when(service.createLobby("TestUser")).thenReturn("ABCDE"); - when(service.getPlayer("ABCDE", - "TestUser")).thenReturn(new Player("TestUser", - true, - "mock-uuid") - ); + void createLobbyShouldReturn200() throws Exception { + when(service.createLobby(anyString())).thenReturn("ABCDE"); + when(service.getPlayer(anyString(), anyString())) + .thenReturn(new Player("TestUser", true, "mock-uuid-123")); mockMvc - .perform(get("/lobby/create").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + .perform( + get("/lobby/create") + .param("username", "TestUser") + .param("uuid", "mock-uuid-123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } @Test - void createLobbyBlankLobbyCode() throws Exception { + void createLobbyBlankLobbyCode() throws Exception { when(service.createLobby("TestUser")).thenReturn(""); mockMvc - .perform(get("/lobby/create").param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); + .perform( + get("/lobby/create") + .param("username", "TestUser") + .param("uuid", "mock-uuid-123")) // <-- Hier fehlte die UUID + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); } @Test - void createLobbyNullLobbyCode() throws Exception { + void createLobbyNullLobbyCode() throws Exception { when(service.createLobby("TestUser")).thenReturn(null); mockMvc - .perform(get("/lobby/create").param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); + .perform( + get("/lobby/create") + .param("username", "TestUser") + .param("uuid", "mock-uuid-123")) // <-- Hier fehlte die UUID + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); } @Test @@ -138,25 +144,27 @@ void leaveLobbyNoSuccess() throws Exception { } @Test - void selectPositionShouldReturn200whenSuccess() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); + void selectPositionShouldReturn200whenSuccess() throws Exception { + when(service.selectPosition("mock-uuid-123", "ABCDE", Team.RED, Role.SPYMASTER)) + .thenReturn(true); mockMvc - .perform( - post("/lobby/ABCDE/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "team": "RED", - "role": "SPYMASTER", - "isHost": "true" - } - """)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Position selected successfully.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + .perform( + post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "TestUser", + "uuid": "mock-uuid-123", + "team": "RED", + "role": "SPYMASTER", + "isHost": true + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Position selected successfully.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java index 0a768b98..2652fba8 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,11 +29,6 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; -/** - * Tests for {@link LobbyService}. - * - *

Validates lobby creation, joining, leaving, and player management behavior. - */ class LobbyServiceTest { private LobbyService lobbyService; @@ -41,7 +37,7 @@ class LobbyServiceTest { private LobbyRepository lobbyRepository; @BeforeEach - void setup() { + void setup() { generator = mock(LobbyCodeGenerator.class); gameService = mock(GameService.class); ChatService chatService = mock(ChatService.class); @@ -52,7 +48,7 @@ void setup() { } @Test - void createLobbyReturnLobbyCode() { + void createLobbyReturnLobbyCode() { lobbyService.createLobby("Host"); Player result = lobbyService.joinLobby("TestUser", "ABCDE", null); @@ -63,7 +59,7 @@ void createLobbyReturnLobbyCode() { } @Test - void createLobbyLobbyCodeIsNull() { + void createLobbyLobbyCodeIsNull() { when(generator.generateLobbyCode()).thenReturn(null); String result = lobbyService.createLobby("Host"); @@ -71,7 +67,7 @@ void createLobbyLobbyCodeIsNull() { } @Test - void createLobbyLobbyCodeIsBlank() { + void createLobbyLobbyCodeIsBlank() { when(generator.generateLobbyCode()).thenReturn(""); String result = lobbyService.createLobby("Host"); @@ -79,32 +75,33 @@ void createLobbyLobbyCodeIsBlank() { } @Test - void joinLobbyReturnFalseLobbyNotExists() { + void joinLobbyReturnFalseLobbyNotExists() { Player result = lobbyService.joinLobby("TestUser", "ABCDE", null); assertNull(result); } @Test - void leaveLobbyReturnTrueLobbyExists() { + void leaveLobbyReturnTrueLobbyExists() { lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost("ABCDE"); - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); + boolean result = lobbyService.leaveLobby(hostUuid, "ABCDE"); assertTrue(result); List players = lobbyService.getPlayers("ABCDE"); - assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); + assertFalse(players.stream().anyMatch(p -> p.uuid().equals(hostUuid))); } @Test - void leaveLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); + void leaveLobbyReturnFalseLobbyNotExists() { + boolean result = lobbyService.leaveLobby("wrong-uuid", "ABCDE"); assertFalse(result); } @Test - void createLobbyShouldGenerateNewCodeIfDuplicateExists() { + void createLobbyShouldGenerateNewCodeIfDuplicateExists() { when(generator.generateLobbyCode()).thenReturn("ABCDE").thenReturn("ABCDE").thenReturn("FGHIJ"); lobbyService.createLobby("Host1"); @@ -114,68 +111,78 @@ void createLobbyShouldGenerateNewCodeIfDuplicateExists() { } @Test - void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { + void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost("ABCDE"); - boolean result = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + boolean result = lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.SPYMASTER); assertTrue(result); } @Test - void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { - boolean result = lobbyService.selectPosition("Host", "XXXXX", Team.RED, Role.SPYMASTER); + void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { + boolean result = lobbyService.selectPosition("wrong-uuid", "XXXXX", Team.RED, Role.SPYMASTER); assertFalse(result); } @Test - void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { + void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { lobbyService.createLobby("Host"); - boolean result = lobbyService.selectPosition("Ghost", "ABCDE", Team.RED, Role.SPYMASTER); + boolean result = lobbyService.selectPosition("wrong-uuid", "ABCDE", Team.RED, Role.SPYMASTER); assertFalse(result); } @Test - void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { + void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE", null); + Player p1 = lobbyService.joinLobby("P1", "ABCDE", null); + String hostUuid = lobbyService.getHost("ABCDE"); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); + boolean firstResult = lobbyService.selectPosition(hostUuid, "ABCDE", + Team.RED, Role.SPYMASTER); + boolean secondResult = lobbyService.selectPosition(p1.uuid(), "ABCDE", + Team.RED, Role.SPYMASTER); assertTrue(firstResult); assertFalse(secondResult); } @Test - void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { + void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE", null); + Player p1 = lobbyService.joinLobby("P1", "ABCDE", null); + String hostUuid = lobbyService.getHost("ABCDE"); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); + boolean firstResult = lobbyService.selectPosition(hostUuid, "ABCDE", + Team.RED, Role.SPYMASTER); + boolean secondResult = lobbyService.selectPosition(p1.uuid(), "ABCDE", + Team.BLUE, Role.SPYMASTER); assertTrue(firstResult); assertTrue(secondResult); } @Test - void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { + void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE", null); + Player p1 = lobbyService.joinLobby("P1", "ABCDE", null); + String hostUuid = lobbyService.getHost("ABCDE"); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); + boolean firstResult = lobbyService.selectPosition(hostUuid, "ABCDE", + Team.RED, Role.OPERATIVE); + boolean secondResult = lobbyService.selectPosition(p1.uuid(), "ABCDE", + Team.RED, Role.OPERATIVE); assertTrue(firstResult); assertTrue(secondResult); } @Test - void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { + void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { List players = lobbyService.getPlayers("UNKNOWN"); assertNotNull(players); @@ -183,119 +190,130 @@ void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { } @Test - void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { + void joinLobbyShouldAllowDuplicateUsernamesWithDifferentUuids() { lobbyService.createLobby("Host"); Player first = lobbyService.joinLobby("Max", "ABCDE", null); Player second = lobbyService.joinLobby("Max", "ABCDE", null); assertNotNull(first); - assertNull(second); + assertNotNull(second); + assertNotEquals(first.uuid(), second.uuid()); List players = lobbyService.getPlayers("ABCDE"); long count = players.stream().filter(p -> p.username().equals("Max")).count(); - assertEquals(1, count); + assertEquals(2, count); } @Test - void selectPositionShouldReturnFalseIfTeamIsNull() { + void selectPositionShouldReturnFalseIfTeamIsNull() { when(generator.generateLobbyCode()).thenReturn("ABCDE"); String lobbyCode = lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost(lobbyCode); - boolean result = lobbyService.selectPosition("Host", lobbyCode, null, Role.OPERATIVE); + boolean result = lobbyService.selectPosition(hostUuid, lobbyCode, null, Role.OPERATIVE); assertFalse(result); } @Test - void selectPositionShouldReturnFalseIfRoleIsNull() { + void selectPositionShouldReturnFalseIfRoleIsNull() { when(generator.generateLobbyCode()).thenReturn("ABCDE"); String lobbyCode = lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost(lobbyCode); - boolean result = lobbyService.selectPosition("Host", lobbyCode, Team.RED, null); + boolean result = lobbyService.selectPosition(hostUuid, lobbyCode, Team.RED, null); assertFalse(result); } @Test - void testGetPlayerTeam() { + void testGetPlayerTeam() { lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + String hostUuid = lobbyService.getHost("ABCDE"); + lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.SPYMASTER); - assertEquals(Team.RED, lobbyService.getPlayerTeam("Host", "ABCDE")); + assertEquals(Team.RED, lobbyService.getPlayerTeam(hostUuid, "ABCDE")); } @Test - void getPlayerTeam_wrongCode() { - assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); + void getPlayerTeam_wrongCode() { + assertNull(lobbyService.getPlayerTeam("wrong-uuid", "invalidCode")); } @Test - void getPlayerTeam_nonExistentPlayer() { + void getPlayerTeam_nonExistentPlayer() { String lobbyCode = lobbyService.createLobby("Host"); - assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); + assertNull(lobbyService.getPlayerTeam("wrong-uuid", lobbyCode)); } @Test - void testGetPlayerRole() { + void testGetPlayerRole() { lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); + String hostUuid = lobbyService.getHost("ABCDE"); + lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.OPERATIVE); - assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole("Host", "ABCDE")); + assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole(hostUuid, "ABCDE")); } @Test - void getPlayerRole_wrongCode() { - assertNull(lobbyService.getPlayerRole("Host", "test")); + void getPlayerRole_wrongCode() { + assertNull(lobbyService.getPlayerRole("wrong-uuid", "test")); } @Test - void getPlayerRole_nonExistentPlayer() { + void getPlayerRole_nonExistentPlayer() { String lobbyCode = lobbyService.createLobby("Host"); - assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); + assertNull(lobbyService.getPlayerRole("wrong-uuid", lobbyCode)); } @Test - void testLobbyIsRemovedWhenItIsEmpty() { + void testLobbyIsRemovedWhenItIsEmpty() { lobbyService.createLobby("Host"); - lobbyService.leaveLobby("Host", "ABCDE"); + String hostUuid = lobbyService.getHost("ABCDE"); + + lobbyService.leaveLobby(hostUuid, "ABCDE"); lobbyService.checkLobbyStillHasPlayers("ABCDE"); + assertFalse(lobbyService.getLobbyList().containsKey("ABCDE")); verify(gameService, times(1)).removeGame("ABCDE"); verify(lobbyRepository, times(1)).deleteById("ABCDE"); } @Test - void testLobbyIsNotRemovedWhenItHasPlayers() { + void testLobbyIsNotRemovedWhenItHasPlayers() { lobbyService.createLobby("Host"); lobbyService.joinLobby("Player1", "ABCDE", null); lobbyService.checkLobbyStillHasPlayers("ABCDE"); + assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); } @Test - void testGetPlayersDto() { + void testGetPlayersDto() { lobbyService.createLobby("Host"); List players = lobbyService.getPlayersDto("ABCDE"); PlayerDto player = players.get(0); + assertEquals("Host", player.username()); assertTrue(player.isHost()); } @Test - void testGetPlayersDto_lobbyNotExists() { + void testGetPlayersDto_lobbyNotExists() { List players = lobbyService.getPlayersDto("UNKNOWN"); + assertNotNull(players); assertTrue(players.isEmpty()); } @Test - void getPlayersDtoShouldReturnPlayerDtosWhenLobbyExists() { + void getPlayersDtoShouldReturnPlayerDtosWhenLobbyExists() { lobbyService.createLobby("Host"); List result = lobbyService.getPlayersDto("ABCDE"); @@ -312,7 +330,7 @@ void getPlayersDtoShouldReturnPlayerDtosWhenLobbyExists() { } @Test - void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { + void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { List result = lobbyService.getPlayersDto("ABCDE"); assertNotNull(result); @@ -320,7 +338,7 @@ void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { } @Test - void testAddGameManagerForLobby() { + void testAddGameManagerForLobby() { lobbyService.createLobby("User"); String hostUuid = lobbyService.getHost("ABCDE"); lobbyService.startGame("ABCDE", hostUuid); @@ -329,32 +347,34 @@ void testAddGameManagerForLobby() { } @Test - void testGetIsStarted() { + void testGetIsStarted() { when(gameService.isGameStarted("ABCDE")).thenReturn(true); boolean result = lobbyService.getIsStarted("ABCDE"); + assertTrue(result); } @Test - void testGetIsStartedGameServiceReturnsFalse() { + void testGetIsStartedGameServiceReturnsFalse() { when(gameService.isGameStarted("ABCDE")).thenReturn(false); boolean result = lobbyService.getIsStarted("ABCDE"); + assertFalse(result); } @Test - void testGetHostWorks() { + void testGetHostWorks() { lobbyService.createLobby("Alice"); lobbyService.joinLobby("Bob", "ABCDE", null); lobbyService.joinLobby("Caesar", "ABCDE", null); String expectedUuid = lobbyService.getPlayers("ABCDE").stream() - .filter(Player::isHost) - .findFirst() - .orElseThrow() - .uuid(); + .filter(Player::isHost) + .findFirst() + .orElseThrow() + .uuid(); String result = lobbyService.getHost("ABCDE"); @@ -364,14 +384,14 @@ void testGetHostWorks() { @ParameterizedTest @NullAndEmptySource @ValueSource(strings = {"ABCDE"}) - void testGetHostReturnsEmptyString(String lobbyCode) { + void testGetHostReturnsEmptyString(String lobbyCode) { String result = lobbyService.getHost(lobbyCode); assertEquals("", result); } @Test - void testRestoreLobbyAddsLobbyToLobbyList() { + void testRestoreLobbyAddsLobbyToLobbyList() { Lobby restoredLobby = new Lobby("ABCDE", "Host"); lobbyService.restoreLobby("ABCDE", restoredLobby); @@ -381,21 +401,22 @@ void testRestoreLobbyAddsLobbyToLobbyList() { } @Test - void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { + void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { lobbyService.createLobby("Host"); lobbyService.joinLobby("Player", "ABCDE", null); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + String hostUuid = lobbyService.getHost("ABCDE"); + + lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.SPYMASTER); Map> snapshots = lobbyService.getLobbySnapshots(); assertTrue(snapshots.containsKey("ABCDE")); assertEquals(2, snapshots.get("ABCDE").size()); - PlayerDto host = - snapshots.get("ABCDE").stream() - .filter(player -> player.username().equals("Host")) - .findFirst() - .orElseThrow(); + PlayerDto host = snapshots.get("ABCDE").stream() + .filter(player -> player.username().equals("Host")) + .findFirst() + .orElseThrow(); assertEquals(Team.RED, host.team()); assertEquals(Role.SPYMASTER, host.role()); @@ -403,7 +424,7 @@ void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { } @Test - void joinLobbyShouldBlockNewPlayerIfGameStarted() { + void joinLobbyShouldBlockNewPlayerIfGameStarted() { when(gameService.isGameStarted("ABCDE")).thenReturn(true); Player result = lobbyService.joinLobby("NewPlayer", "ABCDE", null); @@ -412,7 +433,7 @@ void joinLobbyShouldBlockNewPlayerIfGameStarted() { } @Test - void joinLobbyShouldAllowReconnectIfGameStarted() { + void joinLobbyShouldAllowReconnectIfGameStarted() { lobbyService.getLobbyList().clear(); String lobbyCode = lobbyService.createLobby("Host"); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java index 61c6495c..9d097e91 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -9,125 +9,126 @@ import org.junit.jupiter.api.Test; -/** - * Tests for {@link Lobby}. - * - *

Validates player management and lobby constraints. - */ class LobbyTest { @Test - void constructorShouldInitializeLobbyCorrectly() { + void constructorShouldInitializeLobbyCorrectly() { Lobby lobby = new Lobby("ABCDE", "Host"); assertEquals("ABCDE", lobby.getLobbyCode()); assertEquals(1, lobby.getPlayerList().size()); assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("Host"))); + assertNotNull(lobby.getPlayerList().get(0).uuid()); } @Test - void addPlayerShouldAddPlayer() { + void addPlayerShouldAddPlayer() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.addPlayer("P1", null); + lobby.addPlayer("P1", false); assertEquals(2, lobby.getPlayerList().size()); assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); } @Test - void addPlayerShouldNotExceedMaxPlayers() { + void addPlayerShouldNotExceedMaxPlayers() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.addPlayer("P1", null); - lobby.addPlayer("P2", null); - lobby.addPlayer("P3", null); - lobby.addPlayer("P4", null); + lobby.addPlayer("P1", false); + lobby.addPlayer("P2", false); + lobby.addPlayer("P3", false); + Player rejectedPlayer = lobby.addPlayer("P4", false); assertEquals(4, lobby.getPlayerList().size()); + assertNull(rejectedPlayer); } @Test - void removePlayerShouldRemovePlayer() { + void removePlayerShouldRemovePlayer() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.addPlayer("P1", null); - lobby.removePlayer("P1"); + Player p1 = lobby.addPlayer("P1", false); + String p1Uuid = p1.uuid(); + + lobby.removePlayer(p1Uuid); - assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); + assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.uuid().equals(p1Uuid))); } @Test - void removePlayerShouldDoNothingIfPlayerNotExists() { + void removePlayerShouldDoNothingIfPlayerNotExists() { Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.removePlayer("Ghost"); + lobby.removePlayer("invalid-uuid-1234"); assertEquals(1, lobby.getPlayerList().size()); } @Test - void addPlayerShouldNotAddDuplicatePlayer() { + void addPlayerShouldAllowDuplicateUsernamesWithDifferentUuids() { Lobby lobby = new Lobby("ABCDE", "Host"); - Player first = lobby.addPlayer("Max", null); - Player second = lobby.addPlayer("Max", null); + Player first = lobby.addPlayer("Max", false); + Player second = lobby.addPlayer("Max", false); assertNotNull(first); - assertNull(second); - - long count = lobby.getPlayerList().stream().filter(p -> p.username().equals("Max")).count(); - - assertEquals(1, count); + assertNotNull(second); + assertNotEquals(first.uuid(), second.uuid()); + assertEquals(3, lobby.getPlayerList().size()); } @Test - void hasPlayerShouldReturnTrueIfPlayerExists() { + void hasPlayerShouldReturnTrueIfPlayerExists() { Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); - assertTrue(lobby.hasPlayer("Host")); + assertTrue(lobby.hasPlayer(hostUuid)); } @Test - void hasPlayerShouldReturnFalseIfPlayerDoesNotExist() { + void hasPlayerShouldReturnFalseIfPlayerDoesNotExist() { Lobby lobby = new Lobby("ABCDE", "Host"); - assertFalse(lobby.hasPlayer("Ghost")); + assertFalse(lobby.hasPlayer("invalid-uuid")); } @Test - void setPlayerTeamShouldStoreSelectedTeam() { + void setPlayerTeamShouldStoreSelectedTeam() { Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); - lobby.setPlayerTeam("Host", Team.RED); + lobby.setPlayerTeam(hostUuid, Team.RED); - assertEquals(Team.RED, lobby.getPlayerTeam("Host")); + assertEquals(Team.RED, lobby.getPlayerTeam(hostUuid)); } @Test - void setPlayerRoleShouldStoreSelectedRole() { + void setPlayerRoleShouldStoreSelectedRole() { Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); - lobby.setPlayerRole("Host", Role.SPYMASTER); + lobby.setPlayerRole(hostUuid, Role.SPYMASTER); - assertEquals(Role.SPYMASTER, lobby.getPlayerRole("Host")); + assertEquals(Role.SPYMASTER, lobby.getPlayerRole(hostUuid)); } @Test - void removePlayerShouldAlsoRemoveStoredTeamAndRole() { + void removePlayerShouldAlsoRemoveStoredTeamAndRole() { Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); - lobby.setPlayerTeam("Host", Team.BLUE); - lobby.setPlayerRole("Host", Role.OPERATIVE); + lobby.setPlayerTeam(hostUuid, Team.BLUE); + lobby.setPlayerRole(hostUuid, Role.OPERATIVE); - lobby.removePlayer("Host"); + lobby.removePlayer(hostUuid); - assertNull(lobby.getPlayerTeam("Host")); - assertNull(lobby.getPlayerRole("Host")); + assertNull(lobby.getPlayerTeam(hostUuid)); + assertNull(lobby.getPlayerRole(hostUuid)); } @Test - void startingTeamShouldBeEitherRedOrBlue() { + void startingTeamShouldBeEitherRedOrBlue() { Lobby lobby = new Lobby("ABCDE", "Host"); Team startingTeam = lobby.decideStartingTeam(); @@ -136,7 +137,7 @@ void startingTeamShouldBeEitherRedOrBlue() { } @Test - void addPlayerShouldAssignUuidToNewPlayer() { + void addPlayerShouldAssignUuidToNewPlayer() { Lobby lobby = new Lobby("ABCDE", "Host"); Player newPlayer = lobby.addPlayer("P1", false); @@ -144,7 +145,7 @@ void addPlayerShouldAssignUuidToNewPlayer() { } @Test - void addPlayerShouldAllowReconnectWithCorrectUuid() { + void addPlayerShouldAllowReconnectWithCorrectUuid() { Lobby lobby = new Lobby("ABCDE", "Host"); Player first = lobby.addPlayer("P1", false); String validUuid = first.uuid(); @@ -156,7 +157,7 @@ void addPlayerShouldAllowReconnectWithCorrectUuid() { } @Test - void addPlayerShouldBlockReconnectWithWrongUuid() { + void addPlayerShouldBlockReconnectWithWrongUuid() { Lobby lobby = new Lobby("ABCDE", "Host"); lobby.addPlayer("P1", false); @@ -166,21 +167,14 @@ void addPlayerShouldBlockReconnectWithWrongUuid() { } @Test - void testSetPlayerUuid_playerFound() { - Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.setPlayerUuid("Host", "test-uuid-1"); + void addRestoredPlayerShouldAddPlayerDirectly() { + Lobby lobby = new Lobby("ABCDE"); + Player restoredPlayer = new Player("RestoredUser", false, "custom-uuid-999"); - Player player1 = lobby.getPlayerList().get(0); - assertEquals("test-uuid-1", player1.uuid()); - } + lobby.addRestoredPlayer(restoredPlayer); - @Test - void testSetPlayerUuid_playerNotFound() { - Lobby lobby = new Lobby("ABCDE", "Host"); - lobby.setPlayerUuid("NotHost", "test-uuid-1"); - // if i don't hit the same username, it should not be overridden - - Player player1 = lobby.getPlayerList().get(0); - assertNotEquals("test-uuid-1", player1.uuid()); + assertEquals(1, lobby.getPlayerList().size()); + assertEquals("custom-uuid-999", lobby.getPlayerList().get(0).uuid()); + assertEquals("RestoredUser", lobby.getPlayerList().get(0).username()); } -} +} \ No newline at end of file From a9b58efce4b2eff1a9123c6a407d9591048f8b31 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 23:48:38 +0200 Subject: [PATCH 30/34] fix: change required from UUID to username for lobby creation --- .../codenames/backend/lobby/api/LobbyController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index 3c18cddc..a30931f4 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -40,12 +40,12 @@ public LobbyController(LobbyService service) { /** * Handles a request to create a new lobby. * - * @param uuid the UUID of the requesting user + * @param username the UUID of the requesting user * @return a response containing the result and the generated lobby code */ @GetMapping("/create") - public ResponseEntity createLobby(@RequestParam String uuid) { - String lobbyCode = service.createLobby(uuid); + public ResponseEntity createLobby(@RequestParam String username) { + String lobbyCode = service.createLobby(username); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() .body(new LobbyResponse("Error while creating lobby.", "", null, false, null)); @@ -53,7 +53,7 @@ public ResponseEntity createLobby(@RequestParam String uuid) { List players = service.getPlayersDto(lobbyCode); Player host = Objects.requireNonNull( - service.getPlayer(lobbyCode, uuid)); + service.getPlayer(lobbyCode, username)); String hostUuid = host.uuid(); From 2f38bf6ac8d2a06c09090610f1c22d19e28d00c1 Mon Sep 17 00:00:00 2001 From: 5eli Date: Tue, 16 Jun 2026 23:51:22 +0200 Subject: [PATCH 31/34] style: update javaDoc --- .../codenames/codenames/backend/lobby/api/LobbyController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index a30931f4..ee744ef3 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -40,7 +40,7 @@ public LobbyController(LobbyService service) { /** * Handles a request to create a new lobby. * - * @param username the UUID of the requesting user + * @param username the username of the requesting user * @return a response containing the result and the generated lobby code */ @GetMapping("/create") From c5fa25250ffcf0b2c00ce4a91cf7a48d3eeac1d3 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Wed, 17 Jun 2026 16:36:25 +0200 Subject: [PATCH 32/34] refactor: removed unused variable --- .../codenames/codenames/backend/lobby/api/LobbyController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index ee744ef3..8d9f3558 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -103,7 +103,6 @@ public ResponseEntity joinLobby( @GetMapping("/{lobbyCode}/leave") public ResponseEntity leaveLobby( @PathVariable String lobbyCode, @RequestParam String uuid) { - boolean wasStarted = service.getIsStarted(lobbyCode); boolean left = service.leaveLobby(uuid, lobbyCode); if (left) { From 59f429d491f779e37ebd10c69294fb79cfd5f03f Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Wed, 17 Jun 2026 16:37:00 +0200 Subject: [PATCH 33/34] fix: change getPlayer from uuid to username --- .../codenames/backend/lobby/application/LobbyService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index 4c67baad..bcbeae9e 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -292,14 +292,14 @@ public Role getPlayerRole(String uuid, String lobbyCode) { * Retrieves a specific player from a lobby. * * @param lobbyCode the lobby code - * @param uuid the UUID of the player + * @param username the username of the player * @return the Player object, or null if not found */ - public Player getPlayer(String lobbyCode, String uuid) { + public Player getPlayer(String lobbyCode, String username) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { return lobby.getPlayerList().stream() - .filter(p -> p.uuid().equals(uuid)) + .filter(p -> p.username().equals(username)) .findFirst() .orElse(null); } From f78c389cb0a03b447850d91ad9635605d8739fab Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Wed, 17 Jun 2026 16:37:13 +0200 Subject: [PATCH 34/34] fix: combine V2 flyway migration scripts --- .../V2__add_cheat_flags_to_game_state.sql | 5 ----- .../db/migration/V2__add_player_uuid.sql | 12 ----------- ...uuid_and_add_cheat_flags_to_game_state.sql | 20 +++++++++++++++++++ 3 files changed, 20 insertions(+), 17 deletions(-) delete mode 100644 src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql delete mode 100644 src/main/resources/db/migration/V2__add_player_uuid.sql create mode 100644 src/main/resources/db/migration/V2__add_player_uuid_and_add_cheat_flags_to_game_state.sql diff --git a/src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql b/src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql deleted file mode 100644 index 664a2dc9..00000000 --- a/src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE game_state - ADD COLUMN red_team_cheat_used BOOLEAN DEFAULT FALSE NOT NULL; - -ALTER TABLE game_state - ADD COLUMN blue_team_cheat_used BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__add_player_uuid.sql b/src/main/resources/db/migration/V2__add_player_uuid.sql deleted file mode 100644 index 1a73e104..00000000 --- a/src/main/resources/db/migration/V2__add_player_uuid.sql +++ /dev/null @@ -1,12 +0,0 @@ -ALTER TABLE player - ADD COLUMN uuid VARCHAR(36); - -UPDATE player -SET uuid = gen_random_uuid()::text; - -ALTER TABLE player - ALTER COLUMN uuid SET NOT NULL; - -ALTER TABLE player - ADD CONSTRAINT unique_player_uuid - UNIQUE (uuid); \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__add_player_uuid_and_add_cheat_flags_to_game_state.sql b/src/main/resources/db/migration/V2__add_player_uuid_and_add_cheat_flags_to_game_state.sql new file mode 100644 index 00000000..86c4a9ea --- /dev/null +++ b/src/main/resources/db/migration/V2__add_player_uuid_and_add_cheat_flags_to_game_state.sql @@ -0,0 +1,20 @@ +-- add uuid to player +ALTER TABLE player + ADD COLUMN uuid VARCHAR(36); + +UPDATE player +SET uuid = gen_random_uuid()::text; + +ALTER TABLE player + ALTER COLUMN uuid SET NOT NULL; + +ALTER TABLE player + ADD CONSTRAINT unique_player_uuid + UNIQUE (uuid); + +-- add cheat flags to game state +ALTER TABLE game_state + ADD COLUMN red_team_cheat_used BOOLEAN DEFAULT FALSE NOT NULL; + +ALTER TABLE game_state + ADD COLUMN blue_team_cheat_used BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file