diff --git a/Dockerfile b/Dockerfile index 48d66258..7f684898 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /app COPY . . RUN mvn clean package -DskipTests -FROM eclipse-temurin:17-jdk-jammy +FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY --from=build /app/target/*.jar app.jar diff --git a/docker-compose.yml b/docker-compose.yml index c3c49d10..22217445 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,14 @@ services: postgres: image: postgres:16 restart: unless-stopped + deploy: + resources: + limits: + cpus: "0.25" + memory: 256M + reservations: + cpus: "0.10" + memory: 128M environment: POSTGRES_DB: codenames POSTGRES_USER: codenames_user @@ -17,11 +25,18 @@ services: backend: image: ghcr.io/ss26-se2-codenames/backend:latest restart: unless-stopped + deploy: + resources: + limits: + cpus: "0.50" + memory: 512M + reservations: + cpus: "0.25" + memory: 256M ports: - "53213:8080" - volumes: - - ./data:/app/data environment: # Inject datasource url into application.yml when containerized + JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=75" SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/codenames SPRING_DATASOURCE_USERNAME: codenames_user SPRING_DATASOURCE_PASSWORD: codenames_pass @@ -30,4 +45,4 @@ services: condition: service_healthy volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..080f8789 --- /dev/null +++ b/lombok.config @@ -0,0 +1,6 @@ +#Source - https://stackoverflow.com/a/48971823 +#Posted by mladzo, modified by community. See post 'Timeline' for change history +#Retrieved 2026-06-04, License - CC BY-SA 3.0 + +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true diff --git a/pom.xml b/pom.xml index d20b2dcc..c06821f8 100644 --- a/pom.xml +++ b/pom.xml @@ -1,30 +1,31 @@ - + 4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.5 - + com.codenames Codenames_Backend 0.0.1-SNAPSHOT Codenames_Backend Codenames_Backend - + - + - + - - - - + + + + 17 @@ -58,7 +59,7 @@ org.springframework.boot spring-boot-starter-web - + org.springframework.boot spring-boot-starter-websocket @@ -93,6 +94,10 @@ h2 runtime + + org.springframework.boot + spring-boot-starter-security + diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java b/src/main/java/com/codenames/codenames/backend/chat/api/ChatController.java similarity index 89% rename from src/main/java/com/codenames/codenames/backend/chat/ChatController.java rename to src/main/java/com/codenames/codenames/backend/chat/api/ChatController.java index 91c22ddc..9fc8f11c 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java +++ b/src/main/java/com/codenames/codenames/backend/chat/api/ChatController.java @@ -1,9 +1,11 @@ -package com.codenames.codenames.backend.chat; +package com.codenames.codenames.backend.chat.api; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.utility.ChatMessageType; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; +import com.codenames.codenames.backend.chat.application.ChatService; +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 org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatDto.java b/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatDto.java similarity index 71% rename from src/main/java/com/codenames/codenames/backend/chat/ChatDto.java rename to src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatDto.java index 8cb695da..87ee0064 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatDto.java +++ b/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatDto.java @@ -1,6 +1,4 @@ -package com.codenames.codenames.backend.chat; - -import com.codenames.codenames.backend.utility.ChatMessageType; +package com.codenames.codenames.backend.chat.api.dto; /** * Record class for messages that is used in controller. diff --git a/src/main/java/com/codenames/codenames/backend/utility/ChatMessageType.java b/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java similarity index 54% rename from src/main/java/com/codenames/codenames/backend/utility/ChatMessageType.java rename to src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java index daeb3731..07dcf5ef 100644 --- a/src/main/java/com/codenames/codenames/backend/utility/ChatMessageType.java +++ b/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java @@ -1,8 +1,9 @@ -package com.codenames.codenames.backend.utility; +package com.codenames.codenames.backend.chat.api.dto; /** * Enum representing the type of chat message. */ public enum ChatMessageType { - CHAT -} + CHAT, + SYSTEM +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatService.java b/src/main/java/com/codenames/codenames/backend/chat/application/ChatService.java similarity index 94% rename from src/main/java/com/codenames/codenames/backend/chat/ChatService.java rename to src/main/java/com/codenames/codenames/backend/chat/application/ChatService.java index 15efe4ac..bb7f0c6e 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatService.java +++ b/src/main/java/com/codenames/codenames/backend/chat/application/ChatService.java @@ -1,5 +1,7 @@ -package com.codenames.codenames.backend.chat; +package com.codenames.codenames.backend.chat.application; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.domain.ChatHistory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatHistory.java b/src/main/java/com/codenames/codenames/backend/chat/domain/ChatHistory.java similarity index 89% rename from src/main/java/com/codenames/codenames/backend/chat/ChatHistory.java rename to src/main/java/com/codenames/codenames/backend/chat/domain/ChatHistory.java index 3cc10cab..26e46015 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatHistory.java +++ b/src/main/java/com/codenames/codenames/backend/chat/domain/ChatHistory.java @@ -1,5 +1,6 @@ -package com.codenames.codenames.backend.chat; +package com.codenames.codenames.backend.chat.domain; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/CardEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/CardEntity.java new file mode 100644 index 00000000..700afe32 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/entity/CardEntity.java @@ -0,0 +1,38 @@ +package com.codenames.codenames.backend.database.entity; + +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 lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Persistent entity representing a card record within the database schema. + */ +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "card") +public class CardEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne + @JoinColumn(name = "lobby_code", nullable = false) + private LobbyEntity lobbyEntity; + @Column(nullable = false) + private int position; + @Column(length = 11, nullable = false) + private String word; + @Column(length = 8, nullable = false) + private String color; + @Column(name = "is_guessed", nullable = false) + private Boolean isGuessed; +} diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java new file mode 100644 index 00000000..2fd26858 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java @@ -0,0 +1,44 @@ +package com.codenames.codenames.backend.database.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Persistent entity representing a gameState record within the database schema. + */ +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "game_state") +public class GameStateEntity { + @Id + @Column(name = "lobby_code") + private String lobbyCode; + @OneToOne + @MapsId + @JoinColumn(name = "lobby_code") + private LobbyEntity lobbyEntity; + @Column(name = "current_turn", length = 4, nullable = false) + private String currentTurn; + @Column(name = "current_phase", length = 9, nullable = false) + private String currentPhase; + @Column(name = "clue_word", length = 20) + private String clueWord; + @Column(name = "clue_guess_amount") + private int clueGuessAmount; + @Column(name = "remaining_guesses") + private int remainingGuesses; + @Column(name = "red_team_cheat_used", nullable = false) + private boolean redTeamCheatUsed; + @Column(name = "blue_team_cheat_used", nullable = false) + private boolean blueTeamCheatUsed; +} diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/LobbyEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/LobbyEntity.java new file mode 100644 index 00000000..24d1b59c --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/entity/LobbyEntity.java @@ -0,0 +1,43 @@ +package com.codenames.codenames.backend.database.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SourceType; + +/** + * Persistent entity representing a lobby record within the database schema. + */ +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "lobby") +public class LobbyEntity { + @Id + @Column(name = "lobby_code", length = 5, nullable = false) + private String lobbyCode; + @Column(name = "created_at", insertable = false, updatable = false, nullable = false) + @CreationTimestamp(source = SourceType.DB) + private Timestamp createdAt; + + @OneToOne(mappedBy = "lobbyEntity", cascade = CascadeType.ALL, orphanRemoval = true) + private GameStateEntity gameStateEntity; + + @OneToMany(mappedBy = "lobbyEntity", cascade = CascadeType.ALL, orphanRemoval = true) + private List playerEntities = new ArrayList<>(); + + @OneToMany(mappedBy = "lobbyEntity", cascade = CascadeType.ALL, orphanRemoval = true) + private List cardEntities = new ArrayList<>(); +} 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 new file mode 100644 index 00000000..c7beedfc --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/entity/PlayerEntity.java @@ -0,0 +1,36 @@ +package com.codenames.codenames.backend.database.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Persistent entity representing a player record within the database schema. + */ +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "player") +public class PlayerEntity { + @Id + @Column(nullable = false) + private String uuid; + @ManyToOne + @JoinColumn(name = "lobby_code", nullable = false) + private LobbyEntity lobbyEntity; + @Column(length = 20, nullable = false) + private String username; + @Column(name = "is_host", nullable = false) + private Boolean isHost; + @Column(length = 4, nullable = false) + private String team; + @Column(length = 9, nullable = false) + private String role; +} 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 new file mode 100644 index 00000000..f37483d2 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java @@ -0,0 +1,105 @@ +package com.codenames.codenames.backend.database.persistence; + +import com.codenames.codenames.backend.database.entity.CardEntity; +import com.codenames.codenames.backend.database.entity.GameStateEntity; +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.entity.PlayerEntity; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Clue; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Helper class that is called by the PersistenceService to map all the tables when a snapshot is + * created. + */ +@Slf4j +@Component +public class PersistenceMapper { + /** + * This method creates and returns a lobby entity that is to be stored into the DB. + * + * @param lobbyCode the code to act as PK and FK for other tables + * @param gameManager the gameManager of an active game to derive information for other tables + * @param players the list of players in an active game + * @return the lobby entity to be stored into dp + */ + public LobbyEntity mapAggregateParentLobbyEntity( + String lobbyCode, GameManager gameManager, List players) { + LobbyEntity lobbyEntity = new LobbyEntity(); + lobbyEntity.setLobbyCode(lobbyCode); + + List cardList = gameManager.getCardList(); + + lobbyEntity.setGameStateEntity(mapGameState(lobbyEntity, lobbyCode, gameManager)); + lobbyEntity.setCardEntities(mapCard(lobbyEntity, cardList)); + lobbyEntity.setPlayerEntities(mapPlayer(lobbyEntity, players)); + log.info("Mapping has been completed"); + return lobbyEntity; + } + + private List mapPlayer( + LobbyEntity lobbyEntity, List players) { + List playerList = new ArrayList<>(); + + for (int i = 0; i < players.size(); i++) { + PlayerEntity playerEntity = new PlayerEntity(); + playerEntity.setLobbyEntity(lobbyEntity); + playerEntity.setUsername(players.get(i).username()); + 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; + } + + // Since we are using ORM we always need to map the object as well. + + private GameStateEntity mapGameState( + LobbyEntity lobbyEntity, String lobbyCode, GameManager gameManager) { + GameStateEntity gameStateEntity = new GameStateEntity(); + gameStateEntity.setLobbyEntity(lobbyEntity); + // Since we do not have autogenerating PK we need to manually map. + gameStateEntity.setLobbyCode(lobbyCode); + gameStateEntity.setCurrentTurn(gameManager.getCurrentTurn().name()); + gameStateEntity.setCurrentPhase(gameManager.getCurrentPhase().name()); + gameStateEntity.setClueWord(gameManager.getCurrentClueWord()); + + Clue currentClue = gameManager.getCurrentClue(); + if (currentClue != null) { + gameStateEntity.setClueGuessAmount(gameManager.getCurrentClue().guessAmount()); + gameStateEntity.setRemainingGuesses(gameManager.getRemainingGuesses()); + } else { + gameStateEntity.setClueGuessAmount(0); + gameStateEntity.setRemainingGuesses(0); + } + + gameStateEntity.setRedTeamCheatUsed( + gameManager.isRedTeamCheatUsed()); + + gameStateEntity.setBlueTeamCheatUsed( + gameManager.isBlueTeamCheatUsed()); + + return gameStateEntity; + } + + private List mapCard(LobbyEntity lobbyEntity, List cards) { + List cardList = new ArrayList<>(); + for (int i = 0; i < cards.size(); i++) { + CardEntity cardEntity = new CardEntity(); + cardEntity.setLobbyEntity(lobbyEntity); + cardEntity.setPosition(i); + cardEntity.setWord(cards.get(i).getWord()); + cardEntity.setColor(cards.get(i).getColor().name()); + cardEntity.setIsGuessed(cards.get(i).isGuessed()); + cardList.add(cardEntity); + } + return cardList; + } +} diff --git a/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceService.java b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceService.java new file mode 100644 index 00000000..48fd3177 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceService.java @@ -0,0 +1,57 @@ +package com.codenames.codenames.backend.database.persistence; + +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.repository.LobbyRepository; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** Service class for storing a snapshot into the database. */ +@Service +@Slf4j +public class PersistenceService { + private final LobbyService lobbyService; + private final GameService gameService; + private final PersistenceMapper persistenceMapper; + private final LobbyRepository lobbyRepository; + + /** + * Constructor for the PersistenceService class. + * + * @param lobbyService the service class for lobby, from which we derive playerDto + * @param gameService the service class for game, from which we derive gameManager + * @param persistenceMapper the helper class used for ORM (Object Relational Mapping) + * @param lobbyRepository the repository for saving to DB with cascade + */ + public PersistenceService(LobbyService lobbyService, GameService gameService, + PersistenceMapper persistenceMapper, LobbyRepository lobbyRepository) { + this.lobbyService = lobbyService; + this.gameService = gameService; + this.persistenceMapper = persistenceMapper; + this.lobbyRepository = lobbyRepository; + } + + /** + * Method to create a snapshot and save it to the database. + * + * @param lobbyCode the code to identify a lobby + */ + @Transactional + public void saveSnapShot(String lobbyCode) { + log.info("Saving snapshot of lobby: {}", lobbyCode); + GameManager gameManager = gameService.getGameState(lobbyCode); + log.info("Card state for lobby {}: {}", lobbyCode, gameManager.getCardList()); + List playerList = lobbyService.getPlayersDto(lobbyCode); + LobbyEntity lobbySnapShot = + persistenceMapper.mapAggregateParentLobbyEntity(lobbyCode, gameManager, playerList); + lobbyRepository.deleteById(lobbyCode); + lobbyRepository.flush(); + lobbyRepository.save(lobbySnapShot); + log.info("Snapshot has been saved to DB"); + } +} diff --git a/src/main/java/com/codenames/codenames/backend/database/repository/LobbyRepository.java b/src/main/java/com/codenames/codenames/backend/database/repository/LobbyRepository.java new file mode 100644 index 00000000..31128aae --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/repository/LobbyRepository.java @@ -0,0 +1,11 @@ +package com.codenames.codenames.backend.database.repository; + +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Repository class for Lobby that has the default available methods needed to perform SQL queries. + */ +@Repository +public interface LobbyRepository extends JpaRepository {} 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 new file mode 100644 index 00000000..9dd4301a --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/repository/PlayerRepository.java @@ -0,0 +1,24 @@ +package com.codenames.codenames.backend.database.repository; + +import com.codenames.codenames.backend.database.entity.PlayerEntity; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Repository class for Lobby that has the default available methods needed to perform SQL queries. + */ +@Repository +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 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 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 new file mode 100644 index 00000000..26ea7023 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java @@ -0,0 +1,131 @@ +package com.codenames.codenames.backend.database.restoration; + +import com.codenames.codenames.backend.database.entity.CardEntity; +import com.codenames.codenames.backend.database.entity.GameStateEntity; +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.entity.PlayerEntity; +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.ClueDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +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; +import java.util.List; +import org.springframework.stereotype.Component; + +/** + * Helper class responsible for creating Lobby and GameStateDto, used by RestorationService. + */ +@Component +public class RestorationMapper { + + /** + * Exposed method to generate a Lobby object. + * + * @param lobbyEntity the lobby entry from the database + * @return a Lobby object to be used by RestorationService + */ + public Lobby mapToLobby(LobbyEntity lobbyEntity) { + String lobbyCode = lobbyEntity.getLobbyCode(); + List playerDtoList = mapToPlayerDto(lobbyEntity); + return buildLobby(lobbyCode, playerDtoList); + } + + /** + * Exposed method to generate a GameStateDto object. + * + * @param lobbyEntity the lobby entry from the database + * @return a Lobby object to be used by RestorationService + */ + public GameStateDto mapToGameStateDto(LobbyEntity lobbyEntity) { + GameStateEntity gameStateEntity = lobbyEntity.getGameStateEntity(); + if (gameStateEntity == null) { + throw new IllegalStateException("Game state entity is null"); + } + + // valueOf takes string and returns enum of that string + Team currentTeam = Team.valueOf(gameStateEntity.getCurrentTurn()); + Role currentPhase = Role.valueOf(gameStateEntity.getCurrentPhase()); + ClueDto clueDto = null; + if (gameStateEntity.getClueWord() != null) { + clueDto = new ClueDto(gameStateEntity.getClueWord(), gameStateEntity.getClueGuessAmount()); + } + int remainingGuesses = gameStateEntity.getRemainingGuesses(); + List cardList = mapToCardDto(lobbyEntity); + return new GameStateDto( + null, + currentTeam, + currentPhase, + clueDto, + remainingGuesses, + cardList, + gameStateEntity.isRedTeamCheatUsed(), + gameStateEntity.isBlueTeamCheatUsed()); + } + + private List mapToCardDto(LobbyEntity lobbyEntity) { + List cardList = new ArrayList<>(25); + + for (CardEntity currentCardEntity : lobbyEntity.getCardEntities()) { + CardDto cardDto = + new CardDto( + currentCardEntity.getWord(), + Color.valueOf(currentCardEntity.getColor()), + currentCardEntity.getIsGuessed()); + cardList.add(cardDto); + } + return cardList; + } + + private List mapToPlayerDto(LobbyEntity lobbyEntity) { + List playerList = new ArrayList<>(); + + for (int i = 0; i < lobbyEntity.getPlayerEntities().size(); i++) { + PlayerEntity playerEntity = lobbyEntity.getPlayerEntities().get(i); + Team team = null; + if (playerEntity.getTeam() != null) { + team = Team.valueOf(playerEntity.getTeam()); + } + Role role = null; + if (playerEntity.getRole() != null) { + role = Role.valueOf(playerEntity.getRole()); + } + PlayerDto playerDto = + new PlayerDto( + playerEntity.getUsername(), + team, + role, + playerEntity.getIsHost(), + playerEntity.getUuid()); + playerList.add(playerDto); + } + return playerList; + } + + private Lobby buildLobby(String lobbyCode, List players) { + + Lobby lobby = new Lobby(lobbyCode); + + for (PlayerDto player : players) { + lobby.addRestoredPlayer( + new Player( + player.username(), + player.isHost(), + player.uuid())); + + if (player.team() != null) { + lobby.setPlayerTeam(player.uuid(), player.team()); + } + + if (player.role() != null) { + lobby.setPlayerRole(player.uuid(), player.role()); + } + } + + return lobby; + } +} diff --git a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationService.java b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationService.java new file mode 100644 index 00000000..e7b85423 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationService.java @@ -0,0 +1,87 @@ +package com.codenames.codenames.backend.database.restoration; + +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.repository.LobbyRepository; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.game.domain.GameManagerFactory; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Lobby; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +/** Service class responsible for re-creating all lobbies and game managers. */ +@Slf4j +@Service +public class RestorationService { + private final RestorationMapper restorationMapper; + private final LobbyRepository lobbyRepository; + + private final LobbyService lobbyService; + private final GameService gameService; + private final GameManagerFactory gameManagerFactory; + + private final PlatformTransactionManager transactionManager; + + /** + * Constructor for the service class. + * + * @param restorationMapper the helper class used to create Lobby and GameStateDto + * @param lobbyRepository the lobby entry from the database + * @param lobbyService service class responsible for adding a lobby to a list of active lobbies + * @param gameService service class responsible for binding a gameManager to a lobby + * @param gameManagerFactory factory class responsible for creating a gameManager + */ + public RestorationService( + RestorationMapper restorationMapper, + LobbyRepository lobbyRepository, + LobbyService lobbyService, + GameService gameService, + GameManagerFactory gameManagerFactory, + PlatformTransactionManager transactionManager) { + this.restorationMapper = restorationMapper; + this.lobbyRepository = lobbyRepository; + this.lobbyService = lobbyService; + this.gameService = gameService; + this.gameManagerFactory = gameManagerFactory; + this.transactionManager = transactionManager; + } + + /** Using PostConstruct we automatically call this method when the container restarts. */ + // @PostConstruct methods technically gets called right after the beans are initialized + // Turns out you cannot have PostConstruct and Transactional annotation together + // Solution is to use Transaction Template --> Solution found on StackOverflow + @PostConstruct + public void restoreOnContainerStart() { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setReadOnly(true); + + transactionTemplate.execute( + new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + for (LobbyEntity lobbyEntity : lobbyRepository.findAll()) { + String lobbyCode = lobbyEntity.getLobbyCode(); + + Lobby lobbyObj = restorationMapper.mapToLobby(lobbyEntity); + log.info("Attempting to restore lobby: {}", lobbyCode); + lobbyService.restoreLobby(lobbyCode, lobbyObj); + log.info("Lobby has been restored: {}", lobbyCode); + + GameStateDto gameStateDtoObj = restorationMapper.mapToGameStateDto(lobbyEntity); + GameManager gameManagerObj = gameManagerFactory.createFromSnapshot(gameStateDtoObj); + log.info("Attempting to restore game: {}", lobbyCode); + gameService.restoreGameManager(lobbyCode, gameManagerObj); + log.info("Game has been restored: {}", lobbyCode); + } + } + }); + } +} diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java similarity index 59% rename from src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java rename to src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java index 59d41e9b..9bb465d5 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java @@ -1,12 +1,17 @@ -package com.codenames.codenames.backend.game.controller; - -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.game.dto.ClueMessage; -import com.codenames.codenames.backend.game.dto.PassTurnMessage; -import com.codenames.codenames.backend.game.dto.RevealCardMessage; -import com.codenames.codenames.backend.game.dto.StartGameMessage; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; +package com.codenames.codenames.backend.game.api; + +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; +import com.codenames.codenames.backend.database.persistence.PersistenceService; +import com.codenames.codenames.backend.game.api.dto.CheatCardMessage; +import com.codenames.codenames.backend.game.api.dto.ClueMessage; +import com.codenames.codenames.backend.game.api.dto.PassTurnMessage; +import com.codenames.codenames.backend.game.api.dto.RevealCardMessage; +import com.codenames.codenames.backend.game.api.dto.StartGameMessage; +import com.codenames.codenames.backend.game.application.CheatService; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.CheatResult; +import com.codenames.codenames.backend.game.domain.Clue; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; @@ -21,9 +26,10 @@ public class GameSocketController { private final GameService gameService; + private final CheatService cheatService; private final SimpMessagingTemplate messagingTemplate; - private final SystemStatePersistenceService persistenceService; + private final PersistenceService persistenceService; private static final String GAME_TOPIC_PREFIX = "/topic/game/"; @@ -33,15 +39,53 @@ public class GameSocketController { * @param gameService service responsible for gameplay logic * @param messagingTemplate template used for broadcasting websocket messages * @param persistenceService service used to persist current backend state + * @param cheatService service responsible for cheat handling */ public GameSocketController( GameService gameService, SimpMessagingTemplate messagingTemplate, - SystemStatePersistenceService persistenceService) { + PersistenceService persistenceService, + CheatService cheatService) { this.gameService = gameService; this.messagingTemplate = messagingTemplate; this.persistenceService = persistenceService; + this.cheatService = cheatService; + } + + /** + * Handles a cheat request and sends the result only to the requesting player. + * + * @param message the cheat request containing lobby, username and selected cards + */ + @MessageMapping("/cheat") + public void useCheat(CheatCardMessage message) { + + CheatResult result = + cheatService.useCheat( + message.getLobbyCode(), + message.getUsername(), + message.getPositions()); + + if (result == null) { + return; + } + + ChatDto systemMessage = + new ChatDto( + "System", + result.message(), + ChatMessageType.SYSTEM); + + messagingTemplate.convertAndSend( + "/topic/chat/" + + message.getLobbyCode() + + "/" + + result.team() + + "/operative", + systemMessage); + + persistenceService.saveSnapShot(message.getLobbyCode()); } /** @@ -68,7 +112,7 @@ public void startGame(StartGameMessage message) { public void revealCard(RevealCardMessage message) { gameService.flipCard(message.getLobbyCode(), message.getPosition(), message.getCurrentTurn()); - persistenceService.persistCurrentState(); + persistenceService.saveSnapShot(message.getLobbyCode()); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), @@ -89,7 +133,7 @@ public void submitClue(ClueMessage message) { message.getLobbyCode(), new Clue(message.getWord(), message.getGuessAmount()), message.getCurrentTurn()); - persistenceService.persistCurrentState(); + persistenceService.saveSnapShot(message.getLobbyCode()); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), @@ -105,7 +149,7 @@ public void submitClue(ClueMessage message) { public void passTurn(PassTurnMessage message) { gameService.passTurn(message.getLobbyCode(), message.getCurrentTurn()); - persistenceService.persistCurrentState(); + persistenceService.saveSnapShot(message.getLobbyCode()); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), diff --git a/src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/CardDto.java similarity index 53% rename from src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/CardDto.java index e367571e..060c92a8 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/CardDto.java @@ -1,6 +1,6 @@ -package com.codenames.codenames.backend.serialization; +package com.codenames.codenames.backend.game.api.dto; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.game.domain.Color; /** * Represents the state of a single card, for JSON serialization. @@ -9,4 +9,4 @@ * @param color the color of the card (could also be "hidden") * @param isGuessed the guess state of the card */ -public record CardDataTransferObject(String word, Color color, boolean isGuessed) {} +public record CardDto(String word, Color color, boolean isGuessed) {} diff --git a/src/main/java/com/codenames/codenames/backend/game/api/dto/CheatCardMessage.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/CheatCardMessage.java new file mode 100644 index 00000000..dba6690f --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/CheatCardMessage.java @@ -0,0 +1,16 @@ +package com.codenames.codenames.backend.game.api.dto; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +/** + * WebSocket message for requesting a cheat check on selected cards. + */ +@Getter +@Setter +public class CheatCardMessage { + private String lobbyCode; + private String username; + private List positions; +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/ClueDto.java similarity index 87% rename from src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/ClueDto.java index 26a54187..31c3fcc8 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/ClueDto.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.game.dto; +package com.codenames.codenames.backend.game.api.dto; /** * Data transfer object for a clue, containing the clue word and the allowed number of guesses. diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/ClueMessage.java similarity index 73% rename from src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/ClueMessage.java index 6b120c95..289d3de5 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/ClueMessage.java @@ -1,6 +1,6 @@ -package com.codenames.codenames.backend.game.dto; +package com.codenames.codenames.backend.game.api.dto; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.lobby.domain.Team; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java similarity index 61% rename from src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java index 95f9c982..9b0e390f 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java @@ -1,8 +1,7 @@ -package com.codenames.codenames.backend.serialization; +package com.codenames.codenames.backend.game.api.dto; -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.List; /** @@ -14,9 +13,13 @@ * @param currentClue the current clue object, consisting of word and amount of guesses * @param cardList the cards on the board */ -public record GameStateDataTransferObject( +public record GameStateDto( Team winner, Team currentTurn, Role currentPhase, ClueDto currentClue, - List cardList) {} + int remainingGuesses, + List cardList, + boolean redTeamCheatUsed, + boolean blueTeamCheatUsed) { +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/PassTurnMessage.java similarity index 71% rename from src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/PassTurnMessage.java index 10812d83..a8cc721c 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/PassTurnMessage.java @@ -1,6 +1,6 @@ -package com.codenames.codenames.backend.game.dto; +package com.codenames.codenames.backend.game.api.dto; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.lobby.domain.Team; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/RevealCardMessage.java similarity index 74% rename from src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/RevealCardMessage.java index 940d4676..71c889ad 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/RevealCardMessage.java @@ -1,6 +1,6 @@ -package com.codenames.codenames.backend.game.dto; +package com.codenames.codenames.backend.game.api.dto; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.lobby.domain.Team; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/StartGameMessage.java similarity index 82% rename from src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java rename to src/main/java/com/codenames/codenames/backend/game/api/dto/StartGameMessage.java index e8d846aa..0a26de03 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/StartGameMessage.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.game.dto; +package com.codenames.codenames.backend.game.api.dto; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java b/src/main/java/com/codenames/codenames/backend/game/application/CardGenerator.java similarity index 94% rename from src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java rename to src/main/java/com/codenames/codenames/backend/game/application/CardGenerator.java index af2d1d2b..5c36eedc 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java +++ b/src/main/java/com/codenames/codenames/backend/game/application/CardGenerator.java @@ -1,6 +1,7 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.application; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Color; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/com/codenames/codenames/backend/game/application/CheatService.java b/src/main/java/com/codenames/codenames/backend/game/application/CheatService.java new file mode 100644 index 00000000..8b534fdf --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/application/CheatService.java @@ -0,0 +1,46 @@ +package com.codenames.codenames.backend.game.application; + +import com.codenames.codenames.backend.game.domain.CheatResult; +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 java.util.List; +import org.springframework.stereotype.Service; + +/** Service responsible for validating and executing cheat requests. */ +@Service +public class CheatService { + + private final GameService gameService; + private final LobbyService lobbyService; + + /** + * Creates a new cheat service. + * + * @param gameService service responsible for game logic + * @param lobbyService service responsible for lobby/player data + */ + public CheatService(GameService gameService, LobbyService lobbyService) { + this.gameService = gameService; + this.lobbyService = lobbyService; + } + + /** + * Performs a cheat request for a player. + * + * @param lobbyCode the lobby code + * @param username the requesting username + * @param positions selected card positions + * @return the cheat result or null if the request is invalid + */ + public CheatResult useCheat(String lobbyCode, String username, List positions) { + Team team = lobbyService.getPlayerTeam(username, lobbyCode); + Role role = lobbyService.getPlayerRole(username, lobbyCode); + + if (team == null || role != Role.OPERATIVE) { + return null; + } + + return gameService.useCheat(lobbyCode, positions, team); + } +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/clue/ClueValidationService.java b/src/main/java/com/codenames/codenames/backend/game/application/ClueValidationService.java similarity index 84% rename from src/main/java/com/codenames/codenames/backend/clue/ClueValidationService.java rename to src/main/java/com/codenames/codenames/backend/game/application/ClueValidationService.java index 3ad973bf..78e58ad6 100644 --- a/src/main/java/com/codenames/codenames/backend/clue/ClueValidationService.java +++ b/src/main/java/com/codenames/codenames/backend/game/application/ClueValidationService.java @@ -1,7 +1,7 @@ -package com.codenames.codenames.backend.clue; +package com.codenames.codenames.backend.game.application; -import com.codenames.codenames.backend.playingfield.Board; -import com.codenames.codenames.backend.playingfield.Card; +import com.codenames.codenames.backend.game.domain.Board; +import com.codenames.codenames.backend.game.domain.Card; import java.util.List; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/game/application/GameService.java similarity index 79% rename from src/main/java/com/codenames/codenames/backend/playingfield/GameService.java rename to src/main/java/com/codenames/codenames/backend/game/application/GameService.java index a3ec0b52..fc7df620 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/game/application/GameService.java @@ -1,11 +1,16 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.application; -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.serialization.DataTransferObjectService; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.CheatResult; +import com.codenames.codenames.backend.game.domain.Clue; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.game.domain.GameManagerFactory; +import com.codenames.codenames.backend.game.mapping.DataTransferObjectService; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** @@ -13,6 +18,7 @@ * exposes the methods of the GameManager, so that the websocket controllers can have message * mappings to allow frontend to interact with the backend. */ +@Slf4j @Service public class GameService { @@ -39,6 +45,7 @@ public GameService(GameManagerFactory gameManagerFactory, DataTransferObjectServ */ public void createGameManager(String lobbyCode, Team startingTeam) { games.computeIfAbsent(lobbyCode, key -> gameManagerFactory.create(startingTeam)); + log.info("{}: a new game manager has been created.", lobbyCode); } /** @@ -48,6 +55,7 @@ public void createGameManager(String lobbyCode, Team startingTeam) { */ public void removeGame(String lobbyCode) { games.remove(lobbyCode); + log.info("{}: Game has been removed from list.", lobbyCode); } /** @@ -124,9 +132,9 @@ public void passTurn(String lobbyCode, Team callingTeam) { * @param lobbyCode the unique lobby code * @return the mapped game state transfer object */ - public GameStateDataTransferObject getCurrentGameState(String lobbyCode) { + public GameStateDto getCurrentGameState(String lobbyCode) { GameManager gm = getGame(lobbyCode); - return dtoService.createGameStateDataTransferObject( + return dtoService.createGameStateDto( gm, gm.getCurrentTurn(), gm.getCurrentPhase()); } @@ -151,9 +159,25 @@ public boolean isGameStarted(String lobbyCode) { * * @return map of lobby codes to game state dto snapshots */ - public Map getGameSnapshots() { + public Map getGameSnapshots() { return games.keySet().stream() .collect( java.util.stream.Collectors.toMap(lobbyCode -> lobbyCode, this::getCurrentGameState)); } + + /** + * Performs a cheat request for the given team. + * + * @param lobbyCode the lobby code of the game + * @param positions the selected card positions + * @param team the team requesting the cheat + * @return the cheat result or null if the request is invalid + */ + public CheatResult useCheat( + String lobbyCode, + List positions, + Team team) { + + return getGame(lobbyCode).useCheat(positions, team); + } } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/Board.java b/src/main/java/com/codenames/codenames/backend/game/domain/Board.java similarity index 95% rename from src/main/java/com/codenames/codenames/backend/playingfield/Board.java rename to src/main/java/com/codenames/codenames/backend/game/domain/Board.java index 4e9cefc6..ffb5ff70 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/Board.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/Board.java @@ -1,6 +1,6 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.game.application.CardGenerator; import java.util.ArrayList; import java.util.List; import lombok.Getter; diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/Card.java b/src/main/java/com/codenames/codenames/backend/game/domain/Card.java similarity index 89% rename from src/main/java/com/codenames/codenames/backend/playingfield/Card.java rename to src/main/java/com/codenames/codenames/backend/game/domain/Card.java index 70d6c353..8c15a2bc 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/Card.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/Card.java @@ -1,10 +1,11 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; -import com.codenames.codenames.backend.utility.Color; import lombok.Getter; +import lombok.ToString; /** Represents a single card on a board. */ @Getter +@ToString public class Card { private final String word; private final Color color; diff --git a/src/main/java/com/codenames/codenames/backend/game/domain/CheatResult.java b/src/main/java/com/codenames/codenames/backend/game/domain/CheatResult.java new file mode 100644 index 00000000..94fdfc03 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/domain/CheatResult.java @@ -0,0 +1,12 @@ +package com.codenames.codenames.backend.game.domain; + +import com.codenames.codenames.backend.lobby.domain.Team; +/** + * Result of a cheat attempt. + * + * @param message private message for the player + * @param team the team that used the cheat + */ +public record CheatResult( + String message, + Team team) {} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/clue/Clue.java b/src/main/java/com/codenames/codenames/backend/game/domain/Clue.java similarity index 93% rename from src/main/java/com/codenames/codenames/backend/clue/Clue.java rename to src/main/java/com/codenames/codenames/backend/game/domain/Clue.java index bf067c1b..08fe9abe 100644 --- a/src/main/java/com/codenames/codenames/backend/clue/Clue.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/Clue.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.clue; +package com.codenames.codenames.backend.game.domain; /** Clue class that holds the clue word and guess amount + 1. */ public record Clue(String word, int guessAmount) { diff --git a/src/main/java/com/codenames/codenames/backend/utility/Color.java b/src/main/java/com/codenames/codenames/backend/game/domain/Color.java similarity index 69% rename from src/main/java/com/codenames/codenames/backend/utility/Color.java rename to src/main/java/com/codenames/codenames/backend/game/domain/Color.java index 37c4b6f6..3be9e0cf 100644 --- a/src/main/java/com/codenames/codenames/backend/utility/Color.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/Color.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.utility; +package com.codenames.codenames.backend.game.domain; /** * Enum representing the color of a card on the board. diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java similarity index 76% rename from src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java rename to src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java index 81aca161..72d31828 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java @@ -1,12 +1,11 @@ -package com.codenames.codenames.backend.playingfield; - -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +package com.codenames.codenames.backend.game.domain; + +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.CardGenerator; +import com.codenames.codenames.backend.game.application.ClueValidationService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.List; import lombok.Getter; @@ -36,6 +35,9 @@ public class GameManager { @Getter private Team currentTurn; @Getter private Role currentPhase; + @Getter private boolean redTeamCheatUsed = false; + @Getter private boolean blueTeamCheatUsed = false; + /** * Constructor for a new GameManager and initializes the playing board. * @@ -73,7 +75,7 @@ public GameManager( * @param clueValidationService clue validation service */ public GameManager( - GameStateDataTransferObject state, ClueValidationService clueValidationService) { + GameStateDto state, ClueValidationService clueValidationService) { if (state.cardList() == null || state.cardList().isEmpty()) { throw new IllegalArgumentException("cards cannot be null or empty"); } @@ -84,10 +86,13 @@ public GameManager( this.currentTurn = state.currentTurn(); this.currentPhase = state.currentPhase(); this.winner = state.winner(); + this.redTeamCheatUsed = state.redTeamCheatUsed(); + this.blueTeamCheatUsed = state.blueTeamCheatUsed(); this.currentClue = state.currentClue() == null ? null : new Clue(state.currentClue().word(), state.currentClue().guessAmount()); + this.remainingGuesses = state.remainingGuesses(); this.clueValidationService = clueValidationService; List cards = state.cardList().stream().map(this::toCard).toList(); @@ -100,7 +105,7 @@ public GameManager( this.board = new Board(cards); } - private Card toCard(CardDataTransferObject cardDto) { + private Card toCard(CardDto cardDto) { Card card = new Card(cardDto.word(), cardDto.color()); if (cardDto.isGuessed()) { card.setIsGuessedTrue(); @@ -304,4 +309,78 @@ private void checkCorrectTurn(Team team, Role role) { throw new IllegalStateException("Not your turn/ role"); } } + + /** + * Checks whether a card position is within the valid board range. + * + * @param position the position to validate + * @return true if the position is valid + */ + private boolean isValidPosition(Integer position) { + return position != null + && position >= 0 + && position < board.getCardList().size(); + } + + /** + * Performs the cheat action for the current team. + * The team may use the cheat only once per game. + * If at least one selected card belongs to the team, + * one correct card is returned. + * + * @param positions selected card positions + * @param team the team requesting the cheat + * @return the cheat result or null if the request is invalid + */ + public CheatResult useCheat(List positions, Team team) { + + if (positions == null || positions.isEmpty()) { + return null; + } + + if (team != currentTurn || currentPhase != Role.OPERATIVE) { + return null; + } + + if (team == Team.RED && redTeamCheatUsed) { + return null; + } + + if (team == Team.BLUE && blueTeamCheatUsed) { + return null; + } + + List selectedCards = + positions.stream() + .filter(this::isValidPosition) + .filter(position -> !board.getIsGuessed(position)) + .map(position -> board.getCardList().get(position)) + .toList(); + + if (selectedCards.isEmpty()) { + return null; + } + + List correctCards = + selectedCards.stream() + .filter( + card -> + (team == Team.RED && card.getColor() == Color.RED) + || (team == Team.BLUE && card.getColor() == Color.BLUE)) + .toList(); + + if (team == Team.RED) { + redTeamCheatUsed = true; + } else { + blueTeamCheatUsed = true; + } + + if (correctCards.isEmpty()) { + return new CheatResult("Keine der ausgewählten Karten ist richtig.", team); + } + + Card correctCard = correctCards.get(0); + + return new CheatResult("Die Karte \"" + correctCard.getWord() + "\" ist richtig.", team); + } } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/game/domain/GameManagerFactory.java similarity index 76% rename from src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java rename to src/main/java/com/codenames/codenames/backend/game/domain/GameManagerFactory.java index 63b468ab..eb27f9d3 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/GameManagerFactory.java @@ -1,8 +1,9 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; -import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.CardGenerator; +import com.codenames.codenames.backend.game.application.ClueValidationService; +import com.codenames.codenames.backend.lobby.domain.Team; import org.springframework.stereotype.Component; /** Generates GameManager instances to be used by GameService. */ @@ -39,7 +40,7 @@ public GameManager create(Team startingTeam) { * @param snapshot persisted game state snapshot * @return restored game manager */ - public GameManager createFromSnapshot(GameStateDataTransferObject snapshot) { + public GameManager createFromSnapshot(GameStateDto snapshot) { return new GameManager(snapshot, clueValidationService); } } diff --git a/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java new file mode 100644 index 00000000..5687b1c2 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java @@ -0,0 +1,77 @@ +package com.codenames.codenames.backend.game.mapping; + +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.ClueDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Color; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** Service to create the DTO for the game state, which is then serialized into JSON. */ +@Slf4j +@Service +public class DataTransferObjectService { + + /** + * Helper method to create a card DTO with the correct visibility based on role and guess state. + * + * @param card card object from the board + * @return the card DTO for the game state DTO + */ + private CardDto createCardDto(Card card) { + Color displayColor = card.getColor(); + return new CardDto(card.getWord(), displayColor, card.isGuessed()); + } + + /** + * Creates the game state DTO that needs to be serialized into JSON. + * + * @param gameManager the game manager that holds the state of the game + * @param currentTurn the current turn + * @return a DTO of the current game state + */ + public GameStateDto createGameStateDto( + GameManager gameManager, Team currentTurn, Role currentPhase) { + + List cardList = gameManager.getCardList(); + List cardDataTransferObject = new ArrayList<>(); + for (Card card : cardList) { + cardDataTransferObject.add(createCardDto(card)); + } + if (gameManager.getWinner() != null) { + log.info("A Game is over. Winner is {}.", gameManager.getWinner()); + } + int remainingGuesses = gameManager.getRemainingGuesses(); + + if (gameManager.getCurrentClue() == null) { + return new GameStateDto( + gameManager.getWinner(), + currentTurn, + currentPhase, + null, + remainingGuesses, + cardDataTransferObject, + gameManager.isRedTeamCheatUsed(), + gameManager.isBlueTeamCheatUsed()); + } + + String word = gameManager.getCurrentClue().word(); + int guessAmount = gameManager.getCurrentClue().guessAmount(); + + return new GameStateDto( + gameManager.getWinner(), + currentTurn, + currentPhase, + new ClueDto(word, guessAmount), + remainingGuesses, + cardDataTransferObject, + gameManager.isRedTeamCheatUsed(), + gameManager.isBlueTeamCheatUsed()); + } +} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java deleted file mode 100644 index 05303511..00000000 --- a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.codenames.codenames.backend.lobby; - -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.codenames.codenames.backend.websocket.Player; -import java.security.SecureRandom; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import lombok.Getter; - -/** - * Represents a game lobby containing a limited number of playerList. - * - *

Supports adding and removing playerList - * while enforcing constraints such as maximum player count - * and unique usernames. - */ -@Getter -public class Lobby { - - private static final int MAX_PLAYERS = 4; - - private final String lobbyCode; - private final List playerList = new CopyOnWriteArrayList<>(); - private final SecureRandom random = new SecureRandom(); - /** - * Maps a username to the selected team. - */ - private final Map playerTeams; - - /** - * Maps a username to the selected role. - */ - private final Map playerRoles; - - /** - * Creates a new lobby and adds the initial player. - * - * @param lobbyCode the unique code identifying the lobby - * @param username the username of the player creating the lobby - */ - public Lobby(String lobbyCode, String username) { - this.lobbyCode = lobbyCode; - this.playerTeams = new HashMap<>(); - this.playerRoles = new HashMap<>(); - this.addPlayer(username, true); - } - - /** - * Adds a player to the lobby if capacity allows and the username is unique. - * - * @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 - */ - public boolean addPlayer(String username, boolean isHost) { - boolean alreadyExists = playerList.stream().anyMatch(p -> p.username().equals(username)); - - if (alreadyExists || playerList.size() >= MAX_PLAYERS) { - return false; - } - - playerList.add(new Player(username, isHost)); - return true; - } - - /** - * 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 - * - * @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); - } - - /** - * Removes a player from the lobby. - * - * @param username the username 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); - } - - /** - * Checks whether a player with the given username is in the lobby. - * - * @param username the username 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)); - } - - /** - * Sets the team for a player. - * - * @param username the username of the player - * @param team the team to assign - */ - public void setPlayerTeam(String username, Team team) { - playerTeams.put(username, team); - } - - /** - * Sets the role for a player. - * - * @param username the username of the player - * @param role the role to assign - */ - public void setPlayerRole(String username, Role role) { - playerRoles.put(username, role); - } - - /** - * Returns the team of a player. - * - * @param username the username of the player - * @return the assigned team, or {@code null} if none is assigned - */ - public Team getPlayerTeam(String username) { - return playerTeams.get(username); - } - - /** - * Returns the role of a player. - * - * @param username the username of the player - * @return the assigned role, or {@code null} if none is assigned - */ - public Role getPlayerRole(String username) { - return playerRoles.get(username); - } - - /** - * Randomly decides which team starts the game. - * - * @return the team that starts the game - */ - public Team decideStartingTeam() { - return random.nextBoolean() ? Team.RED : Team.BLUE; - } -} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java similarity index 55% rename from src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java rename to src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java index 4415ca4e..8d9f3558 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbyController.java @@ -1,10 +1,11 @@ -package com.codenames.codenames.backend.lobby.controller; +package com.codenames.codenames.backend.lobby.api; -import com.codenames.codenames.backend.lobby.dto.LobbyResponse; -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; +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 java.util.Objects; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,18 +26,15 @@ public class LobbyController { private final LobbyService service; - private final SystemStatePersistenceService persistenceService; private static final String LOBBY_NOT_FOUND = "Could not find lobby."; /** * Creates a new {@code LobbyController}. * * @param service the lobby service used to handle business logic - * @param persistenceService service used to persist current backend state */ - public LobbyController(LobbyService service, SystemStatePersistenceService persistenceService) { + public LobbyController(LobbyService service) { this.service = service; - this.persistenceService = persistenceService; } /** @@ -50,12 +48,18 @@ 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)); } else { - List players = service.getPlayersDto(lobbyCode); + + Player host = Objects.requireNonNull( + service.getPlayer(lobbyCode, username)); + + String hostUuid = host.uuid(); + return ResponseEntity.ok( - new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false)); + new LobbyResponse( + "Successfully created Lobby.", lobbyCode, players, false, hostUuid)); } } @@ -68,45 +72,52 @@ public ResponseEntity createLobby(@RequestParam String username) */ @GetMapping("/{lobbyCode}/join") public ResponseEntity joinLobby( - @RequestParam String username, @PathVariable String lobbyCode) { - boolean joined = service.joinLobby(username, lobbyCode); - if (joined) { + @RequestParam String username, + @PathVariable String lobbyCode, + @RequestParam(required = false) String uuid) { + Player joinedPlayer = service.joinLobby(username, lobbyCode, uuid); + + if (joinedPlayer != null) { 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()) + ); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); } } /** * 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) { - boolean wasStarted = service.getIsStarted(lobbyCode); - boolean left = service.leaveLobby(username, lobbyCode); + @PathVariable String lobbyCode, @RequestParam String uuid) { + boolean left = service.leaveLobby(uuid, lobbyCode); if (left) { service.checkLobbyStillHasPlayers(lobbyCode); - if (wasStarted) { - persistenceService.persistCurrentState(); - } - return ResponseEntity.ok( - new LobbyResponse( - "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false)); + new LobbyResponse( + "Left lobby successfully.", + lobbyCode, service.getPlayersDto(lobbyCode), + false, + null) + ); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false, null)); } } @@ -122,62 +133,76 @@ 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) + ); } - /** * Handles a request to select a team and role for a player. * * @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) { + @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( - new LobbyResponse( - "Position selected successfully.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false)); + new LobbyResponse( + "Position selected successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false, 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)); } } - /** * 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) { - persistenceService.persistCurrentState(); 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) + ); } 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) + ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java similarity index 80% rename from src/main/java/com/codenames/codenames/backend/websocket/GameController.java rename to src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java index 3cec2aee..803a9926 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/LobbySocketController.java @@ -1,7 +1,10 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.lobby.api; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.lobby.api.dto.JoinMessage; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Player; +import com.codenames.codenames.backend.shared.websocket.SessionRegistry; import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -17,7 +20,7 @@ */ @Slf4j @Controller -public class GameController { +public class LobbySocketController { private final LobbyService lobbyService; private final GameService gameService; @@ -25,14 +28,14 @@ public class GameController { private final SessionRegistry sessionRegistry; /** - * Creates a new {@code GameController}. + * Creates a new {@code LobbySocketController}. * * @param lobbyService the service handling lobby operations * @param gameService the service handling game state retrieval * @param messagingTemplate the messaging template used for broadcasting updates * @param sessionRegistry the registry managing WebSocket sessions */ - public GameController( + public LobbySocketController( LobbyService lobbyService, GameService gameService, SimpMessagingTemplate messagingTemplate, @@ -68,10 +71,15 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) log.error(e.getMessage()); } - boolean joined = lobbyService.joinLobby(message.getName(), message.getCode()); + 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())); + .anyMatch(player -> player.uuid().equals(message.getUuid())); if (!joined && !reconnect) { messagingTemplate.convertAndSend("/topic/errors/" + sessionId, "Join failed"); diff --git a/src/main/java/com/codenames/codenames/backend/websocket/JoinMessage.java b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java similarity index 77% rename from src/main/java/com/codenames/codenames/backend/websocket/JoinMessage.java rename to src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java index 24567a3e..e5f7bd23 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/JoinMessage.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessage.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.lobby.api.dto; import lombok.Getter; import lombok.Setter; @@ -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/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java similarity index 74% rename from src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java rename to src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java index 1b750c27..2368a4f4 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/LobbyResponse.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.lobby.dto; +package com.codenames.codenames.backend.lobby.api.dto; import java.util.List; @@ -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/dto/PlayerDto.java b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java similarity index 65% rename from src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java rename to src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java index 94edb594..ace76666 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/api/dto/PlayerDto.java @@ -1,7 +1,7 @@ -package com.codenames.codenames.backend.lobby.dto; +package com.codenames.codenames.backend.lobby.api.dto; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; /** * A data transfer object for communicating with the frontend, holds user-specific information. @@ -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) { } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyCodeGenerator.java similarity index 93% rename from src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java rename to src/main/java/com/codenames/codenames/backend/lobby/application/LobbyCodeGenerator.java index c878bcf2..7f24e08e 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyCodeGenerator.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.lobby.services; +package com.codenames.codenames.backend.lobby.application; import java.security.SecureRandom; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java similarity index 70% rename from src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java rename to src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java index 91b542db..bcbeae9e 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/application/LobbyService.java @@ -1,12 +1,13 @@ -package com.codenames.codenames.backend.lobby.services; - -import com.codenames.codenames.backend.chat.ChatService; -import com.codenames.codenames.backend.lobby.Lobby; -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.codenames.codenames.backend.websocket.Player; +package com.codenames.codenames.backend.lobby.application; + +import com.codenames.codenames.backend.chat.application.ChatService; +import com.codenames.codenames.backend.database.repository.LobbyRepository; +import com.codenames.codenames.backend.game.application.GameService; +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.List; import java.util.Map; import java.util.Objects; @@ -25,11 +26,11 @@ @Service public class LobbyService { - @Getter - private final Map lobbyList = new ConcurrentHashMap<>(); + @Getter private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; private final GameService gameService; private final ChatService chatService; + private final LobbyRepository lobbyRepository; /** * Creates a new {@code LobbyService}. @@ -37,10 +38,14 @@ public class LobbyService { * @param generator the lobby code generator used to create unique lobby codes */ public LobbyService( - LobbyCodeGenerator generator, ChatService chatService, GameService gameService) { + LobbyCodeGenerator generator, + ChatService chatService, + GameService gameService, + LobbyRepository lobbyRepository) { this.generator = generator; this.chatService = chatService; this.gameService = gameService; + this.lobbyRepository = lobbyRepository; } /** @@ -80,14 +85,30 @@ 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 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); + if (gameService.isGameStarted(lobbyCode)) { + + boolean validReconnect = + lobby.getPlayerList().stream() + .anyMatch(p -> p.uuid().equals(uuid)); + + if (!validReconnect) { + log.warn("{}: join rejected. Game started, no valid reconnect UUID.", 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 false; + return null; } /** @@ -103,14 +124,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; } @@ -121,27 +142,27 @@ public boolean leaveLobby(String username, 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; } @@ -158,6 +179,7 @@ public void checkLobbyStillHasPlayers(String lobbyCode) { lobbyList.remove(lobbyCode); chatService.clearLobbyHistory(lobbyCode); gameService.removeGame(lobbyCode); + lobbyRepository.deleteById(lobbyCode); log.info("{}: Lobby is empty, was removed from list.", lobbyCode); } } @@ -187,9 +209,10 @@ public List getPlayersDto(String lobbyCode) { player -> new PlayerDto( player.username(), - lobby.getPlayerTeam(player.username()), - lobby.getPlayerRole(player.username()), - player.isHost())) + lobby.getPlayerTeam(player.uuid()), + lobby.getPlayerRole(player.uuid()), + player.isHost(), + player.uuid())) .toList(); } return List.of(); @@ -199,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; } } @@ -238,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; } @@ -253,14 +276,32 @@ 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(uuid); + } + 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.getPlayerRole(username); + return lobby.getPlayerList().stream() + .filter(p -> p.username().equals(username)) + .findFirst() + .orElse(null); } return null; } @@ -270,14 +311,18 @@ public Role getPlayerRole(String username, String lobbyCode) { * 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; @@ -287,7 +332,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()) { @@ -299,7 +344,7 @@ public String getHost(String lobbyCode) { } for (Player p : players) { if (p.isHost()) { - return p.username(); + return p.uuid(); } } return ""; 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 new file mode 100644 index 00000000..bb466e73 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Lobby.java @@ -0,0 +1,194 @@ +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.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import lombok.Getter; + +/** + * Represents a game lobby containing a limited number of playerList. + * + *

Supports adding and removing playerList while enforcing constraints such as maximum player + * count and unique usernames. + */ +@Getter +public class Lobby { + + private static final int MAX_PLAYERS = 4; + + private final String lobbyCode; + private final List playerList = new CopyOnWriteArrayList<>(); + private final SecureRandom random = new SecureRandom(); + + /** Maps a UUID to the selected team. */ + private final Map playerTeams; + + /** Maps a UUID to the selected role. */ + private final Map playerRoles; + + /** + * Creates a new lobby and adds the initial player. + * + * @param lobbyCode the unique code identifying the lobby + * @param username the username of the player creating the lobby + */ + public Lobby(String lobbyCode, String username) { + this.lobbyCode = lobbyCode; + this.playerTeams = new HashMap<>(); + this.playerRoles = new HashMap<>(); + 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. + * + * @param username the username of the player + * @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) { + if (requestedUuid != null) { + Optional existingPlayerOpt = playerList.stream() + .filter(p -> p.uuid().equals(requestedUuid)) + .findFirst(); + + if (existingPlayerOpt.isPresent()) { + return existingPlayerOpt.get(); + } else { + return null; + } + } + + if (playerList.size() >= MAX_PLAYERS) { + return null; + } + + String newUuid = randomUUID().toString(); + Player newPlayer = new Player(username, isHost, newUuid); + playerList.add(newPlayer); + + return newPlayer; + } + /** + * Adds a player to lobby. + * + * @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) { + return addPlayer(username, isHost, null); + } + + /** + * 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 + * @return {@code true} if the player was added, {@code false} otherwise + */ + public Player addPlayer(String username, String uuid) { + return addPlayer(username, false, uuid); + } + + /** + * Removes a player from the lobby. + * + * @param uuid the uuid of the player to remove + */ + 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 uuid the uuid to check + * @return {@code true} if the player exists in the lobby, {@code false} otherwise + */ + public boolean hasPlayer(String uuid) { + return playerList.stream().anyMatch(p -> p.uuid().equals(uuid)); + } + + /** + * Sets the team for a player. + * + * @param uuid the UUID of the player + * @param team the team to assign + */ + public void setPlayerTeam(String uuid, Team team) { + playerTeams.put(uuid, team); + } + + /** + * Sets the role for a player. + * + * @param uuid the UUID of the player + * @param role the role to assign + */ + public void setPlayerRole(String uuid, Role role) { + playerRoles.put(uuid, role); + } + + /** + * Returns the team of a player. + * + * @param uuid the UUID of the player + * @return the assigned team, or {@code null} if none is assigned + */ + public Team getPlayerTeam(String uuid) { + return playerTeams.get(uuid); + } + + /** + * Returns the role of a player. + * + * @param uuid the UUID of the player + * @return the assigned role, or {@code null} if none is assigned + */ + public Role getPlayerRole(String uuid) { + return playerRoles.get(uuid); + } + + /** + * Randomly decides which team starts the game. + * + * @return the team that starts the game + */ + public Team decideStartingTeam() { + return random.nextBoolean() ? Team.RED : Team.BLUE; + } +} diff --git a/src/main/java/com/codenames/codenames/backend/websocket/Player.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java similarity index 55% rename from src/main/java/com/codenames/codenames/backend/websocket/Player.java rename to src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java index 38b6cd83..7febd494 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/Player.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Player.java @@ -1,9 +1,9 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.lobby.domain; /** * Represents a player connected to the system. * *

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) { } diff --git a/src/main/java/com/codenames/codenames/backend/utility/Role.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Role.java similarity index 66% rename from src/main/java/com/codenames/codenames/backend/utility/Role.java rename to src/main/java/com/codenames/codenames/backend/lobby/domain/Role.java index 11064f94..8da3a840 100644 --- a/src/main/java/com/codenames/codenames/backend/utility/Role.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Role.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.utility; +package com.codenames.codenames.backend.lobby.domain; /** * Represents the role of a player in the game. diff --git a/src/main/java/com/codenames/codenames/backend/utility/Team.java b/src/main/java/com/codenames/codenames/backend/lobby/domain/Team.java similarity index 54% rename from src/main/java/com/codenames/codenames/backend/utility/Team.java rename to src/main/java/com/codenames/codenames/backend/lobby/domain/Team.java index a615f1bb..3fc6a75b 100644 --- a/src/main/java/com/codenames/codenames/backend/utility/Team.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/domain/Team.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.utility; +package com.codenames.codenames.backend.lobby.domain; /** Represents the teams. */ public enum Team { diff --git a/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java b/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java deleted file mode 100644 index 1c58c2ca..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.codenames.codenames.backend.recovery; - -import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.Optional; -import java.util.concurrent.locks.ReentrantLock; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -/** - * JSON-backed storage for persisted system snapshots. - * - *

Writes are synchronized and performed via temporary-file replace to reduce corruption risk. - */ -@Component -@Getter -public class JsonStateStore { - - private final ObjectMapper objectMapper; - private final Path stateFilePath; - private final ReentrantLock ioLock = new ReentrantLock(); - - /** - * Creates a new state store with configured target file path. - * - * @param objectMapper mapper used for JSON serialization - * @param stateFile configured path to the persisted state file - */ - public JsonStateStore( - ObjectMapper objectMapper, @Value("${app.state-file:data/state.json}") String stateFile) { - this.objectMapper = objectMapper; - this.stateFilePath = Path.of(stateFile); - } - - /** - * Persists the full system snapshot atomically. - * - * @param snapshot full snapshot to store - */ - public void save(SystemSnapshot snapshot) { - ioLock.lock(); - try { - Path parentDirectory = stateFilePath.getParent(); - if (parentDirectory != null) { - Files.createDirectories(parentDirectory); - } - - Path tempFilePath = stateFilePath.resolveSibling(stateFilePath.getFileName() + ".tmp"); - objectMapper.writerWithDefaultPrettyPrinter().writeValue(tempFilePath.toFile(), snapshot); - - moveAtomically(tempFilePath, stateFilePath); - } catch (IOException exception) { - throw new IllegalStateException("Failed to persist state snapshot.", exception); - } finally { - ioLock.unlock(); - } - } - - /** - * Loads the persisted system snapshot when present. - * - * @return optional snapshot - */ - public Optional load() { - ioLock.lock(); - try { - if (!Files.exists(stateFilePath) || Files.size(stateFilePath) == 0L) { - return Optional.empty(); - } - return Optional.of(objectMapper.readValue(stateFilePath.toFile(), SystemSnapshot.class)); - } catch (IOException exception) { - throw new IllegalStateException("Failed to load persisted state snapshot.", exception); - } finally { - ioLock.unlock(); - } - } - - /** - * Replaces target file with source file using atomic move where supported. - * - * @param source temporary source file - * @param target final target file - * @throws IOException when move fails - */ - private void moveAtomically(Path source, Path target) throws IOException { - try { - Files.move( - source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException exception) { - Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); - } - } -} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java deleted file mode 100644 index 17bf2406..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.codenames.codenames.backend.recovery; - -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; -import org.springframework.stereotype.Service; - -/** - * Persists the current runtime state of the backend. - * - *

Collects lobby and game snapshots and stores them through the configured {@link - * JsonStateStore} to enable recovery after application restart. - */ -@Service -public class SystemStatePersistenceService { - - private final JsonStateStore stateStore; - private final LobbyService lobbyService; - private final GameService gameService; - - /** - * Creates a persistence service responsible for storing backend state snapshots. - * - * @param stateStore JSON storage implementation - * @param lobbyService service providing lobby snapshots - * @param gameService service providing game snapshots - */ - public SystemStatePersistenceService( - JsonStateStore stateStore, LobbyService lobbyService, GameService gameService) { - - this.stateStore = stateStore; - this.lobbyService = lobbyService; - this.gameService = gameService; - } - - /** - * Persists the current backend state. - * - *

Creates a {@link SystemSnapshot} containing all active lobbies and games and stores it using - * the configured {@link JsonStateStore}. - */ - public void persistCurrentState() { - - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, - lobbyService.getLobbySnapshots(), - gameService.getGameSnapshots()); - - stateStore.save(snapshot); - } -} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java deleted file mode 100644 index 1425196d..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.codenames.codenames.backend.recovery; - -import com.codenames.codenames.backend.lobby.Lobby; -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.GameManager; -import com.codenames.codenames.backend.playingfield.GameManagerFactory; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -/** Restores persisted lobbies and games into in-memory runtime services on backend startup. */ -@Slf4j -@Service -public class SystemStateRecoveryService { - - private final JsonStateStore stateStore; - private final LobbyService lobbyService; - private final GameService gameService; - private final GameManagerFactory gameManagerFactory; - - /** - * Creates a recovery service. - * - * @param stateStore JSON state store used for loading snapshots - * @param lobbyService lobby runtime service - * @param gameService game runtime service - * @param gameManagerFactory factory used to rebuild game managers from snapshots - */ - public SystemStateRecoveryService( - JsonStateStore stateStore, - LobbyService lobbyService, - GameService gameService, - GameManagerFactory gameManagerFactory) { - this.stateStore = stateStore; - this.lobbyService = lobbyService; - this.gameService = gameService; - this.gameManagerFactory = gameManagerFactory; - } - - /** Loads and restores persisted state at startup when a compatible snapshot exists. */ - @jakarta.annotation.PostConstruct - public void recoverOnStartup() { - stateStore - .load() - .ifPresent( - snapshot -> { - if (snapshot.schemaVersion() != SystemSnapshot.CURRENT_SCHEMA_VERSION) { - log.warn( - "Skipping snapshot restore due to schema mismatch. Found {}, expected {}.", - snapshot.schemaVersion(), - SystemSnapshot.CURRENT_SCHEMA_VERSION); - return; - } - restoreLobbies(snapshot.lobbies()); - restoreGames(snapshot.games()); - }); - } - - /** - * Restores all lobby snapshots into {@link LobbyService}. - * - * @param lobbySnapshots persisted lobby player lists keyed by lobby code - */ - private void restoreLobbies(Map> lobbySnapshots) { - if (lobbySnapshots == null || lobbySnapshots.isEmpty()) { - return; - } - for (Map.Entry> entry : lobbySnapshots.entrySet()) { - Lobby restoredLobby = buildLobby(entry.getKey(), entry.getValue()); - if (restoredLobby != null) { - lobbyService.restoreLobby(entry.getKey(), restoredLobby); - } - } - } - - /** - * Restores all game snapshots into {@link GameService}. - * - * @param gameSnapshots persisted game states keyed by lobby code - */ - private void restoreGames(Map gameSnapshots) { - if (gameSnapshots == null || gameSnapshots.isEmpty()) { - return; - } - for (Map.Entry entry : gameSnapshots.entrySet()) { - GameManager restoredGame = gameManagerFactory.createFromSnapshot(entry.getValue()); - gameService.restoreGameManager(entry.getKey(), restoredGame); - } - } - - /** - * Builds a runtime lobby from a persisted lobby snapshot. - * - * @param lobbyCode target lobby code - * @param players persisted lobby players - * @return rebuilt lobby, or {@code null} when player data is invalid - */ - private Lobby buildLobby(String lobbyCode, List players) { - if (players == null || players.isEmpty()) { - log.warn("Skipping restore for lobby {} due to missing player data.", lobbyCode); - return null; - } - - List validPlayers = - players.stream() - .filter(player -> player.username() != null && !player.username().isBlank()) - .sorted(Comparator.comparing(PlayerDto::isHost).reversed()) - .toList(); - - if (validPlayers.isEmpty()) { - return null; - } - - PlayerDto host = validPlayers.get(0); - Lobby lobby = new Lobby(lobbyCode, host.username()); - - for (PlayerDto player : validPlayers) { - if (!player.username().equals(host.username())) { - lobby.addPlayer(player.username(), player.isHost()); - } - if (player.team() != null) { - lobby.setPlayerTeam(player.username(), player.team()); - } - if (player.role() != null) { - lobby.setPlayerRole(player.username(), player.role()); - } - } - - return lobby; - } -} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java deleted file mode 100644 index 3ae69bb4..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.codenames.codenames.backend.recovery.snapshot; - -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import java.util.List; -import java.util.Map; - -/** - * Root snapshot aggregate for persisted backend runtime state. - * - * @param schemaVersion persisted schema version - * @param lobbies lobby player lists keyed by lobby code - * @param games game states keyed by lobby code - */ -public record SystemSnapshot( - int schemaVersion, - Map> lobbies, - Map games) { - - public static final int CURRENT_SCHEMA_VERSION = 2; -} diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java deleted file mode 100644 index f54d3442..00000000 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.codenames.codenames.backend.serialization; - -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.playingfield.Card; -import com.codenames.codenames.backend.playingfield.GameManager; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import java.util.ArrayList; -import java.util.List; -import org.springframework.stereotype.Service; - -/** Service to create the DTO for the game state, which is then serialized into JSON. */ -@Service -public class DataTransferObjectService { - - /** - * Helper method to create a card DTO with the correct visibility based on role and guess state. - * - * @param card card object from the board - * @return the card DTO for the game state DTO - */ - private CardDataTransferObject createCardDataTransferObject(Card card) { - Color displayColor = card.getColor(); - return new CardDataTransferObject(card.getWord(), displayColor, card.isGuessed()); - } - - /** - * Creates the game state DTO that needs to be serialized into JSON. - * - * @param gameManager the game manager that holds the state of the game - * @param currentTurn the current turn - * @return a DTO of the current game state - */ - public GameStateDataTransferObject createGameStateDataTransferObject( - GameManager gameManager, Team currentTurn, Role currentPhase) { - - List cardList = gameManager.getCardList(); - List cardDataTransferObject = new ArrayList<>(); - for (Card card : cardList) { - cardDataTransferObject.add(createCardDataTransferObject(card)); - } - if (gameManager.getCurrentClue() == null) { - return new GameStateDataTransferObject( - gameManager.getWinner(), - currentTurn, - currentPhase, - null, - cardDataTransferObject); - } - String word = gameManager.getCurrentClue().word(); - int guessAmount = gameManager.getCurrentClue().guessAmount(); - return new GameStateDataTransferObject( - gameManager.getWinner(), - currentTurn, - currentPhase, - new ClueDto(word, guessAmount), - cardDataTransferObject); - } -} diff --git a/src/main/java/com/codenames/codenames/backend/serialization/SerializationJson.java b/src/main/java/com/codenames/codenames/backend/serialization/SerializationJson.java deleted file mode 100644 index ba08f680..00000000 --- a/src/main/java/com/codenames/codenames/backend/serialization/SerializationJson.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.codenames.codenames.backend.serialization; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.stereotype.Component; - -/** Service to serialize the game state DTO into JSON, which is then sent to the frontend. */ -@Component -public class SerializationJson { - private final ObjectMapper mapper; - - /** - * Constructor for serialization (mapper passed as argument to enable mocking in tests). - * - * @param mapper the ObjectMapper object that is used to serialize into JSON - */ - public SerializationJson(ObjectMapper mapper) { - this.mapper = mapper; - } - - /** - * Method to serialize the game state DTO into JSON. - * - * @param gameStateDataTransferObject the DTO of the game state - * @return the string JSON containing: winner, points, list of cards and their information - * @throws IllegalStateException if serialization fails - */ - public String serialize(GameStateDataTransferObject gameStateDataTransferObject) { - try { - return mapper.writeValueAsString(gameStateDataTransferObject); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Error while parsing into JSON.", e); - } - } -} diff --git a/src/main/java/com/codenames/codenames/backend/config/JacksonConfig.java b/src/main/java/com/codenames/codenames/backend/shared/config/JacksonConfig.java similarity index 91% rename from src/main/java/com/codenames/codenames/backend/config/JacksonConfig.java rename to src/main/java/com/codenames/codenames/backend/shared/config/JacksonConfig.java index 61227b4f..7ffb1bd5 100644 --- a/src/main/java/com/codenames/codenames/backend/config/JacksonConfig.java +++ b/src/main/java/com/codenames/codenames/backend/shared/config/JacksonConfig.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.config; +package com.codenames.codenames.backend.shared.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/com/codenames/codenames/backend/shared/config/SecurityConfig.java b/src/main/java/com/codenames/codenames/backend/shared/config/SecurityConfig.java new file mode 100644 index 00000000..9cc97b20 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/shared/config/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.codenames.codenames.backend.shared.config; + +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Value("${app.allowed-origins}") + private String[] allowedOrigins; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.ignoringRequestMatchers("/lobby/**", "/ws-fallback", "/ws-fallback/**")) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests( + auth -> + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() + .requestMatchers("/health") + .permitAll() + .requestMatchers("/lobby/**") + .permitAll() + .requestMatchers("/ws-fallback", "/ws-fallback/**") + .permitAll() + .anyRequest() + .denyAll()) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList(allowedOrigins)); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketConfig.java b/src/main/java/com/codenames/codenames/backend/shared/config/WebSocketConfig.java similarity index 92% rename from src/main/java/com/codenames/codenames/backend/websocket/WebSocketConfig.java rename to src/main/java/com/codenames/codenames/backend/shared/config/WebSocketConfig.java index 908f0286..e7c05eff 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketConfig.java +++ b/src/main/java/com/codenames/codenames/backend/shared/config/WebSocketConfig.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.shared.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -42,7 +42,6 @@ public void configureMessageBroker(MessageBrokerRegistry config) { */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/ws").setAllowedOrigins(allowedOrigins).withSockJS(); registry.addEndpoint("/ws-fallback").setAllowedOrigins(allowedOrigins); } } diff --git a/src/main/java/com/codenames/codenames/backend/websocket/SessionRegistry.java b/src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java similarity index 82% rename from src/main/java/com/codenames/codenames/backend/websocket/SessionRegistry.java rename to src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java index 2aee26c6..33715ff7 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/SessionRegistry.java +++ b/src/main/java/com/codenames/codenames/backend/shared/websocket/SessionRegistry.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.shared.websocket; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -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); } /** diff --git a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java b/src/main/java/com/codenames/codenames/backend/shared/websocket/WebSocketEventListener.java similarity index 95% rename from src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java rename to src/main/java/com/codenames/codenames/backend/shared/websocket/WebSocketEventListener.java index c2982111..672d5992 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java +++ b/src/main/java/com/codenames/codenames/backend/shared/websocket/WebSocketEventListener.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.shared.websocket; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 3a63f2d2..71a98f69 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -21,4 +21,4 @@ spring: enabled: true app: - allowed-origins: "http://localhost:8080,http://10.0.2.2:8080" \ No newline at end of file + allowed-origins: ${APP_ALLOWED_ORIGINS:http://localhost:8080,http://10.0.2.2:8080} \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__initial_migration_schema.sql b/src/main/resources/db/migration/V1__initial_migration_schema.sql new file mode 100644 index 00000000..44e624c9 --- /dev/null +++ b/src/main/resources/db/migration/V1__initial_migration_schema.sql @@ -0,0 +1,58 @@ +-- Name --> Type --> Default --> Nullability --> Constraint + +CREATE TABLE lobby +( + lobby_code VARCHAR(5) PRIMARY KEY, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL -- plan is to use this to delete stale lobbies +); + +CREATE TABLE player +( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + lobby_code VARCHAR(5) NOT NULL REFERENCES lobby (lobby_code) ON DELETE CASCADE, + username VARCHAR(20) NOT NULL, + is_host BOOLEAN DEFAULT FALSE NOT NULL, + team VARCHAR(4) NOT NULL, + role VARCHAR(9) NOT NULL, + + CONSTRAINT check_player_team + CHECK (team IN ('RED', 'BLUE')), + CONSTRAINT check_player_role + CHECK (role IN ('OPERATIVE', 'SPYMASTER')), + CONSTRAINT unique_player_per_lobby -- ask team if username in lobby has to be unique or not + UNIQUE (lobby_code, username) +); + + +CREATE TABLE game_state +( + lobby_code VARCHAR(5) PRIMARY KEY REFERENCES lobby (lobby_code) ON DELETE CASCADE, + current_turn VARCHAR(4) NOT NULL, + current_phase VARCHAR(9) NOT NULL, + clue_word VARCHAR(20), + clue_guess_amount INT DEFAULT 0, + remaining_guesses INT DEFAULT 0, + + CONSTRAINT check_game_turn + CHECK (current_turn IN ('RED', 'BLUE')), + CONSTRAINT check_game_phase + CHECK (current_phase IN ('SPYMASTER', 'OPERATIVE')) +); + +-- each lobby will have 25 entries in the card table +CREATE TABLE card +( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + lobby_code VARCHAR(5) NOT NULL REFERENCES lobby (lobby_code) ON DELETE CASCADE, + position INT NOT NULL, + word VARCHAR(11) NOT NULL, + color VARCHAR(8) NOT NULL, + is_guessed BOOLEAN DEFAULT FALSE NOT NULL, + + CONSTRAINT check_card_position + CHECK (position BETWEEN 0 AND 24), + CONSTRAINT check_card_color + CHECK (color IN ('RED', 'BLUE', 'NEUTRAL', 'ASSASSIN')), + CONSTRAINT unique_card_position_per_game + UNIQUE (lobby_code, position) +); \ 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 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; diff --git a/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java b/src/test/java/com/codenames/codenames/backend/chat/api/ChatControllerTest.java similarity index 82% rename from src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java rename to src/test/java/com/codenames/codenames/backend/chat/api/ChatControllerTest.java index 6a18a085..8fa69939 100644 --- a/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/chat/api/ChatControllerTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.chat; +package com.codenames.codenames.backend.chat.api; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; @@ -6,11 +6,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.utility.ChatMessageType; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.codenames.codenames.backend.websocket.SessionRegistry; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; +import com.codenames.codenames.backend.chat.application.ChatService; +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.shared.websocket.SessionRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -53,7 +55,8 @@ void setUp() { void testSendLobbyMessage_valid() { chatController.sendLobbyMessage(lobbyId, chatDto); - ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = + new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); verify(chatService, times(1)).processMessage(lobbyId, "LOBBY", "", verifiedChatDto); } @@ -63,7 +66,8 @@ void testSendTeamMessage_valid() { chatController.sendTeamMessage(lobbyId, redTeam, chatDto); - ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = + new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); verify(chatService, times(1)).processMessage(lobbyId, "TEAM_RED", "/RED", verifiedChatDto); } @@ -83,7 +87,8 @@ void testSendTeamOperativeMessage_valid() { chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto); - ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = + new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); verify(chatService, times(1)) .processMessage(lobbyId, "OPERATIVE_BLUE", "/BLUE/operative", verifiedChatDto); } diff --git a/src/test/java/com/codenames/codenames/backend/chat/ChatServiceTest.java b/src/test/java/com/codenames/codenames/backend/chat/application/ChatServiceTest.java similarity index 93% rename from src/test/java/com/codenames/codenames/backend/chat/ChatServiceTest.java rename to src/test/java/com/codenames/codenames/backend/chat/application/ChatServiceTest.java index 0607b1e3..38edcc88 100644 --- a/src/test/java/com/codenames/codenames/backend/chat/ChatServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/chat/application/ChatServiceTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.chat; +package com.codenames.codenames.backend.chat.application; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -8,8 +8,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import com.codenames.codenames.backend.utility.ChatMessageType; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/codenames/codenames/backend/chat/ChatHistoryTest.java b/src/test/java/com/codenames/codenames/backend/chat/domain/ChatHistoryTest.java similarity index 90% rename from src/test/java/com/codenames/codenames/backend/chat/ChatHistoryTest.java rename to src/test/java/com/codenames/codenames/backend/chat/domain/ChatHistoryTest.java index 3552dff7..e273a1a2 100644 --- a/src/test/java/com/codenames/codenames/backend/chat/ChatHistoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/chat/domain/ChatHistoryTest.java @@ -1,9 +1,10 @@ -package com.codenames.codenames.backend.chat; +package com.codenames.codenames.backend.chat.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import com.codenames.codenames.backend.utility.ChatMessageType; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java b/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java index b0d093c9..e6eb0ce6 100644 --- a/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java @@ -3,6 +3,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.codenames.codenames.backend.shared.config.SecurityConfig; +import org.springframework.context.annotation.Import; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -10,6 +12,7 @@ import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(HealthController.class) +@Import(SecurityConfig.class) class HealthControllerTest { @Autowired private MockMvc mockMvc; 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 new file mode 100644 index 00000000..ca87bb19 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceMappingTest.java @@ -0,0 +1,134 @@ +package com.codenames.codenames.backend.database.persistence; + +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.database.entity.CardEntity; +import com.codenames.codenames.backend.database.entity.GameStateEntity; +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.entity.PlayerEntity; +import com.codenames.codenames.backend.game.application.CardGenerator; +import com.codenames.codenames.backend.game.application.ClueValidationService; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Clue; +import com.codenames.codenames.backend.game.domain.Color; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PersistenceMappingTest { + private PersistenceMapper persistenceMapper; + private String lobbyCode; + private CardGenerator mockCardGenerator; + private ClueValidationService mockClueValidationService; + private GameManager gameManager; + private PlayerDto player1; + private PlayerDto player2; + private List playerDtoList; + private LobbyEntity lobbyEntity; + private final Color redColor = Color.RED; + private final Team redTeam = Team.RED; + private final Role spymaster = Role.SPYMASTER; + private final Role operative = Role.OPERATIVE; + int maxCardAmount = 25; + + @BeforeEach + void setup() { + persistenceMapper = new PersistenceMapper(); + lobbyCode = "ABCDE"; + + mockCardGenerator = mock(CardGenerator.class); + + mockClueValidationService = mock(ClueValidationService.class); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + 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 = + persistenceMapper.mapAggregateParentLobbyEntity(lobbyCode, gameManager, playerDtoList); + } + + // Modified helper methods from GameManager to generate a game with full cards + private void mockCardGeneration(List cardList) { + when(mockCardGenerator.generateCards(anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenReturn(cardList); + } + + private GameManager helperMethodGenerateFullCardList(Color cardColor, Team startingTeam) { + List cardList = new ArrayList<>(); + for (int i = 0; i < maxCardAmount; i++) { + cardList.add(new Card("Test" + i, cardColor)); + } + mockCardGeneration(cardList); + return new GameManager(startingTeam, mockCardGenerator, mockClueValidationService); + } + + @Test + void testMapPlayer() { + List playerEntities = lobbyEntity.getPlayerEntities(); + + PlayerEntity retrievedPlayer1 = playerEntities.get(0); + + assertEquals(lobbyEntity, retrievedPlayer1.getLobbyEntity()); + assertEquals("Test1", retrievedPlayer1.getUsername()); + assertTrue(retrievedPlayer1.getIsHost()); + assertEquals("RED", retrievedPlayer1.getTeam()); + assertEquals("SPYMASTER", retrievedPlayer1.getRole()); + assertEquals("test-uuid-1", retrievedPlayer1.getUuid()); + } + + @Test + void testMapGameState() { + GameStateEntity gameStateEntity = lobbyEntity.getGameStateEntity(); + assertEquals(lobbyEntity, gameStateEntity.getLobbyEntity()); + assertEquals(lobbyCode, gameStateEntity.getLobbyCode()); + assertEquals("RED", gameStateEntity.getCurrentTurn()); + assertEquals("SPYMASTER", gameStateEntity.getCurrentPhase()); + assertNull(gameStateEntity.getClueWord()); + assertEquals(0, gameStateEntity.getClueGuessAmount()); + assertEquals(0, gameStateEntity.getRemainingGuesses()); + } + + @Test + void testMapGameState_notNullClue() { + when(mockClueValidationService.validateWord(any(), any())).thenReturn(true); + gameManager.submitClue(new Clue("TestWord", 1), redTeam); + lobbyEntity = + persistenceMapper.mapAggregateParentLobbyEntity(lobbyCode, gameManager, playerDtoList); + + GameStateEntity gameStateEntity = lobbyEntity.getGameStateEntity(); + assertEquals("TestWord", gameStateEntity.getClueWord()); + assertEquals(2, gameStateEntity.getClueGuessAmount()); + assertEquals(2, gameStateEntity.getRemainingGuesses()); + } + + @Test + void testMapCard() { + GameManager fullCardGameManager = helperMethodGenerateFullCardList(redColor, redTeam); + LobbyEntity fullCardLobbyEntity = + persistenceMapper.mapAggregateParentLobbyEntity( + lobbyCode, fullCardGameManager, playerDtoList); + List cardEntities = fullCardLobbyEntity.getCardEntities(); + + for (int i = 0; i < maxCardAmount; i++) { + CardEntity cardEntity = cardEntities.get(i); + assertEquals(fullCardLobbyEntity, cardEntity.getLobbyEntity()); + assertEquals(i, cardEntity.getPosition()); + assertEquals("Test" + i, cardEntity.getWord()); + assertEquals("RED", cardEntity.getColor()); + assertFalse(cardEntity.getIsGuessed()); + } + } +} 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 new file mode 100644 index 00000000..5cd04897 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/database/persistence/PersistenceServiceTest.java @@ -0,0 +1,174 @@ +package com.codenames.codenames.backend.database.persistence; + +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.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.database.entity.CardEntity; +import com.codenames.codenames.backend.database.entity.GameStateEntity; +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.entity.PlayerEntity; +import com.codenames.codenames.backend.database.repository.LobbyRepository; +import com.codenames.codenames.backend.game.application.CardGenerator; +import com.codenames.codenames.backend.game.application.ClueValidationService; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Color; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.lobby.api.dto.PlayerDto; +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 java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +// (Spring Docs) +// By default, tests annotated with @DataJpaTest are transactional and roll back at the end of each +// test. +// Also, only uses @Entity annotated classes +@DataJpaTest +class PersistenceServiceTest { + private final LobbyRepository lobbyRepository; + private PersistenceService persistenceService; + private LobbyService mockLobbyService; + private GameService mockGameService; + private CardGenerator mockCardGenerator; + private ClueValidationService mockClueValidationService; + private GameManager gameManager; + private List playerDtoList; + private PlayerDto player1; + private PlayerDto player2; + + private final String lobbyCode = "ABCDE"; + private final int maxCardAmount = 25; + private final Color redColor = Color.RED; + private final Team redTeam = Team.RED; + private final Role spymaster = Role.SPYMASTER; + private final Role operative = Role.OPERATIVE; + + // Cant use new to instantiate Interfaces --> use @Autowired + // Spring checks their active beans (@Repository in this case) and upon matching injects it here + @Autowired + public PersistenceServiceTest(LobbyRepository lobbyRepository) { + this.lobbyRepository = lobbyRepository; + } + + @BeforeEach + void setUp() { + mockLobbyService = mock(LobbyService.class); + mockGameService = mock(GameService.class); + PersistenceMapper persistenceMapper = new PersistenceMapper(); + + persistenceService = + new PersistenceService( + mockLobbyService, mockGameService, persistenceMapper, lobbyRepository); + + mockCardGenerator = mock(CardGenerator.class); + mockClueValidationService = mock(ClueValidationService.class); + + gameManager = helperMethodGenerateFullCardList(redColor, redTeam); + + 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); + + // controlling the saveSnapShot internal methods + when(mockGameService.getGameState(lobbyCode)).thenReturn(gameManager); + when(mockLobbyService.getPlayersDto(lobbyCode)).thenReturn(playerDtoList); + } + + private void mockCardGeneration(List cardList) { + when(mockCardGenerator.generateCards(anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenReturn(cardList); + } + + private GameManager helperMethodGenerateFullCardList(Color cardColor, Team startingTeam) { + List cardList = new ArrayList<>(); + for (int i = 0; i < maxCardAmount; i++) { + cardList.add(new Card("Test" + i, cardColor)); + } + mockCardGeneration(cardList); + return new GameManager(startingTeam, mockCardGenerator, mockClueValidationService); + } + + @Test + void testSaveSnapshot_lobbyEntity() { + persistenceService.saveSnapShot(lobbyCode); + + LobbyEntity lobbyEntity = lobbyRepository.findById(lobbyCode).get(); + assertNotNull(lobbyEntity); + assertEquals(lobbyCode, lobbyEntity.getLobbyCode()); + } + + @Test + void testSaveSnapshot_gameStateEntity() { + persistenceService.saveSnapShot(lobbyCode); + + LobbyEntity lobbyEntity = lobbyRepository.findById(lobbyCode).get(); + GameStateEntity gameStateEntity = lobbyEntity.getGameStateEntity(); + + assertNotNull(gameStateEntity); + assertEquals(lobbyCode, gameStateEntity.getLobbyCode()); + assertEquals("RED", gameStateEntity.getCurrentTurn()); + assertEquals("SPYMASTER", gameStateEntity.getCurrentPhase()); + assertNull(gameStateEntity.getClueWord()); + assertEquals(0, gameStateEntity.getClueGuessAmount()); + assertEquals(0, gameStateEntity.getRemainingGuesses()); + } + + @Test + void testSaveSnapshot_player() { + persistenceService.saveSnapShot(lobbyCode); + + LobbyEntity lobbyEntity = lobbyRepository.findById(lobbyCode).get(); + List playerList = lobbyEntity.getPlayerEntities(); + assertEquals(2, playerList.size()); + + PlayerEntity playerEntity = playerList.get(0); + + assertEquals(lobbyCode, playerEntity.getLobbyEntity().getLobbyCode()); + assertEquals("Test1", playerEntity.getUsername()); + assertTrue(playerEntity.getIsHost()); + assertEquals("RED", playerEntity.getTeam()); + assertEquals("SPYMASTER", playerEntity.getRole()); + assertEquals("test-uuid-1", playerEntity.getUuid()); + } + + @Test + void testSaveSnapshot_card() { + persistenceService.saveSnapShot(lobbyCode); + + LobbyEntity lobbyEntity = lobbyRepository.findById(lobbyCode).get(); + List cardList = lobbyEntity.getCardEntities(); + + assertEquals(25, cardList.size()); + assertEquals(lobbyCode, cardList.get(0).getLobbyEntity().getLobbyCode()); + + assertEquals(0, cardList.get(0).getPosition()); + assertEquals("Test0", cardList.get(0).getWord()); + assertEquals("RED", cardList.get(0).getColor()); + assertFalse(cardList.get(0).getIsGuessed()); + + assertEquals(24, cardList.get(24).getPosition()); + assertEquals("Test24", cardList.get(24).getWord()); + assertEquals("RED", cardList.get(24).getColor()); + assertFalse(cardList.get(24).getIsGuessed()); + } + + @Test + void testSaveSnapshot_noUniquenessViolationOnSecondSave() { + persistenceService.saveSnapShot(lobbyCode); + persistenceService.saveSnapShot(lobbyCode); + assertEquals(lobbyCode, lobbyRepository.findById(lobbyCode).get().getLobbyCode()); + } +} 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 new file mode 100644 index 00000000..140b37ee --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationMapperTest.java @@ -0,0 +1,147 @@ +package com.codenames.codenames.backend.database.restoration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.codenames.codenames.backend.database.entity.CardEntity; +import com.codenames.codenames.backend.database.entity.GameStateEntity; +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.entity.PlayerEntity; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.Color; +import com.codenames.codenames.backend.lobby.domain.Lobby; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class RestorationMapperTest { + private RestorationMapper restorationMapper; + private LobbyEntity lobbyEntity; + private final Team redTeam = Team.RED; + private final Color redColor = Color.RED; + private final Role operativeRole = Role.OPERATIVE; + + @BeforeEach + void setUp() { + restorationMapper = new RestorationMapper(); + lobbyEntity = new LobbyEntity(); + lobbyEntity.setLobbyCode("ABCDE"); + } + + @Test + void testMapToLobbyDto() { + PlayerEntity playerEntity = new PlayerEntity(); + playerEntity.setLobbyEntity(lobbyEntity); + playerEntity.setUsername("Player1"); + playerEntity.setIsHost(true); + playerEntity.setTeam("RED"); + playerEntity.setRole("OPERATIVE"); + playerEntity.setUuid("test-uuid-1"); + List playerList = List.of(playerEntity); + lobbyEntity.setPlayerEntities(playerList); + + Lobby lobby = restorationMapper.mapToLobby(lobbyEntity); + + assertNotNull(lobby); + assertEquals("ABCDE", lobby.getLobbyCode()); + assertEquals(1, lobby.getPlayerList().size()); + assertEquals("test-uuid-1", lobby.getPlayerList().get(0).uuid()); + } + + @Test + void testMapToGameDto() { + GameStateEntity gameStateEntity = new GameStateEntity(); + gameStateEntity.setCurrentTurn("RED"); + gameStateEntity.setCurrentPhase("OPERATIVE"); + gameStateEntity.setClueWord("TestClueWord"); + gameStateEntity.setClueGuessAmount(3); + lobbyEntity.setGameStateEntity(gameStateEntity); + + CardEntity card = new CardEntity(); + card.setWord("TestCardWord"); + card.setColor("RED"); + card.setIsGuessed(false); + lobbyEntity.setCardEntities(List.of(card)); + + GameStateDto gameStateDto = restorationMapper.mapToGameStateDto(lobbyEntity); + + assertNotNull(gameStateDto); + assertEquals(redTeam, gameStateDto.currentTurn()); + assertEquals(operativeRole, gameStateDto.currentPhase()); + assertEquals("TestClueWord", gameStateDto.currentClue().word()); + assertEquals(1, gameStateDto.cardList().size()); + assertEquals("TestCardWord", gameStateDto.cardList().get(0).word()); + assertEquals(redColor, gameStateDto.cardList().get(0).color()); + } + + @Test + void testMapToGameDto_null() { + assertThrows( + IllegalStateException.class, () -> restorationMapper.mapToGameStateDto(lobbyEntity)); + } + + @Test + void testMapToGameDto_nullClueWord() { + GameStateEntity gameStateEntity = new GameStateEntity(); + gameStateEntity.setCurrentTurn("RED"); + gameStateEntity.setCurrentPhase("OPERATIVE"); + gameStateEntity.setClueWord(null); + gameStateEntity.setClueGuessAmount(3); + lobbyEntity.setGameStateEntity(gameStateEntity); + + GameStateDto gameStateDto = restorationMapper.mapToGameStateDto(lobbyEntity); + assertNull(gameStateDto.currentClue()); + } + + @Test + void testBuildLobby_nullTeam() { + PlayerEntity playerEntity = new PlayerEntity(); + playerEntity.setLobbyEntity(lobbyEntity); + playerEntity.setUsername("TestPlayer"); + playerEntity.setIsHost(true); + playerEntity.setTeam(redTeam.toString()); + playerEntity.setRole(operativeRole.toString()); + + PlayerEntity playerEntity2 = new PlayerEntity(); + playerEntity2.setLobbyEntity(lobbyEntity); + playerEntity2.setUsername("TestPlayer2"); + playerEntity2.setIsHost(false); + playerEntity2.setTeam(null); + playerEntity2.setRole(operativeRole.toString()); + List playerList = List.of(playerEntity, playerEntity2); + + lobbyEntity.setPlayerEntities(playerList); + + Lobby lobby = restorationMapper.mapToLobby(lobbyEntity); + + assertNull(lobby.getPlayerTeam("TestPlayer2")); + } + + @Test + void testBuildLobby_nullRole() { + PlayerEntity playerEntity = new PlayerEntity(); + playerEntity.setLobbyEntity(lobbyEntity); + playerEntity.setUsername("TestPlayer"); + playerEntity.setIsHost(true); + playerEntity.setTeam(redTeam.toString()); + playerEntity.setRole(operativeRole.toString()); + + PlayerEntity playerEntity2 = new PlayerEntity(); + playerEntity2.setLobbyEntity(lobbyEntity); + playerEntity2.setUsername("TestPlayer2"); + playerEntity2.setIsHost(false); + playerEntity2.setTeam(redTeam.toString()); + playerEntity2.setRole(null); + List playerList = List.of(playerEntity, playerEntity2); + + lobbyEntity.setPlayerEntities(playerList); + + Lobby lobby = restorationMapper.mapToLobby(lobbyEntity); + + assertNull(lobby.getPlayerRole("TestPlayer2")); + } +} diff --git a/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java new file mode 100644 index 00000000..aa347a47 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java @@ -0,0 +1,97 @@ +package com.codenames.codenames.backend.database.restoration; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.database.entity.LobbyEntity; +import com.codenames.codenames.backend.database.repository.LobbyRepository; +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.ClueDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.game.domain.GameManagerFactory; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Lobby; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; + +class RestorationServiceTest { + private RestorationService restorationService; + private RestorationMapper restorationMapper; + private GameManagerFactory gameManagerFactory; + private LobbyService lobbyService; + private LobbyRepository lobbyRepository; + private GameService gameService; + private TransactionStatus transactionStatus; + private PlatformTransactionManager transactionManager; + private GameStateDto gameStateDto; + private List cardDtoList; + private Lobby mockLobby; + private GameManager mockGameManager; + + private static final Team redTeam = Team.RED; + private static final Role spymaster = Role.SPYMASTER; + + @BeforeEach + void setUp() { + restorationMapper = mock(RestorationMapper.class); + lobbyService = mock(LobbyService.class); + lobbyRepository = mock(LobbyRepository.class); + gameManagerFactory = mock(GameManagerFactory.class); + gameService = mock(GameService.class); + transactionManager = mock(PlatformTransactionManager.class); + transactionStatus = mock(TransactionStatus.class); + + + mockLobby = mock(Lobby.class); + mockGameManager = mock(GameManager.class); + + cardDtoList = List.of(new CardDto("TEST", null, false)); + gameStateDto = + new GameStateDto( + redTeam, + redTeam, + spymaster, + new ClueDto("Test", 1), + 2, + cardDtoList, + false, + false); + + when(transactionManager.getTransaction(any())).thenReturn(transactionStatus); + + restorationService = + new RestorationService( + restorationMapper, + lobbyRepository, + lobbyService, + gameService, + gameManagerFactory, + transactionManager); + } + + @Test + void testRestoreOnContainerStart() { + LobbyEntity lobbyEntity = new LobbyEntity(); + lobbyEntity.setLobbyCode("ABCDE"); + when(lobbyRepository.findAll()).thenReturn(List.of(lobbyEntity)); + + when(restorationMapper.mapToGameStateDto(lobbyEntity)).thenReturn(gameStateDto); + when(restorationMapper.mapToLobby(lobbyEntity)).thenReturn(mockLobby); + when(gameManagerFactory.createFromSnapshot(gameStateDto)).thenReturn(mockGameManager); + + restorationService.restoreOnContainerStart(); + + verify(lobbyService, times(1)).restoreLobby("ABCDE", mockLobby); + verify(gameService, times(1)).restoreGameManager("ABCDE", mockGameManager); + } +} diff --git a/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java new file mode 100644 index 00000000..f74d08bf --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java @@ -0,0 +1,164 @@ +package com.codenames.codenames.backend.game.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; +import com.codenames.codenames.backend.database.persistence.PersistenceService; +import com.codenames.codenames.backend.game.api.dto.CheatCardMessage; +import com.codenames.codenames.backend.game.api.dto.ClueMessage; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.api.dto.PassTurnMessage; +import com.codenames.codenames.backend.game.api.dto.RevealCardMessage; +import com.codenames.codenames.backend.game.api.dto.StartGameMessage; +import com.codenames.codenames.backend.game.application.CheatService; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.CheatResult; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +/** Tests websocket gameplay controller interactions. */ +@ExtendWith(MockitoExtension.class) +class GameSocketControllerTest { + + private static final String LOBBY_CODE = "ABCDE"; + + @Mock private GameService gameService; + + @Mock private SimpMessagingTemplate messagingTemplate; + + @Mock private PersistenceService persistenceService; + + @Mock private CheatService cheatService; + + private GameSocketController controller; + + @BeforeEach + void setUp() { + controller = + new GameSocketController( + gameService, + messagingTemplate, + persistenceService, + cheatService); + } + + @Test + void startGameShouldBroadcastStateWithoutPersisting() { + StartGameMessage message = new StartGameMessage(); + message.setLobbyCode(LOBBY_CODE); + + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDto()); + + controller.startGame(message); + + verify(persistenceService, never()).saveSnapShot(LOBBY_CODE); + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); + } + + @Test + void revealCardShouldPersistAndBroadcastState() { + RevealCardMessage message = new RevealCardMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setPosition(0); + message.setCurrentTurn(Team.RED); + + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDto()); + + controller.revealCard(message); + + verify(gameService).flipCard(LOBBY_CODE, 0, Team.RED); + verify(persistenceService).saveSnapShot(LOBBY_CODE); + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); + } + + @Test + void submitClueShouldPersistAndBroadcastState() { + ClueMessage message = new ClueMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setWord("animal"); + message.setGuessAmount(2); + message.setCurrentTurn(Team.RED); + + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDto()); + + controller.submitClue(message); + + verify(gameService).submitClue(anyString(), any(), any()); + verify(persistenceService).saveSnapShot(LOBBY_CODE); + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); + } + + @Test + void passTurnShouldPersistAndBroadcastUpdatedState() { + PassTurnMessage message = new PassTurnMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setCurrentTurn(Team.RED); + + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDto()); + + controller.passTurn(message); + + verify(gameService).passTurn(LOBBY_CODE, Team.RED); + verify(persistenceService).saveSnapShot(LOBBY_CODE); + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); + } + + private GameStateDto createGameStateDto() { + return new GameStateDto(null, Team.RED, null, null, 0, List.of(), false, false); + } + + @Test + void useCheatShouldSendPrivateSystemMessageAndPersistSnapshot() { + CheatCardMessage message = new CheatCardMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setUsername("Max"); + message.setPositions(List.of(0, 1)); + + CheatResult result = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED); + + when(cheatService.useCheat(LOBBY_CODE, "Max", List.of(0, 1))).thenReturn(result); + + controller.useCheat(message); + + verify(cheatService).useCheat(LOBBY_CODE, "Max", List.of(0, 1)); + + verify(messagingTemplate) + .convertAndSend( + "/topic/chat/" + LOBBY_CODE + "/RED/operative", + new ChatDto("System", result.message(), ChatMessageType.SYSTEM)); + + verify(persistenceService).saveSnapShot(LOBBY_CODE); + } + + @Test + void useCheatShouldDoNothingWhenResultIsNull() { + CheatCardMessage message = new CheatCardMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setUsername("Max"); + message.setPositions(List.of(0, 1)); + + when(cheatService.useCheat(LOBBY_CODE, "Max", List.of(0, 1))).thenReturn(null); + + controller.useCheat(message); + + verify(cheatService).useCheat(LOBBY_CODE, "Max", List.of(0, 1)); + verify(messagingTemplate, never()) + .convertAndSendToUser(anyString(), anyString(), any(Object.class)); + verify(persistenceService, never()).saveSnapShot(LOBBY_CODE); + } +} diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java b/src/test/java/com/codenames/codenames/backend/game/application/CardGeneratorTest.java similarity index 92% rename from src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java rename to src/test/java/com/codenames/codenames/backend/game/application/CardGeneratorTest.java index eed02baa..ca651072 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/application/CardGeneratorTest.java @@ -1,9 +1,10 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.application; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Color; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/codenames/codenames/backend/game/application/CheatServiceTest.java b/src/test/java/com/codenames/codenames/backend/game/application/CheatServiceTest.java new file mode 100644 index 00000000..dfb3d865 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/game/application/CheatServiceTest.java @@ -0,0 +1,75 @@ +package com.codenames.codenames.backend.game.application; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.game.domain.CheatResult; +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 java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests the functionality of CheatService. */ +class CheatServiceTest { + + private static final String LOBBY_CODE = "ABCDE"; + private static final String USERNAME = "Max"; + + private GameService gameService; + private LobbyService lobbyService; + private CheatService cheatService; + + @BeforeEach + void setUp() { + gameService = mock(GameService.class); + lobbyService = mock(LobbyService.class); + cheatService = new CheatService(gameService, lobbyService); + } + + @Test + void useCheatShouldReturnResultForOperative() { + List positions = List.of(0, 1); + CheatResult expected = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED); + + when(lobbyService.getPlayerTeam(USERNAME, LOBBY_CODE)).thenReturn(Team.RED); + when(lobbyService.getPlayerRole(USERNAME, LOBBY_CODE)).thenReturn(Role.OPERATIVE); + when(gameService.useCheat(LOBBY_CODE, positions, Team.RED)).thenReturn(expected); + + CheatResult result = cheatService.useCheat(LOBBY_CODE, USERNAME, positions); + + assertEquals(expected, result); + verify(gameService).useCheat(LOBBY_CODE, positions, Team.RED); + } + + @Test + void useCheatShouldReturnNullWhenTeamIsMissing() { + List positions = List.of(0, 1); + + when(lobbyService.getPlayerTeam(USERNAME, LOBBY_CODE)).thenReturn(null); + when(lobbyService.getPlayerRole(USERNAME, LOBBY_CODE)).thenReturn(Role.OPERATIVE); + + CheatResult result = cheatService.useCheat(LOBBY_CODE, USERNAME, positions); + + assertNull(result); + verify(gameService, never()).useCheat(LOBBY_CODE, positions, Team.RED); + } + + @Test + void useCheatShouldReturnNullWhenPlayerIsNotOperative() { + List positions = List.of(0, 1); + + when(lobbyService.getPlayerTeam(USERNAME, LOBBY_CODE)).thenReturn(Team.RED); + when(lobbyService.getPlayerRole(USERNAME, LOBBY_CODE)).thenReturn(Role.SPYMASTER); + + CheatResult result = cheatService.useCheat(LOBBY_CODE, USERNAME, positions); + + assertNull(result); + verify(gameService, never()).useCheat(LOBBY_CODE, positions, Team.RED); + } +} \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/clue/ClueValidationServiceTest.java b/src/test/java/com/codenames/codenames/backend/game/application/ClueValidationServiceTest.java similarity index 89% rename from src/test/java/com/codenames/codenames/backend/clue/ClueValidationServiceTest.java rename to src/test/java/com/codenames/codenames/backend/game/application/ClueValidationServiceTest.java index 972f677f..4a042715 100644 --- a/src/test/java/com/codenames/codenames/backend/clue/ClueValidationServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/application/ClueValidationServiceTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.clue; +package com.codenames.codenames.backend.game.application; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -7,10 +7,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.playingfield.Board; -import com.codenames.codenames.backend.playingfield.Card; -import com.codenames.codenames.backend.playingfield.CardGenerator; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.game.domain.Board; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Color; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java similarity index 68% rename from src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java rename to src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java index c8b01b29..1cfc2611 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.application; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -9,14 +9,17 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; -import com.codenames.codenames.backend.serialization.DataTransferObjectService; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.ClueDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.CheatResult; +import com.codenames.codenames.backend.game.domain.Clue; +import com.codenames.codenames.backend.game.domain.Color; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.game.domain.GameManagerFactory; +import com.codenames.codenames.backend.game.mapping.DataTransferObjectService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -106,20 +109,21 @@ void testRestoreGameManager() { @Test void testGetCurrentGameState() { - GameStateDataTransferObject expected = - new GameStateDataTransferObject( + GameStateDto expected = + new GameStateDto( null, redTeam, Role.SPYMASTER, new ClueDto("ANIMAL", 2), - List.of(new CardDataTransferObject("Dog", Color.RED, false))); + 3, + List.of(new CardDto("Dog", Color.RED, false)), false, false); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); when(mockGameManager.getRemainingGuesses()).thenReturn(2); - when(mockDtoService.createGameStateDataTransferObject(mockGameManager, redTeam, Role.SPYMASTER)) + when(mockDtoService.createGameStateDto(mockGameManager, redTeam, Role.SPYMASTER)) .thenReturn(expected); - GameStateDataTransferObject result = gameService.getCurrentGameState(lobbyCode); + GameStateDto result = gameService.getCurrentGameState(lobbyCode); assertEquals(expected, result); } @@ -136,22 +140,37 @@ void testIsGameStartedWhenGameDoesNotExist() { @Test void getGameSnapshotsShouldReturnAllCurrentGameStates() { - GameStateDataTransferObject expected = - new GameStateDataTransferObject( + GameStateDto expected = + new GameStateDto( null, redTeam, Role.SPYMASTER, new ClueDto("ANIMAL", 2), - List.of(new CardDataTransferObject("Dog", Color.RED, false))); + 3, + List.of(new CardDto("Dog", Color.RED, false)), false, false); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); - when(mockDtoService.createGameStateDataTransferObject(mockGameManager, redTeam, Role.SPYMASTER)) + when(mockDtoService.createGameStateDto(mockGameManager, redTeam, Role.SPYMASTER)) .thenReturn(expected); - Map snapshots = gameService.getGameSnapshots(); + Map snapshots = gameService.getGameSnapshots(); assertEquals(1, snapshots.size()); assertEquals(expected, snapshots.get(lobbyCode)); } -} + + @Test + void testUseCheat() { + List positions = List.of(0, 1); + CheatResult expected = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED); + + when(mockGameManager.useCheat(positions, redTeam)).thenReturn(expected); + + CheatResult result = + gameService.useCheat(lobbyCode, positions, redTeam); + + assertEquals(expected, result); + verify(mockGameManager, times(1)).useCheat(positions, redTeam); + } +} \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java deleted file mode 100644 index 44022629..00000000 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.codenames.codenames.backend.game.controller; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.codenames.codenames.backend.game.dto.ClueMessage; -import com.codenames.codenames.backend.game.dto.PassTurnMessage; -import com.codenames.codenames.backend.game.dto.RevealCardMessage; -import com.codenames.codenames.backend.game.dto.StartGameMessage; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Team; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.messaging.simp.SimpMessagingTemplate; - -/** Tests websocket gameplay controller interactions. */ -@ExtendWith(MockitoExtension.class) -class GameSocketControllerTest { - - private static final String LOBBY_CODE = "ABCDE"; - - @Mock private GameService gameService; - - @Mock private SimpMessagingTemplate messagingTemplate; - - @Mock private SystemStatePersistenceService persistenceService; - - private GameSocketController controller; - - @BeforeEach - void setUp() { - controller = new GameSocketController(gameService, messagingTemplate, persistenceService); - } - - @Test - void startGameShouldBroadcastStateWithoutPersisting() { - StartGameMessage message = new StartGameMessage(); - message.setLobbyCode(LOBBY_CODE); - - when(gameService.getCurrentGameState(LOBBY_CODE)) - .thenReturn(createGameStateDataTransferObject()); - - controller.startGame(message); - - verify(persistenceService, never()).persistCurrentState(); - verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); - } - - @Test - void revealCardShouldPersistAndBroadcastState() { - RevealCardMessage message = new RevealCardMessage(); - message.setLobbyCode(LOBBY_CODE); - message.setPosition(0); - message.setCurrentTurn(Team.RED); - - when(gameService.getCurrentGameState(LOBBY_CODE)) - .thenReturn(createGameStateDataTransferObject()); - - controller.revealCard(message); - - verify(gameService).flipCard(LOBBY_CODE, 0, Team.RED); - verify(persistenceService).persistCurrentState(); - verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); - } - - @Test - void submitClueShouldPersistAndBroadcastState() { - ClueMessage message = new ClueMessage(); - message.setLobbyCode(LOBBY_CODE); - message.setWord("animal"); - message.setGuessAmount(2); - message.setCurrentTurn(Team.RED); - - when(gameService.getCurrentGameState(LOBBY_CODE)) - .thenReturn(createGameStateDataTransferObject()); - - controller.submitClue(message); - - verify(gameService).submitClue(anyString(), any(), any()); - verify(persistenceService).persistCurrentState(); - verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); - } - - @Test - void passTurnShouldPersistAndBroadcastUpdatedState() { - PassTurnMessage message = new PassTurnMessage(); - message.setLobbyCode(LOBBY_CODE); - message.setCurrentTurn(Team.RED); - - when(gameService.getCurrentGameState(LOBBY_CODE)) - .thenReturn(createGameStateDataTransferObject()); - - controller.passTurn(message); - - verify(gameService).passTurn(LOBBY_CODE, Team.RED); - verify(persistenceService).persistCurrentState(); - verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); - } - - private GameStateDataTransferObject createGameStateDataTransferObject() { - return new GameStateDataTransferObject(null, Team.RED, null, null, List.of()); - } -} diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/BoardTest.java similarity index 95% rename from src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java rename to src/test/java/com/codenames/codenames/backend/game/domain/BoardTest.java index 1f6308c1..2ecee413 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/BoardTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -10,7 +10,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.game.application.CardGenerator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/CardTest.java similarity index 91% rename from src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java rename to src/test/java/com/codenames/codenames/backend/game/domain/CardTest.java index 8c9a0b0d..d4eb1d6d 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/CardTest.java @@ -1,11 +1,10 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.codenames.codenames.backend.utility.Color; import org.junit.jupiter.api.Test; /** Unit tests for Card. */ diff --git a/src/test/java/com/codenames/codenames/backend/clue/ClueTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/ClueTest.java similarity index 94% rename from src/test/java/com/codenames/codenames/backend/clue/ClueTest.java rename to src/test/java/com/codenames/codenames/backend/game/domain/ClueTest.java index d761b362..04f51c19 100644 --- a/src/test/java/com/codenames/codenames/backend/clue/ClueTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/ClueTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.clue; +package com.codenames.codenames.backend.game.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java similarity index 64% rename from src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java rename to src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java index 25e72ad7..27a26e89 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -6,13 +6,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; -import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.ClueDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.CardGenerator; +import com.codenames.codenames.backend.game.application.ClueValidationService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,15 +38,14 @@ void testCreate() { @Test void testCreateFromSnapshotWithClue() { - GameStateDataTransferObject snapshot = - new GameStateDataTransferObject( + GameStateDto snapshot = + new GameStateDto( Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), - List.of( - new CardDataTransferObject("Dog", Color.RED, true), - new CardDataTransferObject("Cat", Color.BLUE, false))); + 3, + List.of(new CardDto("Dog", Color.RED, true), new CardDto("Cat", Color.BLUE, false)), false, false); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); @@ -62,13 +61,9 @@ void testCreateFromSnapshotWithClue() { @Test void testCreateFromSnapshotWithoutClue() { - GameStateDataTransferObject snapshot = - new GameStateDataTransferObject( - null, - Team.BLUE, - Role.SPYMASTER, - null, - List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); + GameStateDto snapshot = + new GameStateDto( + null, Team.BLUE, Role.SPYMASTER, null,0, List.of(new CardDto("Tree", Color.BLUE, false)), false, false); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); @@ -80,17 +75,18 @@ void testCreateFromSnapshotWithoutClue() { @Test void testCreateFromSnapshotMapsCardsAndCountsGuessedCardsByColor() { - GameStateDataTransferObject snapshot = - new GameStateDataTransferObject( - null, - Team.RED, - Role.OPERATIVE, - null, - List.of( - new CardDataTransferObject("Dog", Color.RED, true), - new CardDataTransferObject("Cat", Color.RED, false), - new CardDataTransferObject("Tree", Color.BLUE, true), - new CardDataTransferObject("Sun", Color.NEUTRAL, true))); + GameStateDto snapshot = + new GameStateDto( + null, + Team.RED, + Role.OPERATIVE, + null, + 0, + List.of( + new CardDto("Dog", Color.RED, true), + new CardDto("Cat", Color.RED, false), + new CardDto("Tree", Color.BLUE, true), + new CardDto("Sun", Color.NEUTRAL, true)), false, false); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java similarity index 77% rename from src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java rename to src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java index 615386a3..d3face6b 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.playingfield; +package com.codenames.codenames.backend.game.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -12,14 +12,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.game.api.dto.CardDto; +import com.codenames.codenames.backend.game.api.dto.ClueDto; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.CardGenerator; +import com.codenames.codenames.backend.game.application.ClueValidationService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -335,8 +334,8 @@ void testPassTurn_throwsWhenSpymaster() { @Test void recoveryConstructorThrowsWhenCardsAreNull() { - GameStateDataTransferObject state = - new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, null); + GameStateDto state = + new GameStateDto(null, Team.RED, Role.SPYMASTER, null, 0, null, false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -344,8 +343,8 @@ void recoveryConstructorThrowsWhenCardsAreNull() { @Test void recoveryConstructorThrowsWhenCardsAreEmpty() { - GameStateDataTransferObject state = - new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, List.of()); + GameStateDto state = + new GameStateDto(null, Team.RED, Role.SPYMASTER, null, 0, List.of(), false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -353,11 +352,11 @@ void recoveryConstructorThrowsWhenCardsAreEmpty() { @Test void recoveryConstructorThrowsWhenCurrentTurnIsNull() { - List cards = - List.of(new CardDataTransferObject("Dog", Color.RED, false)); + List cards = + List.of(new CardDto("Dog", Color.RED, false)); - GameStateDataTransferObject state = - new GameStateDataTransferObject(null, null, Role.SPYMASTER, null, cards); + GameStateDto state = + new GameStateDto(null, null, Role.SPYMASTER, null, 0, cards, false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -365,11 +364,11 @@ void recoveryConstructorThrowsWhenCurrentTurnIsNull() { @Test void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { - List cards = - List.of(new CardDataTransferObject("Dog", Color.RED, false)); + List cards = + List.of(new CardDto("Dog", Color.RED, false)); - GameStateDataTransferObject state = - new GameStateDataTransferObject(null, Team.RED, null, null, cards); + GameStateDto state = + new GameStateDto(null, Team.RED, null, null, 0, cards, false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -377,14 +376,14 @@ void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { @Test void recoveryConstructorRestoresPersistedState() { - List cards = + List cards = List.of( - new CardDataTransferObject("Dog", Color.RED, true), - new CardDataTransferObject("Cat", Color.BLUE, false)); + new CardDto("Dog", Color.RED, true), + new CardDto("Cat", Color.BLUE, false)); - GameStateDataTransferObject state = - new GameStateDataTransferObject( - Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), cards); + GameStateDto state = + new GameStateDto( + Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 3, cards, false, false); GameManager restored = new GameManager(state, mockClueValidationService); @@ -396,4 +395,73 @@ void recoveryConstructorRestoresPersistedState() { assertEquals(0, restored.getCurrentBlueFound()); assertTrue(restored.getCardList().get(0).isGuessed()); } + + @Test + void useCheatReturnsNullWhenNoCardsSelected() { + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(), redTeam); + + assertNull(result); + } + + @Test + void useCheatReturnsNullWhenNotOperativePhase() { + CheatResult result = gameManager.useCheat(List.of(0), redTeam); + + assertNull(result); + } + + @Test + void useCheatReturnsCorrectCardNameWhenSelectedCardsContainTeamCard() { + mockCardGeneration( + List.of( + new Card("Dog", Color.BLUE), + new Card("Cat", Color.RED), + new Card("Tree", Color.NEUTRAL))); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(0, 1, 2), redTeam); + + assertEquals("Die Karte \"Cat\" ist richtig.", result.message()); + assertTrue(gameManager.isRedTeamCheatUsed()); + } + + @Test + void useCheatReturnsNoCorrectCardMessageWhenNoSelectedCardIsCorrect() { + mockCardGeneration( + List.of( + new Card("Dog", Color.BLUE), + new Card("Tree", Color.NEUTRAL))); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(0, 1), redTeam); + + assertEquals("Keine der ausgewählten Karten ist richtig.", result.message()); + assertTrue(gameManager.isRedTeamCheatUsed()); + } + + @Test + void useCheatCanOnlyBeUsedOncePerTeam() { + mockCardGeneration(List.of(new Card("Dog", Color.RED))); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult firstResult = gameManager.useCheat(List.of(0), redTeam); + CheatResult secondResult = gameManager.useCheat(List.of(0), redTeam); + + assertEquals("Die Karte \"Dog\" ist richtig.", firstResult.message()); + assertNull(secondResult); + } + + @Test + void useCheatReturnsNullWhenOnlyInvalidPositionsAreSelected() { + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(-1, 99), redTeam); + + assertNull(result); + } } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java deleted file mode 100644 index 7c28cc89..00000000 --- a/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.codenames.codenames.backend.lobby; - -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 com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link Lobby}. - * - *

Validates player management and lobby constraints. - */ - -class LobbyTest { - - @Test - 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"))); - } - - @Test - void addPlayerShouldAddPlayer() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.addPlayer("P1"); - - assertEquals(2, lobby.getPlayerList().size()); - assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); - } - - @Test - void addPlayerShouldNotExceedMaxPlayers() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.addPlayer("P1"); - lobby.addPlayer("P2"); - lobby.addPlayer("P3"); - lobby.addPlayer("P4"); - - assertEquals(4, lobby.getPlayerList().size()); - } - - @Test - void removePlayerShouldRemovePlayer() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.addPlayer("P1"); - lobby.removePlayer("P1"); - - assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); - } - - @Test - void removePlayerShouldDoNothingIfPlayerNotExists() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.removePlayer("Ghost"); - - assertEquals(1, lobby.getPlayerList().size()); - } - - @Test - void addPlayerShouldNotAddDuplicatePlayer() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - boolean first = lobby.addPlayer("Max"); - boolean second = lobby.addPlayer("Max"); - - assertTrue(first); - assertFalse(second); - - long count = lobby.getPlayerList().stream().filter(p -> p.username().equals("Max")).count(); - - assertEquals(1, count); - } - - @Test - void hasPlayerShouldReturnTrueIfPlayerExists() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - assertTrue(lobby.hasPlayer("Host")); - } - - @Test - void hasPlayerShouldReturnFalseIfPlayerDoesNotExist() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - assertFalse(lobby.hasPlayer("Ghost")); - } - - @Test - void setPlayerTeamShouldStoreSelectedTeam() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.setPlayerTeam("Host", Team.RED); - - assertEquals(Team.RED, lobby.getPlayerTeam("Host")); - } - - @Test - void setPlayerRoleShouldStoreSelectedRole() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.setPlayerRole("Host", Role.SPYMASTER); - - assertEquals(Role.SPYMASTER, lobby.getPlayerRole("Host")); - } - - @Test - void removePlayerShouldAlsoRemoveStoredTeamAndRole() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - lobby.setPlayerTeam("Host", Team.BLUE); - lobby.setPlayerRole("Host", Role.OPERATIVE); - - lobby.removePlayer("Host"); - - assertNull(lobby.getPlayerTeam("Host")); - assertNull(lobby.getPlayerRole("Host")); - } - - @Test - void startingTeamShouldBeEitherRedOrBlue() { - Lobby lobby = new Lobby("ABCDE", "Host"); - - Team startingTeam = lobby.decideStartingTeam(); - - assertTrue(startingTeam == Team.RED || startingTeam == Team.BLUE); - } -} 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 new file mode 100644 index 00000000..0aae9a10 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbyControllerTest.java @@ -0,0 +1,248 @@ +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; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.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; + +@WebMvcTest(LobbyController.class) +@Import(SecurityConfig.class) +class LobbyControllerTest { + + @Autowired private MockMvc mockMvc; + + @MockBean private LobbyService service; + + @Test + 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") + .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 { + when(service.createLobby("TestUser")).thenReturn(""); + + mockMvc + .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 { + when(service.createLobby("TestUser")).thenReturn(null); + + mockMvc + .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 joinLobbyShouldReturn200WhenSuccess() throws Exception { + 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.")); + } + + @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 + void getLobbyInfoShouldReturn200() throws Exception { + when(service.getPlayersDto("ABCDE")) + .thenReturn(List.of(new PlayerDto("test", null, null, true, null))); + String url = "/lobby/ABCDE"; + mockMvc.perform(get(url)).andExpect(status().isOk()); + } + + @Test + 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.")); + } + + @Test + void leaveLobbyShouldReturn200WhenSuccess() throws Exception { + when(service.getIsStarted("ABCDE")).thenReturn(false); + when(service.leaveLobby(anyString(), anyString())).thenReturn(true); + + mockMvc + .perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser") + .param("uuid", "mock-uuid-123")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + } + + @Test + void leaveLobbyNoSuccess() throws Exception { + when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); + + mockMvc + .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 + 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", + "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 + void selectPositionShouldReturn400WhenAssignmentFails() throws Exception { + when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); + + mockMvc + .perform( + post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "TestUser", + "team": "RED", + "role": "SPYMASTER", + "isHost": "true" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + } + + @Test + void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { + List players = + List.of(new PlayerDto("Alice", null, null, true, null), + new PlayerDto("Bob", null, null, false, null)); + + when(service.getPlayersDto("ABCDE")).thenReturn(players); + + mockMvc + .perform(get("/lobby/ABCDE")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Lobby info retrieved successfully.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username").value("Alice")) + .andExpect(jsonPath("$.playerList[1].username").value("Bob")); + } + + @Test + void testStartGameReturns200WhenConditionIsMet() throws Exception { + List players = + 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(anyString(), anyString())).thenReturn(true); + + mockMvc + .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)); + + when(service.getPlayersDto("ABCDE")).thenReturn(players); + when(service.startGame("ABCDE", "Alice")).thenReturn(false); + + mockMvc + .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/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java similarity index 72% rename from src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java rename to src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java index 815bde5d..1dfdc242 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.lobby.api; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -9,9 +9,12 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.lobby.api.dto.JoinMessage; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Player; +import com.codenames.codenames.backend.shared.websocket.SessionRegistry; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,16 +22,16 @@ import org.springframework.messaging.simp.SimpMessagingTemplate; /** - * Unit tests for {@link GameController}. + * Unit tests for {@link LobbySocketController}. * *

Uses mocks to verify interactions with dependencies. */ -class GameControllerTest { +class LobbySocketControllerTest { private LobbyService lobbyService; private GameService gameService; private SessionRegistry sessionRegistry; - private GameController controller; + private LobbySocketController controller; private SimpMessagingTemplate messagingTemplate; @BeforeEach @@ -38,7 +41,8 @@ void setup() { messagingTemplate = mock(SimpMessagingTemplate.class); sessionRegistry = new SessionRegistry(); - controller = new GameController(lobbyService, gameService, messagingTemplate, sessionRegistry); + controller = + new LobbySocketController(lobbyService, gameService, messagingTemplate, sessionRegistry); } @Test @@ -47,6 +51,7 @@ void shouldRegisterJoinAndRegisterSession() { JoinMessage msg = new JoinMessage(); msg.setName("Max"); msg.setCode("ABCDE"); + msg.setUuid(null); SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(); @@ -55,14 +60,16 @@ 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")); @@ -84,9 +91,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"); @@ -121,8 +127,10 @@ 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); @@ -130,7 +138,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)); } @@ -141,6 +149,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { JoinMessage msg = new JoinMessage(); msg.setName("Max"); msg.setCode("ABCDE"); + msg.setUuid("existing-uuid"); SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(); @@ -148,8 +157,9 @@ 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); @@ -161,7 +171,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } - private GameStateDataTransferObject createGameStatePayload() { - return new GameStateDataTransferObject(null, null, null, null, List.of()); + private GameStateDto createGameStatePayload() { + return new GameStateDto(null, null, null, null, 0, List.of(), false, false); } } diff --git a/src/test/java/com/codenames/codenames/backend/websocket/JoinMessageTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessageTest.java similarity index 87% rename from src/test/java/com/codenames/codenames/backend/websocket/JoinMessageTest.java rename to src/test/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessageTest.java index fe644721..68593cd8 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/JoinMessageTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/dto/JoinMessageTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.lobby.api.dto; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGeneratorTest.java b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyCodeGeneratorTest.java similarity index 90% rename from src/test/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGeneratorTest.java rename to src/test/java/com/codenames/codenames/backend/lobby/application/LobbyCodeGeneratorTest.java index 9b1395d7..62467d33 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGeneratorTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyCodeGeneratorTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.lobby.services; +package com.codenames.codenames.backend.lobby.application; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; 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 new file mode 100644 index 00000000..2652fba8 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/lobby/application/LobbyServiceTest.java @@ -0,0 +1,453 @@ +package com.codenames.codenames.backend.lobby.application; + +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; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.chat.application.ChatService; +import com.codenames.codenames.backend.database.repository.LobbyRepository; +import com.codenames.codenames.backend.game.application.GameService; +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.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class LobbyServiceTest { + + private LobbyService lobbyService; + private LobbyCodeGenerator generator; + private GameService gameService; + private LobbyRepository lobbyRepository; + + @BeforeEach + void setup() { + generator = mock(LobbyCodeGenerator.class); + gameService = mock(GameService.class); + ChatService chatService = mock(ChatService.class); + lobbyRepository = mock(LobbyRepository.class); + + lobbyService = new LobbyService(generator, chatService, gameService, lobbyRepository); + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + } + + @Test + void createLobbyReturnLobbyCode() { + lobbyService.createLobby("Host"); + Player result = lobbyService.joinLobby("TestUser", "ABCDE", null); + + assertNotNull(result); + + List players = lobbyService.getPlayers("ABCDE"); + assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); + } + + @Test + void createLobbyLobbyCodeIsNull() { + when(generator.generateLobbyCode()).thenReturn(null); + String result = lobbyService.createLobby("Host"); + + assertNull(result); + } + + @Test + void createLobbyLobbyCodeIsBlank() { + when(generator.generateLobbyCode()).thenReturn(""); + String result = lobbyService.createLobby("Host"); + + assertNull(result); + } + + @Test + void joinLobbyReturnFalseLobbyNotExists() { + Player result = lobbyService.joinLobby("TestUser", "ABCDE", null); + assertNull(result); + } + + @Test + void leaveLobbyReturnTrueLobbyExists() { + lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost("ABCDE"); + + boolean result = lobbyService.leaveLobby(hostUuid, "ABCDE"); + + assertTrue(result); + + List players = lobbyService.getPlayers("ABCDE"); + + assertFalse(players.stream().anyMatch(p -> p.uuid().equals(hostUuid))); + } + + @Test + void leaveLobbyReturnFalseLobbyNotExists() { + boolean result = lobbyService.leaveLobby("wrong-uuid", "ABCDE"); + assertFalse(result); + } + + @Test + void createLobbyShouldGenerateNewCodeIfDuplicateExists() { + when(generator.generateLobbyCode()).thenReturn("ABCDE").thenReturn("ABCDE").thenReturn("FGHIJ"); + + lobbyService.createLobby("Host1"); + String code2 = lobbyService.createLobby("Host2"); + + assertEquals("FGHIJ", code2); + } + + @Test + void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { + lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost("ABCDE"); + + boolean result = lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.SPYMASTER); + + assertTrue(result); + } + + @Test + void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { + boolean result = lobbyService.selectPosition("wrong-uuid", "XXXXX", Team.RED, Role.SPYMASTER); + + assertFalse(result); + } + + @Test + void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { + lobbyService.createLobby("Host"); + + boolean result = lobbyService.selectPosition("wrong-uuid", "ABCDE", Team.RED, Role.SPYMASTER); + + assertFalse(result); + } + + @Test + void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { + lobbyService.createLobby("Host"); + Player p1 = lobbyService.joinLobby("P1", "ABCDE", null); + String hostUuid = lobbyService.getHost("ABCDE"); + + 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() { + lobbyService.createLobby("Host"); + Player p1 = lobbyService.joinLobby("P1", "ABCDE", null); + String hostUuid = lobbyService.getHost("ABCDE"); + + 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() { + lobbyService.createLobby("Host"); + Player p1 = lobbyService.joinLobby("P1", "ABCDE", null); + String hostUuid = lobbyService.getHost("ABCDE"); + + 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() { + List players = lobbyService.getPlayers("UNKNOWN"); + + assertNotNull(players); + assertTrue(players.isEmpty()); + } + + @Test + void joinLobbyShouldAllowDuplicateUsernamesWithDifferentUuids() { + lobbyService.createLobby("Host"); + + Player first = lobbyService.joinLobby("Max", "ABCDE", null); + Player second = lobbyService.joinLobby("Max", "ABCDE", null); + + assertNotNull(first); + assertNotNull(second); + assertNotEquals(first.uuid(), second.uuid()); + + List players = lobbyService.getPlayers("ABCDE"); + + long count = players.stream().filter(p -> p.username().equals("Max")).count(); + + assertEquals(2, count); + } + + @Test + void selectPositionShouldReturnFalseIfTeamIsNull() { + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + String lobbyCode = lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost(lobbyCode); + + boolean result = lobbyService.selectPosition(hostUuid, lobbyCode, null, Role.OPERATIVE); + + assertFalse(result); + } + + @Test + void selectPositionShouldReturnFalseIfRoleIsNull() { + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + String lobbyCode = lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost(lobbyCode); + + boolean result = lobbyService.selectPosition(hostUuid, lobbyCode, Team.RED, null); + + assertFalse(result); + } + + @Test + void testGetPlayerTeam() { + lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost("ABCDE"); + lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.SPYMASTER); + + assertEquals(Team.RED, lobbyService.getPlayerTeam(hostUuid, "ABCDE")); + } + + @Test + void getPlayerTeam_wrongCode() { + assertNull(lobbyService.getPlayerTeam("wrong-uuid", "invalidCode")); + } + + @Test + void getPlayerTeam_nonExistentPlayer() { + String lobbyCode = lobbyService.createLobby("Host"); + + assertNull(lobbyService.getPlayerTeam("wrong-uuid", lobbyCode)); + } + + @Test + void testGetPlayerRole() { + lobbyService.createLobby("Host"); + String hostUuid = lobbyService.getHost("ABCDE"); + lobbyService.selectPosition(hostUuid, "ABCDE", Team.RED, Role.OPERATIVE); + + assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole(hostUuid, "ABCDE")); + } + + @Test + void getPlayerRole_wrongCode() { + assertNull(lobbyService.getPlayerRole("wrong-uuid", "test")); + } + + @Test + void getPlayerRole_nonExistentPlayer() { + String lobbyCode = lobbyService.createLobby("Host"); + + assertNull(lobbyService.getPlayerRole("wrong-uuid", lobbyCode)); + } + + @Test + void testLobbyIsRemovedWhenItIsEmpty() { + lobbyService.createLobby("Host"); + 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() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("Player1", "ABCDE", null); + lobbyService.checkLobbyStillHasPlayers("ABCDE"); + + assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); + } + + @Test + 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() { + List players = lobbyService.getPlayersDto("UNKNOWN"); + + assertNotNull(players); + assertTrue(players.isEmpty()); + } + + @Test + void getPlayersDtoShouldReturnPlayerDtosWhenLobbyExists() { + lobbyService.createLobby("Host"); + + List result = lobbyService.getPlayersDto("ABCDE"); + + assertNotNull(result); + assertEquals(1, result.size()); + + PlayerDto player = result.get(0); + + assertEquals("Host", player.username()); + assertNull(player.team()); + assertNull(player.role()); + assertTrue(player.isHost()); + } + + @Test + void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { + List result = lobbyService.getPlayersDto("ABCDE"); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void testAddGameManagerForLobby() { + lobbyService.createLobby("User"); + String hostUuid = lobbyService.getHost("ABCDE"); + lobbyService.startGame("ABCDE", hostUuid); + + verify(gameService, times(1)).createGameManager(eq("ABCDE"), any(Team.class)); + } + + @Test + void testGetIsStarted() { + when(gameService.isGameStarted("ABCDE")).thenReturn(true); + + boolean result = lobbyService.getIsStarted("ABCDE"); + + assertTrue(result); + } + + @Test + void testGetIsStartedGameServiceReturnsFalse() { + when(gameService.isGameStarted("ABCDE")).thenReturn(false); + + boolean result = lobbyService.getIsStarted("ABCDE"); + + assertFalse(result); + } + + @Test + 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(); + + String result = lobbyService.getHost("ABCDE"); + + assertEquals(expectedUuid, result); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"ABCDE"}) + void testGetHostReturnsEmptyString(String lobbyCode) { + String result = lobbyService.getHost(lobbyCode); + + assertEquals("", result); + } + + @Test + void testRestoreLobbyAddsLobbyToLobbyList() { + Lobby restoredLobby = new Lobby("ABCDE", "Host"); + + lobbyService.restoreLobby("ABCDE", restoredLobby); + + assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); + assertEquals(restoredLobby, lobbyService.getLobbyList().get("ABCDE")); + } + + @Test + void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("Player", "ABCDE", null); + 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(); + + 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/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java deleted file mode 100644 index 3ae261c6..00000000 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.codenames.codenames.backend.lobby.controller; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -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; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -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.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(LobbyController.class) -class LobbyControllerTest { - - @Autowired private MockMvc mockMvc; - - @MockBean private LobbyService service; - - @MockBean private SystemStatePersistenceService persistenceService; - - @Test - void createLobbyShouldReturn200() throws Exception { - when(service.createLobby("TestUser")).thenReturn("ABCDE"); - - mockMvc - .perform(get("/lobby/create").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - - verifyNoInteractions(persistenceService); - } - - @Test - 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("")); - } - - @Test - 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("")); - } - - @Test - void joinLobbyShouldReturn200WhenSuccess() throws Exception { - when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); - - mockMvc - .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); - - verifyNoInteractions(persistenceService); - } - - @Test - void getLobbyInfoShouldReturn200() throws Exception { - when(service.getPlayersDto("ABCDE")) - .thenReturn(List.of(new PlayerDto("test", null, null, true))); - String url = "/lobby/ABCDE"; - mockMvc.perform(get(url)).andExpect(status().isOk()); - } - - @Test - void joinLobbyShouldReturn400WhenNotFound() throws Exception { - when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); - - mockMvc - .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); - - mockMvc - .perform(get("/lobby/ABCDE/leave").param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Left lobby successfully.")); - - verifyNoInteractions(persistenceService); - } - - @Test - 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.")); - } - - @Test - void selectPositionShouldReturn200whenSuccess() throws Exception { - when(service.selectPosition("TestUser", "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")); - - verifyNoInteractions(persistenceService); - } - - @Test - void selectPositionShouldReturn400WhenAssignmentFails() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); - - mockMvc - .perform( - post("/lobby/ABCDE/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "team": "RED", - "role": "SPYMASTER", - "isHost": "true" - } - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } - - @Test - void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { - List players = - List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); - - when(service.getPlayersDto("ABCDE")).thenReturn(players); - - mockMvc - .perform(get("/lobby/ABCDE")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Lobby info retrieved successfully.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username").value("Alice")) - .andExpect(jsonPath("$.playerList[1].username").value("Bob")); - } - - @Test - void testStartGameReturns200WhenConditionIsMet() throws Exception { - List players = - List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); - - when(service.getPlayersDto("ABCDE")).thenReturn(players); - when(service.startGame("ABCDE", "Alice")).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")); - - verify(persistenceService).persistCurrentState(); - } - - @Test - void testStartGameReturns400WhenServiceReturnsFalse() throws Exception { - List players = - List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); - - 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")); - } -} 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 new file mode 100644 index 00000000..9d097e91 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/LobbyTest.java @@ -0,0 +1,180 @@ +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.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; + +import org.junit.jupiter.api.Test; + +class LobbyTest { + + @Test + 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() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + lobby.addPlayer("P1", false); + + assertEquals(2, lobby.getPlayerList().size()); + assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); + } + + @Test + void addPlayerShouldNotExceedMaxPlayers() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + 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() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + Player p1 = lobby.addPlayer("P1", false); + String p1Uuid = p1.uuid(); + + lobby.removePlayer(p1Uuid); + + assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.uuid().equals(p1Uuid))); + } + + @Test + void removePlayerShouldDoNothingIfPlayerNotExists() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + lobby.removePlayer("invalid-uuid-1234"); + + assertEquals(1, lobby.getPlayerList().size()); + } + + @Test + void addPlayerShouldAllowDuplicateUsernamesWithDifferentUuids() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + Player first = lobby.addPlayer("Max", false); + Player second = lobby.addPlayer("Max", false); + + assertNotNull(first); + assertNotNull(second); + assertNotEquals(first.uuid(), second.uuid()); + assertEquals(3, lobby.getPlayerList().size()); + } + + @Test + void hasPlayerShouldReturnTrueIfPlayerExists() { + Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); + + assertTrue(lobby.hasPlayer(hostUuid)); + } + + @Test + void hasPlayerShouldReturnFalseIfPlayerDoesNotExist() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + assertFalse(lobby.hasPlayer("invalid-uuid")); + } + + @Test + void setPlayerTeamShouldStoreSelectedTeam() { + Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); + + lobby.setPlayerTeam(hostUuid, Team.RED); + + assertEquals(Team.RED, lobby.getPlayerTeam(hostUuid)); + } + + @Test + void setPlayerRoleShouldStoreSelectedRole() { + Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); + + lobby.setPlayerRole(hostUuid, Role.SPYMASTER); + + assertEquals(Role.SPYMASTER, lobby.getPlayerRole(hostUuid)); + } + + @Test + void removePlayerShouldAlsoRemoveStoredTeamAndRole() { + Lobby lobby = new Lobby("ABCDE", "Host"); + String hostUuid = lobby.getPlayerList().get(0).uuid(); + + lobby.setPlayerTeam(hostUuid, Team.BLUE); + lobby.setPlayerRole(hostUuid, Role.OPERATIVE); + + lobby.removePlayer(hostUuid); + + assertNull(lobby.getPlayerTeam(hostUuid)); + assertNull(lobby.getPlayerRole(hostUuid)); + } + + @Test + void startingTeamShouldBeEitherRedOrBlue() { + Lobby lobby = new Lobby("ABCDE", "Host"); + + Team startingTeam = lobby.decideStartingTeam(); + + 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); + } + + @Test + void addRestoredPlayerShouldAddPlayerDirectly() { + Lobby lobby = new Lobby("ABCDE"); + Player restoredPlayer = new Player("RestoredUser", false, "custom-uuid-999"); + + lobby.addRestoredPlayer(restoredPlayer); + + 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 diff --git a/src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java similarity index 69% rename from src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java rename to src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java index 33f1b002..a231174b 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/domain/PlayerTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.lobby.domain; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -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()); } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java deleted file mode 100644 index 3363a43e..00000000 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ /dev/null @@ -1,393 +0,0 @@ -package com.codenames.codenames.backend.lobby.services; - -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.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.codenames.codenames.backend.chat.ChatService; -import com.codenames.codenames.backend.lobby.Lobby; -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.codenames.codenames.backend.websocket.Player; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -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; - private LobbyCodeGenerator generator; - private GameService gameService; - - @BeforeEach - void setup() { - generator = mock(LobbyCodeGenerator.class); - gameService = mock(GameService.class); - ChatService chatService = mock(ChatService.class); - - lobbyService = new LobbyService(generator, chatService, gameService); - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - } - - @Test - void createLobbyReturnLobbyCode() { - lobbyService.createLobby("Host"); - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); - - assertTrue(result); - - List players = lobbyService.getPlayers("ABCDE"); - assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); - } - - @Test - void createLobbyLobbyCodeIsNull() { - when(generator.generateLobbyCode()).thenReturn(null); - String result = lobbyService.createLobby("Host"); - - assertNull(result); - } - - @Test - void createLobbyLobbyCodeIsBlank() { - when(generator.generateLobbyCode()).thenReturn(""); - String result = lobbyService.createLobby("Host"); - - assertNull(result); - } - - @Test - void joinLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); - assertFalse(result); - } - - @Test - void leaveLobbyReturnTrueLobbyExists() { - lobbyService.createLobby("Host"); - - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); - - assertTrue(result); - - List players = lobbyService.getPlayers("ABCDE"); - - assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); - } - - @Test - void leaveLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); - assertFalse(result); - } - - @Test - void createLobbyShouldGenerateNewCodeIfDuplicateExists() { - when(generator.generateLobbyCode()).thenReturn("ABCDE").thenReturn("ABCDE").thenReturn("FGHIJ"); - - lobbyService.createLobby("Host1"); - String code2 = lobbyService.createLobby("Host2"); - - assertEquals("FGHIJ", code2); - } - - @Test - void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { - lobbyService.createLobby("Host"); - - boolean result = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - - assertTrue(result); - } - - @Test - void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { - boolean result = lobbyService.selectPosition("Host", "XXXXX", Team.RED, Role.SPYMASTER); - - assertFalse(result); - } - - @Test - void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { - lobbyService.createLobby("Host"); - - boolean result = lobbyService.selectPosition("Ghost", "ABCDE", Team.RED, Role.SPYMASTER); - - assertFalse(result); - } - - @Test - void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); - - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); - - assertTrue(firstResult); - assertFalse(secondResult); - } - - @Test - void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); - - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); - - assertTrue(firstResult); - assertTrue(secondResult); - } - - @Test - void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); - - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); - - assertTrue(firstResult); - assertTrue(secondResult); - } - - @Test - void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { - List players = lobbyService.getPlayers("UNKNOWN"); - - assertNotNull(players); - assertTrue(players.isEmpty()); - } - - @Test - void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { - lobbyService.createLobby("Host"); - - boolean first = lobbyService.joinLobby("Max", "ABCDE"); - boolean second = lobbyService.joinLobby("Max", "ABCDE"); - - assertTrue(first); - assertFalse(second); - - List players = lobbyService.getPlayers("ABCDE"); - - long count = players.stream().filter(p -> p.username().equals("Max")).count(); - - assertEquals(1, count); - } - - @Test - void selectPositionShouldReturnFalseIfTeamIsNull() { - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - String lobbyCode = lobbyService.createLobby("Host"); - - boolean result = lobbyService.selectPosition("Host", lobbyCode, null, Role.OPERATIVE); - - assertFalse(result); - } - - @Test - void selectPositionShouldReturnFalseIfRoleIsNull() { - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - String lobbyCode = lobbyService.createLobby("Host"); - - boolean result = lobbyService.selectPosition("Host", lobbyCode, Team.RED, null); - - assertFalse(result); - } - - @Test - void testGetPlayerTeam() { - lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - - assertEquals(Team.RED, lobbyService.getPlayerTeam("Host", "ABCDE")); - } - - @Test - void getPlayerTeam_wrongCode() { - assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); - } - - @Test - void getPlayerTeam_nonExistentPlayer() { - String lobbyCode = lobbyService.createLobby("Host"); - - assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); - } - - @Test - void testGetPlayerRole() { - lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - - assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole("Host", "ABCDE")); - } - - @Test - void getPlayerRole_wrongCode() { - assertNull(lobbyService.getPlayerRole("Host", "test")); - } - - @Test - void getPlayerRole_nonExistentPlayer() { - String lobbyCode = lobbyService.createLobby("Host"); - - assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); - } - - @Test - void testLobbyIsRemovedWhenItIsEmpty() { - lobbyService.createLobby("Host"); - lobbyService.leaveLobby("Host", "ABCDE"); - lobbyService.checkLobbyStillHasPlayers("ABCDE"); - assertFalse(lobbyService.getLobbyList().containsKey("ABCDE")); - verify(gameService, times(1)).removeGame("ABCDE"); - } - - @Test - void testLobbyIsNotRemovedWhenItHasPlayers() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("Player1", "ABCDE"); - lobbyService.checkLobbyStillHasPlayers("ABCDE"); - assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); - } - - @Test - 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() { - List players = lobbyService.getPlayersDto("UNKNOWN"); - assertNotNull(players); - assertTrue(players.isEmpty()); - } - - @Test - void getPlayersDtoShouldReturnPlayerDtosWhenLobbyExists() { - lobbyService.createLobby("Host"); - - List result = lobbyService.getPlayersDto("ABCDE"); - - assertNotNull(result); - assertEquals(1, result.size()); - - PlayerDto player = result.get(0); - - assertEquals("Host", player.username()); - assertNull(player.team()); - assertNull(player.role()); - assertTrue(player.isHost()); - } - - @Test - void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { - List result = lobbyService.getPlayersDto("ABCDE"); - - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - @Test - void testAddGameManagerForLobby() { - lobbyService.createLobby("User"); - lobbyService.startGame("ABCDE", "User"); - verify(gameService, times(1)).createGameManager(eq("ABCDE"), any(Team.class)); - } - - @Test - void testGetIsStarted() { - when(gameService.isGameStarted("ABCDE")).thenReturn(true); - - boolean result = lobbyService.getIsStarted("ABCDE"); - assertTrue(result); - } - - @Test - void testGetIsStartedGameServiceReturnsFalse() { - when(gameService.isGameStarted("ABCDE")).thenReturn(false); - - boolean result = lobbyService.getIsStarted("ABCDE"); - assertFalse(result); - } - - @Test - void testGetHostWorks() { - lobbyService.createLobby("Alice"); - lobbyService.joinLobby("Bob", "ABCDE"); - lobbyService.joinLobby("Caesar", "ABCDE"); - - String expected = "Alice"; - String result = lobbyService.getHost("ABCDE"); - - assertEquals(expected, result); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {"ABCDE"}) - void testGetHostReturnsEmptyString(String lobbyCode) { - String result = lobbyService.getHost(lobbyCode); - - assertEquals("", result); - } - - @Test - void testRestoreLobbyAddsLobbyToLobbyList() { - Lobby restoredLobby = new Lobby("ABCDE", "Host"); - - lobbyService.restoreLobby("ABCDE", restoredLobby); - - assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); - assertEquals(restoredLobby, lobbyService.getLobbyList().get("ABCDE")); - } - - @Test - void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("Player", "ABCDE"); - lobbyService.selectPosition("Host", "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(); - - assertEquals(Team.RED, host.team()); - assertEquals(Role.SPYMASTER, host.role()); - assertTrue(host.isHost()); - } -} diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java deleted file mode 100644 index b03acf68..00000000 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.codenames.codenames.backend.recovery; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class JsonStateStoreTest { - - @TempDir Path tempDir; - - @Test - void loadReturnsEmptyWhenStateFileDoesNotExist() { - Path stateFile = tempDir.resolve("state.json"); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - - Optional loadedSnapshot = loadSnapshot(stateStore); - - assertTrue(loadedSnapshot.isEmpty()); - } - - @Test - void loadReturnsEmptyWhenStateFileIsEmpty() throws IOException { - Path stateFile = tempDir.resolve("state.json"); - Files.createFile(stateFile); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - - Optional loadedSnapshot = loadSnapshot(stateStore); - - assertTrue(loadedSnapshot.isEmpty()); - } - - @Test - void saveAndLoadRoundTrip() { - Path stateFile = tempDir.resolve("state.json"); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - - List lobbyPlayers = - List.of( - new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), - new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false)); - - GameStateDataTransferObject gameSnapshot = - new GameStateDataTransferObject( - null, Team.RED, Role.OPERATIVE, new ClueDto("ANIMAL", 2), List.of()); - - SystemSnapshot expectedSnapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, - Map.of("ABCDE", lobbyPlayers), - Map.of("ABCDE", gameSnapshot)); - - stateStore.save(expectedSnapshot); - Optional loadedSnapshot = loadSnapshot(stateStore); - - assertTrue(loadedSnapshot.isPresent()); - assertTrue(Files.exists(stateFile)); - - SystemSnapshot actualSnapshot = loadedSnapshot.get(); - assertEquals(SystemSnapshot.CURRENT_SCHEMA_VERSION, actualSnapshot.schemaVersion()); - assertEquals(1, actualSnapshot.lobbies().size()); - assertEquals(1, actualSnapshot.games().size()); - - List actualPlayers = actualSnapshot.lobbies().get("ABCDE"); - assertEquals(2, actualPlayers.size()); - assertEquals("Host", actualPlayers.get(0).username()); - assertEquals(Team.RED, actualPlayers.get(0).team()); - assertEquals(Role.SPYMASTER, actualPlayers.get(0).role()); - assertTrue(actualPlayers.get(0).isHost()); - - GameStateDataTransferObject actualGameSnapshot = actualSnapshot.games().get("ABCDE"); - assertEquals(Team.RED, actualGameSnapshot.currentTurn()); - assertEquals(Role.OPERATIVE, actualGameSnapshot.currentPhase()); - assertEquals("ANIMAL", actualGameSnapshot.currentClue().word()); - assertEquals(2, actualGameSnapshot.currentClue().guessAmount()); - assertTrue(actualGameSnapshot.cardList().isEmpty()); - } - - @Test - void saveOverwritesPreviousSnapshot() { - Path stateFile = tempDir.resolve("state.json"); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - - SystemSnapshot firstSnapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); - SystemSnapshot secondSnapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, - Map.of("ABCDE", List.of()), - Map.of()); - - stateStore.save(firstSnapshot); - stateStore.save(secondSnapshot); - - Optional loadedSnapshot = loadSnapshot(stateStore); - - assertTrue(loadedSnapshot.isPresent()); - assertTrue(loadedSnapshot.get().lobbies().containsKey("ABCDE")); - assertEquals(1, loadedSnapshot.get().lobbies().size()); - } - - @Test - void saveThrowsIllegalStateWhenStateParentCannotBeCreated() throws IOException { - Path fileAsParent = tempDir.resolve("not-a-directory"); - Files.writeString(fileAsParent, "occupied"); - Path stateFile = fileAsParent.resolve("state.json"); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); - - assertThrows(IllegalStateException.class, () -> stateStore.save(snapshot)); - } - - @Test - void loadThrowsIllegalStateWhenJsonIsInvalid() throws IOException { - Path stateFile = tempDir.resolve("state.json"); - Files.writeString(stateFile, "{invalid-json"); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - - assertThrows(IllegalStateException.class, stateStore::load); - } - - @Test - void gettersExposeConfiguredDependencies() { - ObjectMapper mapper = new ObjectMapper(); - Path stateFile = tempDir.resolve("state.json"); - JsonStateStore stateStore = new JsonStateStore(mapper, stateFile.toString()); - - assertSame(mapper, stateStore.getObjectMapper()); - assertEquals(stateFile, stateStore.getStateFilePath()); - assertNotNull(stateStore.getIoLock()); - } - - @Test - void saveWorksWhenStateFileHasNoParentDirectory() throws IOException { - String fileName = "json-state-store-" + System.nanoTime() + ".json"; - Path stateFile = Path.of(fileName); - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), fileName); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); - - try { - stateStore.save(snapshot); - - assertTrue(Files.exists(stateFile)); - assertTrue(stateStore.load().isPresent()); - } finally { - Files.deleteIfExists(stateFile); - Files.deleteIfExists(Path.of(fileName + ".tmp")); - } - } - - private Optional loadSnapshot(JsonStateStore stateStore) { - return stateStore.load(); - } -} diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java deleted file mode 100644 index c0d956dc..00000000 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.codenames.codenames.backend.recovery; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -class SystemStatePersistenceServiceTest { - - @Test - void persistCurrentStateSavesCurrentSystemSnapshot() { - JsonStateStore stateStore = mock(JsonStateStore.class); - LobbyService lobbyService = mock(LobbyService.class); - GameService gameService = mock(GameService.class); - - Map> lobbySnapshots = Map.of("ABCDE", List.of()); - - Map gameSnapshots = Map.of(); - - when(lobbyService.getLobbySnapshots()).thenReturn(lobbySnapshots); - when(gameService.getGameSnapshots()).thenReturn(gameSnapshots); - - SystemStatePersistenceService persistenceService = - new SystemStatePersistenceService(stateStore, lobbyService, gameService); - - persistenceService.persistCurrentState(); - - ArgumentCaptor snapshotCaptor = ArgumentCaptor.forClass(SystemSnapshot.class); - - verify(stateStore, times(1)).save(snapshotCaptor.capture()); - - SystemSnapshot snapshot = snapshotCaptor.getValue(); - - assertEquals(SystemSnapshot.CURRENT_SCHEMA_VERSION, snapshot.schemaVersion()); - assertEquals(lobbySnapshots, snapshot.lobbies()); - assertEquals(gameSnapshots, snapshot.games()); - } -} diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java deleted file mode 100644 index a0d7528d..00000000 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java +++ /dev/null @@ -1,262 +0,0 @@ -package com.codenames.codenames.backend.recovery; - -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 com.codenames.codenames.backend.chat.ChatService; -import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.lobby.Lobby; -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.lobby.services.LobbyCodeGenerator; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.CardGenerator; -import com.codenames.codenames.backend.playingfield.GameManager; -import com.codenames.codenames.backend.playingfield.GameManagerFactory; -import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; -import com.codenames.codenames.backend.serialization.DataTransferObjectService; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class SystemStateRecoveryServiceTest { - - @TempDir Path tempDir; - - @Test - void recoverOnStartupDoesNothingWhenNoSnapshotExists() { - TestContext context = createContext(tempDir.resolve("state.json")); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - assertFalse(context.gameService().isGameStarted("ABCDE")); - } - - @Test - void recoverOnStartupSkipsSnapshotWhenSchemaVersionDiffers() { - TestContext context = createContext(tempDir.resolve("state.json")); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION + 1, Map.of(), Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - assertFalse(context.gameService().isGameStarted("ABCDE")); - } - - @Test - void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { - TestContext context = createContext(tempDir.resolve("state.json")); - List lobbyPlayers = - List.of( - new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), - new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false)); - GameStateDataTransferObject gameSnapshot = - new GameStateDataTransferObject( - null, - Team.RED, - Role.OPERATIVE, - new ClueDto("ANIMAL", 2), - List.of( - new CardDataTransferObject("Dog", Color.RED, true), - new CardDataTransferObject("Cat", Color.BLUE, false))); - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, - Map.of("ABCDE", lobbyPlayers), - Map.of("ABCDE", gameSnapshot)); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - Lobby restoredLobby = context.lobbyService().getLobbyList().get("ABCDE"); - assertEquals(2, restoredLobby.getPlayerList().size()); - assertEquals(Team.RED, restoredLobby.getPlayerTeam("Host")); - assertEquals(Role.SPYMASTER, restoredLobby.getPlayerRole("Host")); - assertEquals(Team.BLUE, restoredLobby.getPlayerTeam("Player")); - assertEquals(Role.OPERATIVE, restoredLobby.getPlayerRole("Player")); - - assertTrue(context.gameService().isGameStarted("ABCDE")); - GameManager restoredGame = context.gameService().getGameState("ABCDE"); - assertEquals(Team.RED, restoredGame.getCurrentTurn()); - assertEquals(Role.OPERATIVE, restoredGame.getCurrentPhase()); - assertEquals("ANIMAL", restoredGame.getCurrentClue().word()); - assertTrue(restoredGame.getCardList().get(0).isGuessed()); - assertFalse(restoredGame.getCardList().get(1).isGuessed()); - } - - @Test - void recoverOnStartupWithCurrentSchemaAndEmptyMapsDoesNothing() { - TestContext context = createContext(tempDir.resolve("state-empty.json")); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - assertFalse(context.gameService().isGameStarted("ABCDE")); - } - - @Test - void recoverOnStartupHandlesNullLobbyAndGameMaps() { - TestContext context = createContext(tempDir.resolve("state-null-maps.json")); - SystemSnapshot snapshot = new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, null, null); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - assertFalse(context.gameService().isGameStarted("ABCDE")); - } - - @Test - void recoverOnStartupSkipsLobbyWhenSnapshotEntryIsNull() { - TestContext context = createContext(tempDir.resolve("state-null-lobby-entry.json")); - Map> lobbies = new HashMap<>(); - lobbies.put("ABCDE", null); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, lobbies, Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - } - - @Test - void recoverOnStartupSkipsLobbyWhenPlayersListIsNull() { - TestContext context = createContext(tempDir.resolve("state-null-players.json")); - Map> lobbies = new HashMap<>(); - lobbies.put("ABCDE", null); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, lobbies, Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - } - - @Test - void recoverOnStartupSkipsLobbyWhenPlayersListIsEmpty() { - TestContext context = createContext(tempDir.resolve("state-empty-players.json")); - List lobbySnapshot = List.of(); - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - } - - @Test - void recoverOnStartupSkipsLobbyWhenAllUsernamesAreInvalid() { - TestContext context = createContext(tempDir.resolve("state-invalid-usernames.json")); - List lobbySnapshot = - List.of( - new PlayerDto(" ", Team.RED, Role.SPYMASTER, true), - new PlayerDto(null, Team.BLUE, Role.OPERATIVE, false)); - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - } - - @Test - void recoverOnStartupRestoresLobbyWhenSomeTeamOrRoleValuesAreMissing() { - TestContext context = createContext(tempDir.resolve("state-missing-team-role.json")); - List lobbySnapshot = - List.of( - new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), - new PlayerDto("Player", null, null, false)); - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - Lobby restoredLobby = context.lobbyService().getLobbyList().get("ABCDE"); - assertEquals(Team.RED, restoredLobby.getPlayerTeam("Host")); - assertEquals(Role.SPYMASTER, restoredLobby.getPlayerRole("Host")); - assertNull(restoredLobby.getPlayerTeam("Player")); - assertNull(restoredLobby.getPlayerRole("Player")); - } - - @Test - void recoverOnStartupRestoresOnlyGamesWhenLobbiesMapIsNull() { - TestContext context = createContext(tempDir.resolve("state-lobbies-null-games-present.json")); - GameStateDataTransferObject gameSnapshot = - new GameStateDataTransferObject( - null, - Team.BLUE, - Role.SPYMASTER, - null, - List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, null, Map.of("ABCDE", gameSnapshot)); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().isEmpty()); - assertTrue(context.gameService().isGameStarted("ABCDE")); - } - - @Test - void recoverOnStartupRestoresOnlyLobbiesWhenGamesMapIsNull() { - TestContext context = createContext(tempDir.resolve("state-games-null-lobbies-present.json")); - List lobbySnapshot = List.of(new PlayerDto("Host", Team.RED, Role.SPYMASTER, true)); - SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), null); - context.stateStore().save(snapshot); - - context.recoveryService().recoverOnStartup(); - - assertTrue(context.lobbyService().getLobbyList().containsKey("ABCDE")); - assertFalse(context.gameService().isGameStarted("ABCDE")); - } - - private TestContext createContext(Path stateFile) { - JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - GameManagerFactory gameManagerFactory = - new GameManagerFactory( - new CardGenerator("CodenamesWordlist.txt"), new ClueValidationService()); - GameService gameService = new GameService(gameManagerFactory, new DataTransferObjectService()); - LobbyService lobbyService = - new LobbyService(new LobbyCodeGenerator(), new ChatService(null), gameService); - SystemStateRecoveryService recoveryService = - new SystemStateRecoveryService(stateStore, lobbyService, gameService, gameManagerFactory); - - return new TestContext(stateStore, lobbyService, gameService, recoveryService); - } - - private record TestContext( - JsonStateStore stateStore, - LobbyService lobbyService, - GameService gameService, - SystemStateRecoveryService recoveryService) {} -} diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 12050802..13501e73 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -5,12 +5,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.playingfield.Card; -import com.codenames.codenames.backend.playingfield.GameManager; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; +import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.Card; +import com.codenames.codenames.backend.game.domain.Clue; +import com.codenames.codenames.backend.game.domain.Color; +import com.codenames.codenames.backend.game.domain.GameManager; +import com.codenames.codenames.backend.game.mapping.DataTransferObjectService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,7 +22,7 @@ class DataTransferObjectServiceTest { Card cardGuessed; GameManager mockGameManager; DataTransferObjectService service; - GameStateDataTransferObject gameStateDto; + GameStateDto gameStateDto; private static final Team redTeam = Team.RED; private static final Role spymaster = Role.SPYMASTER; private static final Role operative = Role.OPERATIVE; @@ -40,13 +42,13 @@ void setUp() { when(mockGameManager.getRemainingGuesses()).thenReturn(2); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, redTeam, spymaster); + service.createGameStateDto(mockGameManager, redTeam, spymaster); } @Test void testSpymasterVisibility() { gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, redTeam, spymaster); + service.createGameStateDto(mockGameManager, redTeam, spymaster); assertEquals(Color.RED, gameStateDto.cardList().get(0).color()); } @@ -69,20 +71,20 @@ void testGetWinner_exists() { void testGetWinner_null() { when(mockGameManager.getWinner()).thenReturn(null); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, redTeam, operative); + service.createGameStateDto(mockGameManager, redTeam, operative); assertNull(gameStateDto.winner()); } @Test - void testCreateGameStateDataTransferObject() { + void testCreateGameStateDto() { Clue clue = new Clue("word", 1); when(mockGameManager.getCardList()).thenReturn(List.of()); when(mockGameManager.getWinner()).thenReturn(null); when(mockGameManager.getCurrentClue()).thenReturn(clue); when(mockGameManager.getRemainingGuesses()).thenReturn(3); - GameStateDataTransferObject dto = - service.createGameStateDataTransferObject(mockGameManager, redTeam, operative); + GameStateDto dto = + service.createGameStateDto(mockGameManager, redTeam, operative); assertEquals(clue.word(), dto.currentClue().word()); assertEquals(clue.guessAmount(), dto.currentClue().guessAmount()); diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java deleted file mode 100644 index e2b109c1..00000000 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.codenames.codenames.backend.serialization; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.playingfield.Card; -import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class SerializationJsonTest { - - SerializationJson serializer; - Card card; - List dummyList; - GameStateDataTransferObject dummyGameState; - ObjectMapper mapper = new ObjectMapper(); - private static final Team redTeam = Team.RED; - private static final Role spymaster = Role.SPYMASTER; - - @BeforeEach - void setUp() { - card = new Card("TEST", Color.RED); - serializer = new SerializationJson(mapper); - - dummyList = List.of(new CardDataTransferObject("TEST", null, false)); - dummyGameState = - new GameStateDataTransferObject( - redTeam, redTeam, spymaster, new ClueDto("Test", 1), dummyList); - } - - @Test - void testSerialize_pass() { - String expectedResult = - """ - {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}"""; - String result = serializer.serialize(dummyGameState); - assertEquals(expectedResult, result); - } - - // writeValueAsString throws JsonProcessingException, - // I either need try catch block or have my method throw the exception. - @Test - void testSerialize_exception() throws JsonProcessingException { - ObjectMapper mockMapper = mock(ObjectMapper.class); - SerializationJson exceptionSerializer = new SerializationJson(mockMapper); - var exception = new MockJsonProcessingException("Because of protected constructors"); - when(mockMapper.writeValueAsString(any())).thenThrow(exception); - assertThrows(IllegalStateException.class, () -> exceptionSerializer.serialize(null)); - } - - // Testing exception for writeValueAsString requires passing objectMapper in a constructor. - // We then create this custom exception and throw it. (Solution found on stack overflow) - static class MockJsonProcessingException extends JsonProcessingException { - public MockJsonProcessingException(String message) { - super(message); - } - } -} diff --git a/src/test/java/com/codenames/codenames/backend/shared/config/SecurityConfigTest.java b/src/test/java/com/codenames/codenames/backend/shared/config/SecurityConfigTest.java new file mode 100644 index 00000000..e4a86f08 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/shared/config/SecurityConfigTest.java @@ -0,0 +1,70 @@ +package com.codenames.codenames.backend.shared.config; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RestController; + +@WebMvcTest(controllers = SecurityConfigTest.EmptyController.class) +@Import(SecurityConfig.class) +@TestPropertySource(properties = "app.allowed-origins=http://localhost:8080,http://10.0.2.2:8080") +class SecurityConfigTest { + + @Autowired private MockMvc mockMvc; + + @Test + void healthEndpointShouldBePublic() throws Exception { + mockMvc.perform(get("/health")).andExpect(status().isNotFound()); + } + + @Test + void lobbyEndpointShouldBePublic() throws Exception { + mockMvc.perform(get("/lobby/test")).andExpect(status().isNotFound()); + } + + @Test + void lobbyPostShouldNotRequireCsrfToken() throws Exception { + mockMvc + .perform(post("/lobby/test").contentType(MediaType.APPLICATION_JSON).content("{}")) + .andExpect(status().isNotFound()); + } + + @Test + void sockJsEndpointShouldBeDenied() throws Exception { + mockMvc.perform(get("/ws")).andExpect(status().isForbidden()); + } + + @Test + void websocketFallbackEndpointShouldBePublic() throws Exception { + mockMvc.perform(get("/ws-fallback")).andExpect(status().isNotFound()); + } + + @Test + void corsPreflightShouldAllowConfiguredOrigin() throws Exception { + mockMvc + .perform( + options("/lobby/test") + .header("Origin", "http://localhost:8080") + .header("Access-Control-Request-Method", "GET")) + .andExpect(status().isOk()) + .andExpect(header().string("Access-Control-Allow-Origin", "http://localhost:8080")); + } + + @Test + void unknownEndpointShouldBeDenied() throws Exception { + mockMvc.perform(get("/internal")).andExpect(status().isForbidden()); + } + + @RestController + static class EmptyController {} +} diff --git a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketConfigTest.java b/src/test/java/com/codenames/codenames/backend/shared/config/WebSocketConfigTest.java similarity index 73% rename from src/test/java/com/codenames/codenames/backend/websocket/WebSocketConfigTest.java rename to src/test/java/com/codenames/codenames/backend/shared/config/WebSocketConfigTest.java index 3d095c72..461d96bf 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketConfigTest.java +++ b/src/test/java/com/codenames/codenames/backend/shared/config/WebSocketConfigTest.java @@ -1,17 +1,17 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.shared.config; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; /** Unit tests for {@link WebSocketConfig}. */ class WebSocketConfigTest { + @Test void shouldConfigureMessageBroker() { WebSocketConfig config = new WebSocketConfig(); @@ -25,7 +25,7 @@ void shouldConfigureMessageBroker() { } @Test - void shouldRegisterStompEndpoints() { + void shouldRegisterRawWebSocketEndpoint() { WebSocketConfig config = new WebSocketConfig(); StompEndpointRegistry registry = mock(StompEndpointRegistry.class); @@ -35,25 +35,16 @@ void shouldRegisterStompEndpoints() { org.springframework.web.socket.config.annotation.StompWebSocketEndpointRegistration .class); - when(registry.addEndpoint("/ws")).thenReturn(endpointRegistration); - when(registry.addEndpoint("/ws-fallback")).thenReturn(endpointRegistration); - - var sockJsRegistration = - mock(org.springframework.web.socket.config.annotation.SockJsServiceRegistration.class); - String[] origins = new String[] {"http://localhost:8080", "http://10.0.2.2:8080"}; ReflectionTestUtils.setField(config, "allowedOrigins", origins); + when(registry.addEndpoint("/ws-fallback")).thenReturn(endpointRegistration); when(endpointRegistration.setAllowedOrigins(origins)).thenReturn(endpointRegistration); - when(endpointRegistration.withSockJS()).thenReturn(sockJsRegistration); - config.registerStompEndpoints(registry); - verify(registry).addEndpoint("/ws"); - verify(endpointRegistration, Mockito.times(2)).setAllowedOrigins(origins); - verify(endpointRegistration).withSockJS(); verify(registry).addEndpoint("/ws-fallback"); + verify(endpointRegistration).setAllowedOrigins(origins); } } diff --git a/src/test/java/com/codenames/codenames/backend/websocket/SessionRegistryTest.java b/src/test/java/com/codenames/codenames/backend/shared/websocket/SessionRegistryTest.java similarity index 92% rename from src/test/java/com/codenames/codenames/backend/websocket/SessionRegistryTest.java rename to src/test/java/com/codenames/codenames/backend/shared/websocket/SessionRegistryTest.java index f2abf949..2e6dc268 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/SessionRegistryTest.java +++ b/src/test/java/com/codenames/codenames/backend/shared/websocket/SessionRegistryTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.shared.websocket; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; diff --git a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java b/src/test/java/com/codenames/codenames/backend/shared/websocket/WebSocketEventListenerTest.java similarity index 98% rename from src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java rename to src/test/java/com/codenames/codenames/backend/shared/websocket/WebSocketEventListenerTest.java index 88577291..6dc1e250 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java +++ b/src/test/java/com/codenames/codenames/backend/shared/websocket/WebSocketEventListenerTest.java @@ -1,4 +1,4 @@ -package com.codenames.codenames.backend.websocket; +package com.codenames.codenames.backend.shared.websocket; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull;