diff options
author | Joffrey Bion <joffrey.bion@amadeus.com> | 2018-07-05 10:38:14 +0200 |
---|---|---|
committer | Joffrey BION <joffrey.bion@gmail.com> | 2018-07-14 03:06:23 +0200 |
commit | 2c28e6e21137ec6bb1ae6ca34860ad920c289426 (patch) | |
tree | 67ba2f866e2b9d5188b26b0d3f9924b110587671 /backend | |
parent | Rework visibilities (diff) | |
download | seven-wonders-2c28e6e21137ec6bb1ae6ca34860ad920c289426.tar.gz seven-wonders-2c28e6e21137ec6bb1ae6ca34860ad920c289426.tar.bz2 seven-wonders-2c28e6e21137ec6bb1ae6ca34860ad920c289426.zip |
Kotlin migration: Spring server
Diffstat (limited to 'backend')
108 files changed, 2469 insertions, 3433 deletions
diff --git a/backend/build.gradle b/backend/build.gradle index aec1bc9e..6c733559 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,10 +1,21 @@ -plugins { - id 'org.springframework.boot' version '2.0.1.RELEASE' +buildscript { + ext { + kotlinVersion = '1.2.51' + springBootVersion = '2.0.3.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") + classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}") + } } -apply plugin: 'java' -apply plugin: 'idea' apply plugin: 'checkstyle' +apply plugin: 'kotlin' +apply plugin: 'kotlin-spring' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' @@ -13,6 +24,20 @@ version '1.0-SNAPSHOT' sourceCompatibility = 1.8 +compileKotlin { + kotlinOptions { + freeCompilerArgs = ["-Xjsr305=strict"] + jvmTarget = "1.8" + } +} + +compileTestKotlin { + kotlinOptions { + freeCompilerArgs = ["-Xjsr305=strict"] + jvmTarget = "1.8" + } +} + repositories { mavenCentral() } @@ -23,12 +48,16 @@ configurations { dependencies { compile project(':game-engine') + compile 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + compile 'org.jetbrains.kotlin:kotlin-reflect' // required by Spring 5 compile 'org.springframework.boot:spring-boot-starter-websocket' compile 'org.springframework.boot:spring-boot-starter-security' - // required by spring security with websockets + // required by spring security when using websockets compile 'org.springframework.security:spring-security-messaging' + compile "com.fasterxml.jackson.module:jackson-module-kotlin" + compile 'ch.qos.logback:logback-classic:1.1.8' compile 'org.hildan.livedoc:livedoc-springboot:4.3.2' compile 'org.hildan.livedoc:livedoc-ui-webjar:4.3.2' @@ -38,7 +67,7 @@ dependencies { testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile 'org.hildan.jackstomp:jackstomp:1.1.0' - checkstyleConfig "org.hildan.checkstyle:checkstyle-config:2.1.0" + checkstyleConfig 'org.hildan.checkstyle:checkstyle-config:2.1.0' } checkstyle { diff --git a/backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java b/backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java deleted file mode 100644 index c656b2c3..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.luxons.sevenwonders; - -import org.hildan.livedoc.spring.boot.starter.EnableJSONDoc; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@EnableJSONDoc -public class SevenWonders { - - public static void main(String[] args) { - SpringApplication.run(SevenWonders.class, args); - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java deleted file mode 100644 index c404cd74..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.luxons.sevenwonders.actions; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -import org.hildan.livedoc.core.annotations.types.ApiType; -import org.hildan.livedoc.core.annotations.types.ApiTypeProperty; - -/** - * The action to choose the player's name. This is the first action that should be called. - */ -@ApiType(group = "Actions") -public class ChooseNameAction { - - @NotNull - @Size(min = 2, max = 20) - private String playerName; - - /** - * @return The display name of the player. May contain spaces and special characters. - */ - @ApiTypeProperty(required = true) - public String getPlayerName() { - return playerName; - } - - public void setPlayerName(String playerName) { - this.playerName = playerName; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java deleted file mode 100644 index 771a5a9c..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.luxons.sevenwonders.actions; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -import org.hildan.livedoc.core.annotations.types.ApiType; - -/** - * The action to create a game. - */ -@ApiType(group = "Actions") -public class CreateGameAction { - - @NotNull - @Size(min = 2, max = 30) - private String gameName; - - /** - * @return The name of the game to create - */ - public String getGameName() { - return gameName; - } - - public void setGameName(String gameName) { - this.gameName = gameName; - } -} - - diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java deleted file mode 100644 index 2675dc71..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.luxons.sevenwonders.actions; - -import javax.validation.constraints.NotNull; - -import org.hildan.livedoc.core.annotations.types.ApiType; -import org.luxons.sevenwonders.doc.Documentation; - -/** - * The action to join a game. - */ -@ApiType(group = Documentation.GROUP_ACTIONS) -public class JoinGameAction { - - @NotNull - private Long gameId; - - /** - * @return The ID of the game to join - */ - public Long getGameId() { - return gameId; - } - - public void setGameId(Long gameId) { - this.gameId = gameId; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/PrepareMoveAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/PrepareMoveAction.java deleted file mode 100644 index e3622b1b..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/actions/PrepareMoveAction.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.luxons.sevenwonders.actions; - -import javax.validation.constraints.NotNull; - -import org.hildan.livedoc.core.annotations.types.ApiType; -import org.luxons.sevenwonders.doc.Documentation; -import org.luxons.sevenwonders.game.api.PlayerMove; - -/** - * The action to prepare the next move during a game. - */ -@ApiType(group = Documentation.GROUP_ACTIONS) -public class PrepareMoveAction { - - @NotNull - private PlayerMove move; - - /** - * @return the move to prepare - */ - public PlayerMove getMove() { - return move; - } - - public void setMove(PlayerMove move) { - this.move = move; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java deleted file mode 100644 index ce272340..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.luxons.sevenwonders.actions; - -import java.util.List; -import javax.validation.constraints.NotNull; - -import org.hildan.livedoc.core.annotations.types.ApiType; -import org.luxons.sevenwonders.doc.Documentation; - -/** - * The action to update the order of the players around the table. Can only be called in the lobby by the owner of the - * game. - */ -@ApiType(group = Documentation.GROUP_ACTIONS) -public class ReorderPlayersAction { - - @NotNull - private List<String> orderedPlayers; - - /** - * @return the list of usernames of the players, in the new order - */ - public List<String> getOrderedPlayers() { - return orderedPlayers; - } - - public void setOrderedPlayers(List<String> orderedPlayers) { - this.orderedPlayers = orderedPlayers; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java deleted file mode 100644 index 7c48dee1..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.luxons.sevenwonders.actions; - -import javax.validation.constraints.NotNull; - -import org.hildan.livedoc.core.annotations.types.ApiType; -import org.luxons.sevenwonders.doc.Documentation; -import org.luxons.sevenwonders.game.api.CustomizableSettings; - -/** - * The action to update the settings of the game. Can only be called in the lobby by the owner of the game. - */ -@ApiType(group = Documentation.GROUP_ACTIONS) -public class UpdateSettingsAction { - - @NotNull - private CustomizableSettings settings; - - /** - * @return the new values for the settings - */ - public CustomizableSettings getSettings() { - return settings; - } - - public void setSettings(CustomizableSettings settings) { - this.settings = settings; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java b/backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java deleted file mode 100644 index bebbd477..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.luxons.sevenwonders.config; - -import java.security.Principal; -import java.util.Map; - -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.server.support.DefaultHandshakeHandler; - -class AnonymousUsersHandshakeHandler extends DefaultHandshakeHandler { - - private int playerId = 0; - - @Override - public Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, - Map<String, Object> attributes) { - Principal p = super.determineUser(request, wsHandler, attributes); - if (p == null) { - p = new UsernamePasswordAuthenticationToken("player" + playerId++, null); - } - return p; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java b/backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java deleted file mode 100644 index 855b7800..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.luxons.sevenwonders.config; - -import org.luxons.sevenwonders.validation.DestinationAccessValidator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.simp.stomp.StompCommand; -import org.springframework.messaging.simp.stomp.StompHeaderAccessor; -import org.springframework.messaging.support.ChannelInterceptorAdapter; -import org.springframework.stereotype.Component; - -@Component -public class TopicSubscriptionInterceptor extends ChannelInterceptorAdapter { - - private static final Logger logger = LoggerFactory.getLogger(TopicSubscriptionInterceptor.class); - - private final DestinationAccessValidator destinationAccessValidator; - - @Autowired - public TopicSubscriptionInterceptor(DestinationAccessValidator destinationAccessValidator) { - this.destinationAccessValidator = destinationAccessValidator; - } - - @Override - public Message<?> preSend(Message<?> message, MessageChannel channel) { - StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message); - if (StompCommand.SUBSCRIBE.equals(headerAccessor.getCommand())) { - String username = headerAccessor.getUser().getName(); - String destination = headerAccessor.getDestination(); - if (!destinationAccessValidator.hasAccess(username, destination)) { - sendForbiddenSubscriptionError(username, destination); - return null; - } - } - return message; - } - - private void sendForbiddenSubscriptionError(String username, String destination) { - logger.error(String.format("Player '%s' is not allowed to access %s", username, destination)); - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/WebSecurityConfig.java b/backend/src/main/java/org/luxons/sevenwonders/config/WebSecurityConfig.java deleted file mode 100644 index 2cfb966d..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/config/WebSecurityConfig.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.luxons.sevenwonders.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; - -@Configuration -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity httpSecurity) { - // this disables default authentication settings - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java b/backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java deleted file mode 100644 index b8018a86..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.luxons.sevenwonders.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.simp.config.ChannelRegistration; -import org.springframework.messaging.simp.config.MessageBrokerRegistry; -import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; -import org.springframework.web.socket.config.annotation.StompEndpointRegistry; -import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; -import org.springframework.web.socket.server.support.DefaultHandshakeHandler; - -@Configuration -@EnableWebSocketMessageBroker -public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - - private final TopicSubscriptionInterceptor topicSubscriptionInterceptor; - - @Autowired - public WebSocketConfig(TopicSubscriptionInterceptor topicSubscriptionInterceptor) { - this.topicSubscriptionInterceptor = topicSubscriptionInterceptor; - } - - @Override - public void configureMessageBroker(MessageBrokerRegistry config) { - // prefixes for all subscriptions - config.enableSimpleBroker("/queue", "/topic"); - config.setUserDestinationPrefix("/user"); - - // /app for normal calls, /topic for subscription events - config.setApplicationDestinationPrefixes("/app", "/topic"); - } - - @Override - public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/seven-wonders-websocket") - .setHandshakeHandler(handshakeHandler()) - .setAllowedOrigins("http://localhost:3000") // to allow frontend server proxy requests in dev mode - .withSockJS(); - } - - @Bean - public DefaultHandshakeHandler handshakeHandler() { - return new AnonymousUsersHandshakeHandler(); - } - - @Override - public void configureClientInboundChannel(ChannelRegistration registration) { - registration.interceptors(topicSubscriptionInterceptor); - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/controllers/GameBrowserController.java b/backend/src/main/java/org/luxons/sevenwonders/controllers/GameBrowserController.java deleted file mode 100644 index 9bb96559..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/controllers/GameBrowserController.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; -import java.util.Collection; -import java.util.Collections; - -import org.hildan.livedoc.core.annotations.Api; -import org.luxons.sevenwonders.actions.CreateGameAction; -import org.luxons.sevenwonders.actions.JoinGameAction; -import org.luxons.sevenwonders.errors.ApiMisuseException; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.LobbyRepository; -import org.luxons.sevenwonders.repositories.PlayerRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.messaging.simp.annotation.SendToUser; -import org.springframework.messaging.simp.annotation.SubscribeMapping; -import org.springframework.stereotype.Controller; -import org.springframework.validation.annotation.Validated; - -/** - * This is the place where the player looks for a game. - */ -@Api(name = "GameBrowser") -@Controller -public class GameBrowserController { - - private static final Logger logger = LoggerFactory.getLogger(GameBrowserController.class); - - private final LobbyController lobbyController; - - private final LobbyRepository lobbyRepository; - - private final PlayerRepository playerRepository; - - private final SimpMessagingTemplate template; - - @Autowired - public GameBrowserController(LobbyController lobbyController, LobbyRepository lobbyRepository, - PlayerRepository playerRepository, SimpMessagingTemplate template) { - this.lobbyController = lobbyController; - this.lobbyRepository = lobbyRepository; - this.playerRepository = playerRepository; - this.template = template; - } - - /** - * Gets the created or updated games. The list of existing games is received on this topic at once upon - * subscription, and then each time the list changes. - * - * @param principal - * the connected user's information - * - * @return the current list of {@link Lobby}s - */ - @SubscribeMapping("/games") // prefix /topic not shown - public Collection<Lobby> listGames(Principal principal) { - logger.info("Player '{}' subscribed to /topic/games", principal.getName()); - return lobbyRepository.list(); - } - - /** - * Creates a new {@link Lobby}. - * - * @param action - * the action to create the game - * @param principal - * the connected user's information - * - * @return the newly created {@link Lobby} - */ - @MessageMapping("/lobby/create") - @SendToUser("/queue/lobby/joined") - public Lobby createGame(@Validated CreateGameAction action, Principal principal) { - checkThatUserIsNotInAGame(principal, "cannot create another game"); - - Player gameOwner = playerRepository.find(principal.getName()); - Lobby lobby = lobbyRepository.create(action.getGameName(), gameOwner); - - logger.info("Game '{}' ({}) created by {} ({})", lobby.getName(), lobby.getId(), gameOwner.getDisplayName(), - gameOwner.getUsername()); - - // notify everyone that a new game exists - template.convertAndSend("/topic/games", Collections.singletonList(lobby)); - return lobby; - } - - /** - * Joins an existing {@link Lobby}. - * - * @param action - * the action to join the game - * @param principal - * the connected user's information - * - * @return the {@link Lobby} that has just been joined - */ - @MessageMapping("/lobby/join") - @SendToUser("/queue/lobby/joined") - public Lobby joinGame(@Validated JoinGameAction action, Principal principal) { - checkThatUserIsNotInAGame(principal, "cannot join another game"); - - Lobby lobby = lobbyRepository.find(action.getGameId()); - Player newPlayer = playerRepository.find(principal.getName()); - lobby.addPlayer(newPlayer); - - logger.info("Player '{}' ({}) joined game {}", newPlayer.getDisplayName(), newPlayer.getUsername(), - lobby.getName()); - lobbyController.sendLobbyUpdateToPlayers(lobby); - return lobby; - } - - private void checkThatUserIsNotInAGame(Principal principal, String impossibleActionDescription) { - Lobby lobby = playerRepository.find(principal.getName()).getLobby(); - if (lobby != null) { - throw new UserAlreadyInGameException(lobby.getName(), impossibleActionDescription); - } - } - - static class UserAlreadyInGameException extends ApiMisuseException { - UserAlreadyInGameException(String gameName, String impossibleActionDescription) { - super("Client already in game '" + gameName + "', " + impossibleActionDescription); - } - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java b/backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java deleted file mode 100644 index f8d60ece..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; -import java.util.List; - -import org.hildan.livedoc.core.annotations.Api; -import org.luxons.sevenwonders.actions.PrepareMoveAction; -import org.luxons.sevenwonders.game.Game; -import org.luxons.sevenwonders.game.api.PlayerTurnInfo; -import org.luxons.sevenwonders.game.api.Table; -import org.luxons.sevenwonders.game.cards.CardBack; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.output.PreparedCard; -import org.luxons.sevenwonders.repositories.PlayerRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Controller; - -/** - * This API is for in-game events management. - */ -@Api(name = "Game") -@Controller -public class GameController { - - private static final Logger logger = LoggerFactory.getLogger(GameController.class); - - private final SimpMessagingTemplate template; - - private final PlayerRepository playerRepository; - - @Autowired - public GameController(SimpMessagingTemplate template, PlayerRepository playerRepository) { - this.template = template; - this.playerRepository = playerRepository; - } - - /** - * Notifies the game that the player is ready to receive his hand. - * - * @param principal - * the connected user's information - */ - @MessageMapping("/game/sayReady") - public void ready(Principal principal) { - Player player = playerRepository.find(principal.getName()); - player.setReady(true); - Game game = player.getGame(); - logger.info("Game {}: player {} is ready for the next turn", game.getId(), player); - - Lobby lobby = player.getLobby(); - List<Player> players = lobby.getPlayers(); - - boolean allReady = players.stream().allMatch(Player::isReady); - if (allReady) { - logger.info("Game {}: all players ready, sending turn info", game.getId()); - players.forEach(p -> p.setReady(false)); - sendTurnInfo(players, game); - } else { - sendPlayerReady(game.getId(), player); - } - } - - private void sendTurnInfo(List<Player> players, Game game) { - for (PlayerTurnInfo turnInfo : game.getCurrentTurnInfo()) { - Player player = players.get(turnInfo.getPlayerIndex()); - template.convertAndSendToUser(player.getUsername(), "/queue/game/turn", turnInfo); - } - } - - private void sendPlayerReady(long gameId, Player player) { - template.convertAndSend("/topic/game/" + gameId + "/playerReady", player.getUsername()); - } - - /** - * Prepares the player's next move. When all players have prepared their moves, all moves are executed. - * - * @param action - * the action to prepare the move - * @param principal - * the connected user's information - */ - @MessageMapping("/game/prepareMove") - public void prepareMove(PrepareMoveAction action, Principal principal) { - Player player = playerRepository.find(principal.getName()); - Game game = player.getGame(); - CardBack preparedCardBack = game.prepareMove(player.getIndex(), action.getMove()); - PreparedCard preparedCard = new PreparedCard(player, preparedCardBack); - logger.info("Game {}: player {} prepared move {}", game.getId(), principal.getName(), action.getMove()); - - if (game.allPlayersPreparedTheirMove()) { - logger.info("Game {}: all players have prepared their move, executing turn...", game.getId()); - Table table = game.playTurn(); - sendPlayedMoves(game.getId(), table); - } else { - sendPreparedCard(game.getId(), preparedCard); - } - } - - private void sendPlayedMoves(long gameId, Table table) { - template.convertAndSend("/topic/game/" + gameId + "/tableUpdates", table); - } - - private void sendPreparedCard(long gameId, PreparedCard preparedCard) { - template.convertAndSend("/topic/game/" + gameId + "/prepared", preparedCard); - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/controllers/HomeController.java b/backend/src/main/java/org/luxons/sevenwonders/controllers/HomeController.java deleted file mode 100644 index 97bc8b67..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/controllers/HomeController.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; - -import org.hildan.livedoc.core.annotations.Api; -import org.luxons.sevenwonders.actions.ChooseNameAction; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.PlayerRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.annotation.SendToUser; -import org.springframework.stereotype.Controller; -import org.springframework.validation.annotation.Validated; - -/** - * Handles actions in the homepage of the game. - */ -@Api(name = "Home") -@Controller -public class HomeController { - - private static final Logger logger = LoggerFactory.getLogger(HomeController.class); - - private final PlayerRepository playerRepository; - - @Autowired - public HomeController(PlayerRepository playerRepository) { - this.playerRepository = playerRepository; - } - - /** - * Creates/updates the player's name (for the user's session). - * - * @param action - * the action to choose the name of the player - * @param principal - * the connected user's information - * - * @return the created {@link Player} object - */ - @MessageMapping("/chooseName") - @SendToUser("/queue/nameChoice") - public Player chooseName(@Validated ChooseNameAction action, Principal principal) { - String username = principal.getName(); - Player player = playerRepository.createOrUpdate(username, action.getPlayerName()); - - logger.info("Player '{}' chose the name '{}'", username, player.getDisplayName()); - return player; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java b/backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java deleted file mode 100644 index 330a0f7f..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; -import java.util.Collections; - -import org.hildan.livedoc.core.annotations.Api; -import org.luxons.sevenwonders.actions.ReorderPlayersAction; -import org.luxons.sevenwonders.actions.UpdateSettingsAction; -import org.luxons.sevenwonders.errors.ApiMisuseException; -import org.luxons.sevenwonders.game.Game; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.PlayerRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Controller; -import org.springframework.validation.annotation.Validated; - -/** - * Handles actions in the game's lobby. The lobby is the place where players gather before a game. - */ -@Api(name = "Lobby") -@Controller -public class LobbyController { - - private static final Logger logger = LoggerFactory.getLogger(LobbyController.class); - - private final PlayerRepository playerRepository; - - private final SimpMessagingTemplate template; - - @Autowired - public LobbyController(PlayerRepository playerRepository, SimpMessagingTemplate template) { - this.playerRepository = playerRepository; - this.template = template; - } - - /** - * Leaves the current lobby. - * - * @param principal - * the connected user's information - */ - @MessageMapping("/lobby/leave") - public void leave(Principal principal) { - Lobby lobby = getLobby(principal); - Player player = lobby.removePlayer(principal.getName()); - - logger.info("Player {} left game '{}'", player, lobby.getName()); - sendLobbyUpdateToPlayers(lobby); - } - - /** - * Reorders the players in the current lobby. This can only be done by the lobby's owner. - * - * @param action - * the action to reorder the players - * @param principal - * the connected user's information - */ - @MessageMapping("/lobby/reorderPlayers") - public void reorderPlayers(@Validated ReorderPlayersAction action, Principal principal) { - Lobby lobby = getOwnedLobby(principal); - lobby.reorderPlayers(action.getOrderedPlayers()); - - logger.info("Players in game '{}' reordered to {}", lobby.getName(), action.getOrderedPlayers()); - sendLobbyUpdateToPlayers(lobby); - } - - /** - * Updates the game settings. This can only be done by the lobby's owner. - * - * @param action - * the action to update the settings - * @param principal - * the connected user's information - */ - @MessageMapping("/lobby/updateSettings") - public void updateSettings(@Validated UpdateSettingsAction action, Principal principal) { - Lobby lobby = getOwnedLobby(principal); - lobby.setSettings(action.getSettings()); - - logger.info("Updated settings of game '{}'", lobby.getName()); - sendLobbyUpdateToPlayers(lobby); - } - - void sendLobbyUpdateToPlayers(Lobby lobby) { - template.convertAndSend("/topic/lobby/" + lobby.getId() + "/updated", lobby); - template.convertAndSend("/topic/games", Collections.singletonList(lobby)); - } - - /** - * Starts the game. - * - * @param principal - * the connected user's information - */ - @MessageMapping("/lobby/startGame") - public void startGame(Principal principal) { - Lobby lobby = getOwnedLobby(principal); - Game game = lobby.startGame(); - - logger.info("Game {} successfully started", game.getId()); - template.convertAndSend("/topic/lobby/" + lobby.getId() + "/started", ""); - } - - private Lobby getOwnedLobby(Principal principal) { - Lobby lobby = getLobby(principal); - if (!lobby.isOwner(principal.getName())) { - throw new PlayerIsNotOwnerException(principal.getName()); - } - return lobby; - } - - private Lobby getLobby(Principal principal) { - Lobby lobby = getPlayer(principal).getLobby(); - if (lobby == null) { - throw new PlayerNotInLobbyException(principal.getName()); - } - return lobby; - } - - private Player getPlayer(Principal principal) { - return playerRepository.find(principal.getName()); - } - - static class PlayerNotInLobbyException extends ApiMisuseException { - PlayerNotInLobbyException(String username) { - super("User " + username + " is not in a lobby, create or join a game first"); - } - } - - static class PlayerIsNotOwnerException extends ApiMisuseException { - PlayerIsNotOwnerException(String username) { - super("User " + username + " does not own the lobby he's in"); - } - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/doc/Documentation.java b/backend/src/main/java/org/luxons/sevenwonders/doc/Documentation.java deleted file mode 100644 index e27bc2ff..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/doc/Documentation.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.luxons.sevenwonders.doc; - -public class Documentation { - - public static final String GROUP_ACTIONS = "Actions"; -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java b/backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java deleted file mode 100644 index 0d7d1a82..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.luxons.sevenwonders.errors; - -public class ApiMisuseException extends RuntimeException { - - public ApiMisuseException(String message) { - super(message); - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java b/backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java deleted file mode 100644 index 16a40046..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.luxons.sevenwonders.errors; - -enum ErrorType { - USER_INPUT, - VALIDATION, - CLIENT, - SERVER -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java b/backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java deleted file mode 100644 index 628da4f8..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.luxons.sevenwonders.errors; - -import java.util.List; -import java.util.Locale; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.MessageSource; -import org.springframework.messaging.converter.MessageConversionException; -import org.springframework.messaging.handler.annotation.MessageExceptionHandler; -import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; -import org.springframework.messaging.simp.annotation.SendToUser; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.web.bind.annotation.ControllerAdvice; - -@ControllerAdvice -public class ExceptionHandler { - - private static final Logger logger = LoggerFactory.getLogger(ExceptionHandler.class); - - private static final String ERROR_CODE_VALIDATION = "INVALID_DATA"; - - private static final String ERROR_CODE_CONVERSION = "INVALID_MESSAGE_FORMAT"; - - private static final String ERROR_MSG_VALIDATION = "Invalid input data"; - - private static final String ERROR_MSG_CONVERSION = "Invalid input format"; - - private final MessageSource messageSource; - - @Autowired - public ExceptionHandler(MessageSource messageSource) { - this.messageSource = messageSource; - } - - @MessageExceptionHandler - @SendToUser("/queue/errors") - public UIError handleUserInputError(UserInputException exception) { - logger.error("Incorrect user input: " + exception.getMessage()); - String messageKey = exception.getMessageResourceKey(); - String message = messageSource.getMessage(messageKey, exception.getParams(), messageKey, Locale.US); - return new UIError(messageKey, message, ErrorType.USER_INPUT); - } - - @MessageExceptionHandler - @SendToUser("/queue/errors") - public UIError handleValidationError(MethodArgumentNotValidException exception) { - logger.error("Invalid input", exception); - UIError uiError = new UIError(ERROR_CODE_VALIDATION, ERROR_MSG_VALIDATION, ErrorType.VALIDATION); - - BindingResult result = exception.getBindingResult(); - if (result != null) { - List<ObjectError> errors = result.getAllErrors(); - uiError.addDetails(errors); - } - return uiError; - } - - @MessageExceptionHandler - @SendToUser("/queue/errors") - public UIError handleConversionError(MessageConversionException exception) { - logger.error("Error interpreting the message", exception); - return new UIError(ERROR_CODE_CONVERSION, ERROR_MSG_CONVERSION, ErrorType.VALIDATION); - } - - @MessageExceptionHandler - @SendToUser("/queue/errors") - public UIError handleApiError(ApiMisuseException exception) { - logger.error("Invalid API input", exception); - return new UIError(exception.getClass().getSimpleName(), exception.getMessage(), ErrorType.CLIENT); - } - - @MessageExceptionHandler - @SendToUser("/queue/errors") - public UIError handleUnexpectedInternalError(Throwable exception) { - logger.error("Uncaught exception thrown during message handling", exception); - return new UIError(exception.getClass().getSimpleName(), exception.getMessage(), ErrorType.SERVER); - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java b/backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java deleted file mode 100644 index fbc1ee1d..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.luxons.sevenwonders.errors; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; - -public class UIError { - - private final String code; - - private final String message; - - private final ErrorType type; - - private List<UIErrorDetail> details = new ArrayList<>(); - - UIError(String code, String message, ErrorType type) { - this.code = code; - this.message = message; - this.type = type; - } - - public String getCode() { - return code; - } - - public String getMessage() { - return message; - } - - public ErrorType getType() { - return type; - } - - public List<UIErrorDetail> getDetails() { - return details; - } - - void addDetails(List<ObjectError> objectErrors) { - for (ObjectError objectError : objectErrors) { - this.details.add(convertError(objectError)); - } - } - - private UIErrorDetail convertError(ObjectError objectError) { - if (objectError instanceof FieldError) { - return new UIErrorDetail((FieldError) objectError); - } else { - return new UIErrorDetail(objectError); - } - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java b/backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java deleted file mode 100644 index dc4250bb..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.luxons.sevenwonders.errors; - -import org.springframework.validation.FieldError; -import org.springframework.validation.ObjectError; - -class UIErrorDetail { - - private final Object rejectedValue; - - private final String path; - - private final String message; - - UIErrorDetail(FieldError error) { - rejectedValue = error.getRejectedValue(); - path = error.getObjectName() + '.' + error.getField(); - message = "Invalid value for field '" + error.getField() + "': " + error.getDefaultMessage(); - } - - UIErrorDetail(ObjectError error) { - rejectedValue = null; - path = error.getObjectName(); - message = "Invalid value for object '" + error.getObjectName() + "': " + error.getDefaultMessage(); - } - - public Object getRejectedValue() { - return rejectedValue; - } - - public String getPath() { - return path; - } - - public String getMessage() { - return message; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java b/backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java deleted file mode 100644 index 4033a696..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.luxons.sevenwonders.errors; - -public class UserInputException extends RuntimeException { - - private final String messageResourceKey; - - private final Object[] params; - - public UserInputException(String messageResourceKey, Object... params) { - this.messageResourceKey = messageResourceKey; - this.params = params; - } - - String getMessageResourceKey() { - return messageResourceKey; - } - - Object[] getParams() { - return params; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/lobby/Lobby.java b/backend/src/main/java/org/luxons/sevenwonders/lobby/Lobby.java deleted file mode 100644 index 1ddde0d7..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/lobby/Lobby.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.luxons.sevenwonders.lobby; - -import java.util.ArrayList; -import java.util.List; - -import org.luxons.sevenwonders.game.Game; -import org.luxons.sevenwonders.game.api.CustomizableSettings; -import org.luxons.sevenwonders.game.data.GameDefinition; - -public class Lobby { - - private final long id; - - private final String name; - - private final Player owner; - - private final transient GameDefinition gameDefinition; - - private final List<Player> players; - - private CustomizableSettings settings; - - private State state = State.LOBBY; - - public Lobby(long id, String name, Player owner, GameDefinition gameDefinition) { - this.id = id; - this.name = name; - this.owner = owner; - this.gameDefinition = gameDefinition; - this.players = new ArrayList<>(gameDefinition.getMinPlayers()); - this.settings = new CustomizableSettings(); - addPlayer(owner); - } - - public long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getOwner() { - return owner.getUsername(); - } - - public List<Player> getPlayers() { - return players; - } - - public CustomizableSettings getSettings() { - return settings; - } - - public void setSettings(CustomizableSettings settings) { - this.settings = settings; - } - - public State getState() { - return state; - } - - public synchronized void addPlayer(Player player) throws GameAlreadyStartedException, PlayerOverflowException { - if (hasStarted()) { - throw new GameAlreadyStartedException(name); - } - if (maxPlayersReached()) { - throw new PlayerOverflowException(gameDefinition.getMaxPlayers()); - } - if (playerNameAlreadyUsed(player.getDisplayName())) { - throw new PlayerNameAlreadyUsedException(player.getDisplayName(), name); - } - player.setIndex(players.size()); - player.setLobby(this); - players.add(player); - } - - private boolean hasStarted() { - return state != State.LOBBY; - } - - private boolean maxPlayersReached() { - return players.size() >= gameDefinition.getMaxPlayers(); - } - - private boolean playerNameAlreadyUsed(String name) { - return players.stream().anyMatch(p -> p.getDisplayName().equals(name)); - } - - public synchronized Game startGame() throws PlayerUnderflowException { - if (!hasEnoughPlayers()) { - throw new PlayerUnderflowException(gameDefinition.getMinPlayers()); - } - state = State.PLAYING; - Game game = gameDefinition.initGame(id, settings, players.size()); - players.forEach(p -> p.setGame(game)); - return game; - } - - private boolean hasEnoughPlayers() { - return players.size() >= gameDefinition.getMinPlayers(); - } - - public void reorderPlayers(List<String> orderedUsernames) throws UnknownPlayerException { - List<Player> formerList = new ArrayList<>(players); - players.clear(); - for (int i = 0; i < orderedUsernames.size(); i++) { - Player player = getPlayer(formerList, orderedUsernames.get(i)); - players.add(player); - player.setIndex(i); - } - } - - private static Player getPlayer(List<Player> players, String username) throws UnknownPlayerException { - return players.stream() - .filter(p -> p.getUsername().equals(username)) - .findAny() - .orElseThrow(() -> new UnknownPlayerException(username)); - } - - public boolean isOwner(String username) { - return owner.getUsername().equals(username); - } - - public boolean containsUser(String username) { - return players.stream().anyMatch(p -> p.getUsername().equals(username)); - } - - public Player removePlayer(String username) throws UnknownPlayerException { - Player player = getPlayer(players, username); - players.remove(player); - player.setIndex(-1); - player.setLobby(null); - player.setGame(null); - return player; - } - - static class GameAlreadyStartedException extends IllegalStateException { - GameAlreadyStartedException(String name) { - super(String.format("Game '%s' has already started", name)); - } - } - - static class PlayerOverflowException extends IllegalStateException { - PlayerOverflowException(int max) { - super(String.format("Maximum %d players allowed", max)); - } - } - - static class PlayerUnderflowException extends IllegalStateException { - PlayerUnderflowException(int min) { - super(String.format("Minimum %d players required to start a game", min)); - } - } - - static class PlayerNameAlreadyUsedException extends IllegalStateException { - PlayerNameAlreadyUsedException(String displayName, String gameName) { - super(String.format("Name '%s' is already used by a player in game '%s'", displayName, gameName)); - } - } - - static class UnknownPlayerException extends IllegalStateException { - UnknownPlayerException(String username) { - super(String.format("Unknown player '%s'", username)); - } - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/lobby/Player.java b/backend/src/main/java/org/luxons/sevenwonders/lobby/Player.java deleted file mode 100644 index 496e1e67..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/lobby/Player.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.luxons.sevenwonders.lobby; - -import org.luxons.sevenwonders.game.Game; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -public class Player { - - private final String username; - - private String displayName; - - private int index; - - private boolean ready; - - private transient Lobby lobby; - - private transient Game game; - - public Player(String username, String displayName) { - this.username = username; - this.displayName = displayName; - } - - public String getUsername() { - return username; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public int getIndex() { - return index; - } - - public void setIndex(int index) { - this.index = index; - } - - public boolean isReady() { - return ready; - } - - public void setReady(boolean ready) { - this.ready = ready; - } - - @JsonIgnore - public Lobby getLobby() { - return lobby; - } - - public void setLobby(Lobby lobby) { - this.lobby = lobby; - } - - @JsonIgnore - public Game getGame() { - return game; - } - - public void setGame(Game game) { - this.game = game; - } - - @Override - public String toString() { - return "'" + displayName + "' (" + username + ")"; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/lobby/State.java b/backend/src/main/java/org/luxons/sevenwonders/lobby/State.java deleted file mode 100644 index 17f3b101..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/lobby/State.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.luxons.sevenwonders.lobby; - -public enum State { - LOBBY, - PLAYING -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/output/PreparedCard.java b/backend/src/main/java/org/luxons/sevenwonders/output/PreparedCard.java deleted file mode 100644 index 5a122274..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/output/PreparedCard.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.luxons.sevenwonders.output; - -import org.luxons.sevenwonders.game.cards.CardBack; -import org.luxons.sevenwonders.lobby.Player; - -public class PreparedCard { - - private final Player player; - - private final CardBack cardBack; - - public PreparedCard(Player player, CardBack cardBack) { - this.player = player; - this.cardBack = cardBack; - } - - public Player getPlayer() { - return player; - } - - public CardBack getCardBack() { - return cardBack; - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java b/backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java deleted file mode 100644 index b1b11c82..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.luxons.sevenwonders.repositories; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.luxons.sevenwonders.game.data.GameDefinitionLoader; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -@Repository -public class LobbyRepository { - - private final GameDefinitionLoader gameDefinitionLoader; - - private Map<Long, Lobby> lobbies = new HashMap<>(); - - private long lastGameId = 0; - - @Autowired - public LobbyRepository() { - this.gameDefinitionLoader = new GameDefinitionLoader(); - } - - public Collection<Lobby> list() { - return lobbies.values(); - } - - public Lobby create(String gameName, Player owner) { - long id = lastGameId++; - Lobby lobby = new Lobby(id, gameName, owner, gameDefinitionLoader.getGameDefinition()); - lobbies.put(id, lobby); - return lobby; - } - - public Lobby find(long lobbyId) throws LobbyNotFoundException { - Lobby lobby = lobbies.get(lobbyId); - if (lobby == null) { - throw new LobbyNotFoundException(lobbyId); - } - return lobby; - } - - public Lobby remove(long lobbyId) throws LobbyNotFoundException { - Lobby lobby = lobbies.remove(lobbyId); - if (lobby == null) { - throw new LobbyNotFoundException(lobbyId); - } - return lobby; - } - - public static class LobbyNotFoundException extends RuntimeException { - LobbyNotFoundException(long id) { - super("Lobby not found for id '" + id + "'"); - } - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java b/backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java deleted file mode 100644 index 62c827db..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.luxons.sevenwonders.repositories; - -import java.util.HashMap; -import java.util.Map; - -import org.luxons.sevenwonders.errors.ApiMisuseException; -import org.luxons.sevenwonders.lobby.Player; -import org.springframework.stereotype.Repository; - -@Repository -public class PlayerRepository { - - private Map<String, Player> players = new HashMap<>(); - - public boolean contains(String username) { - return players.containsKey(username); - } - - public Player createOrUpdate(String username, String displayName) { - if (players.containsKey(username)) { - return update(username, displayName); - } else { - return create(username, displayName); - } - } - - private Player create(String username, String displayName) { - Player player = new Player(username, displayName); - players.put(username, player); - return player; - } - - private Player update(String username, String displayName) throws PlayerNotFoundException { - Player player = find(username); - player.setDisplayName(displayName); - return player; - } - - public Player find(String username) throws PlayerNotFoundException { - Player player = players.get(username); - if (player == null) { - throw new PlayerNotFoundException(username); - } - return player; - } - - public Player remove(String username) { - Player player = players.remove(username); - if (player == null) { - throw new PlayerNotFoundException(username); - } - return player; - } - - public static class PlayerNotFoundException extends ApiMisuseException { - PlayerNotFoundException(String username) { - super("Player '" + username + "' doesn't exist"); - } - } -} diff --git a/backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java b/backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java deleted file mode 100644 index cd17b0a5..00000000 --- a/backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.luxons.sevenwonders.validation; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.repositories.LobbyRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class DestinationAccessValidator { - - private static final Pattern lobbyDestination = Pattern.compile(".*?/lobby/(?<id>\\d+?)(/.*)?"); - - private static final Pattern gameDestination = Pattern.compile(".*?/game/(?<id>\\d+?)(/.*)?"); - - private final LobbyRepository lobbyRepository; - - @Autowired - public DestinationAccessValidator(LobbyRepository lobbyRepository) { - this.lobbyRepository = lobbyRepository; - } - - public boolean hasAccess(String username, String destination) { - if (username == null) { - // unnamed user cannot belong to anything - return false; - } - if (hasForbiddenGameReference(username, destination)) { - return false; - } - if (hasForbiddenLobbyReference(username, destination)) { - return false; - } - return true; - } - - private boolean hasForbiddenGameReference(String username, String destination) { - Matcher gameMatcher = gameDestination.matcher(destination); - if (!gameMatcher.matches()) { - return false; // no game reference is always OK - } - int gameId = extractId(gameMatcher); - return !isUserInLobby(username, gameId); - } - - private boolean hasForbiddenLobbyReference(String username, String destination) { - Matcher lobbyMatcher = lobbyDestination.matcher(destination); - if (!lobbyMatcher.matches()) { - return false; // no lobby reference is always OK - } - int lobbyId = extractId(lobbyMatcher); - return !isUserInLobby(username, lobbyId); - } - - private boolean isUserInLobby(String username, int lobbyId) { - Lobby lobby = lobbyRepository.find(lobbyId); - return lobby.containsUser(username); - } - - private static int extractId(Matcher matcher) { - String id = matcher.group("id"); - return Integer.parseInt(id); - } -} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt new file mode 100644 index 00000000..04f03956 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt @@ -0,0 +1,13 @@ +package org.luxons.sevenwonders + +import org.hildan.livedoc.spring.boot.starter.EnableJSONDoc +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableJSONDoc +class SevenWonders + +fun main(args: Array<String>) { + runApplication<SevenWonders>(*args) +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt new file mode 100644 index 00000000..ab444780 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt @@ -0,0 +1,19 @@ +package org.luxons.sevenwonders.actions + +import org.hildan.livedoc.core.annotations.types.ApiType +import org.hildan.livedoc.core.annotations.types.ApiTypeProperty +import org.luxons.sevenwonders.doc.Documentation +import javax.validation.constraints.Size + +/** + * The action to choose the player's name. This is the first action that should be called. + */ +@ApiType(group = Documentation.GROUP_ACTIONS) +class ChooseNameAction( + /** + * The display name of the player. May contain spaces and special characters. + */ + @Size(min = 2, max = 20) + @ApiTypeProperty(required = true) + val playerName: String +) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt new file mode 100644 index 00000000..fbe598f2 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt @@ -0,0 +1,21 @@ +package org.luxons.sevenwonders.actions + +import org.hildan.livedoc.core.annotations.types.ApiType +import org.hildan.livedoc.core.annotations.types.ApiTypeProperty +import org.luxons.sevenwonders.doc.Documentation +import javax.validation.constraints.Size + +/** + * The action to create a game. + */ +@ApiType(group = Documentation.GROUP_ACTIONS) +class CreateGameAction( + /** + * The name of the game to create. + */ + @Size(min = 2, max = 30) + @ApiTypeProperty(required = true) + val gameName: String +) + + diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt new file mode 100644 index 00000000..002309b3 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt @@ -0,0 +1,17 @@ +package org.luxons.sevenwonders.actions + +import org.hildan.livedoc.core.annotations.types.ApiType +import org.hildan.livedoc.core.annotations.types.ApiTypeProperty +import org.luxons.sevenwonders.doc.Documentation + +/** + * The action to join a game. + */ +@ApiType(group = Documentation.GROUP_ACTIONS) +class JoinGameAction( + /** + * The ID of the game to join. + */ + @ApiTypeProperty(required = true) + val gameId: Long +) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt new file mode 100644 index 00000000..6b39c486 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt @@ -0,0 +1,18 @@ +package org.luxons.sevenwonders.actions + +import org.hildan.livedoc.core.annotations.types.ApiType +import org.hildan.livedoc.core.annotations.types.ApiTypeProperty +import org.luxons.sevenwonders.doc.Documentation +import org.luxons.sevenwonders.game.api.PlayerMove + +/** + * The action to prepare the next move during a game. + */ +@ApiType(group = Documentation.GROUP_ACTIONS) +class PrepareMoveAction( + /** + * The move to prepare. + */ + @ApiTypeProperty(required = true) + val move: PlayerMove +) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt new file mode 100644 index 00000000..79a32137 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt @@ -0,0 +1,18 @@ +package org.luxons.sevenwonders.actions + +import org.hildan.livedoc.core.annotations.types.ApiType +import org.hildan.livedoc.core.annotations.types.ApiTypeProperty +import org.luxons.sevenwonders.doc.Documentation + +/** + * The action to update the order of the players around the table. Can only be called in the lobby by the owner of the + * game. + */ +@ApiType(group = Documentation.GROUP_ACTIONS) +class ReorderPlayersAction( + /** + * The list of usernames of the players, in the new order. + */ + @ApiTypeProperty(required = true) + val orderedPlayers: List<String> +) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt new file mode 100644 index 00000000..d13e5b45 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt @@ -0,0 +1,18 @@ +package org.luxons.sevenwonders.actions + +import org.hildan.livedoc.core.annotations.types.ApiType +import org.hildan.livedoc.core.annotations.types.ApiTypeProperty +import org.luxons.sevenwonders.doc.Documentation +import org.luxons.sevenwonders.game.api.CustomizableSettings + +/** + * The action to update the settings of the game. Can only be called in the lobby by the owner of the game. + */ +@ApiType(group = Documentation.GROUP_ACTIONS) +class UpdateSettingsAction( + /** + * The new values for the settings. + */ + @ApiTypeProperty(required = true) + val settings: CustomizableSettings +) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt new file mode 100644 index 00000000..db707d1b --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt @@ -0,0 +1,27 @@ +package org.luxons.sevenwonders.config + +import org.springframework.http.server.ServerHttpRequest +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.web.socket.WebSocketHandler +import org.springframework.web.socket.server.support.DefaultHandshakeHandler +import java.security.Principal + +/** + * Generates [Principal] objects for anonymous users in the form "playerX", where X is an auto-incremented number. + */ +internal class AnonymousUsersHandshakeHandler : DefaultHandshakeHandler() { + + private var playerId = 0 + + override fun determineUser( + request: ServerHttpRequest, + wsHandler: WebSocketHandler, + attributes: Map<String, Any> + ): Principal? { + var p = super.determineUser(request, wsHandler, attributes) + if (p == null) { + p = UsernamePasswordAuthenticationToken("player" + playerId++, null) + } + return p + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt new file mode 100644 index 00000000..f4c55c2c --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt @@ -0,0 +1,38 @@ +package org.luxons.sevenwonders.config + +import org.luxons.sevenwonders.validation.DestinationAccessValidator +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.simp.stomp.StompCommand +import org.springframework.messaging.simp.stomp.StompHeaderAccessor +import org.springframework.messaging.support.ChannelInterceptor +import org.springframework.stereotype.Component + +@Component +class TopicSubscriptionInterceptor @Autowired constructor( + private val destinationAccessValidator: DestinationAccessValidator +) : ChannelInterceptor { + + override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? { + val headerAccessor = StompHeaderAccessor.wrap(message) + if (StompCommand.SUBSCRIBE == headerAccessor.command) { + val username = headerAccessor.user!!.name + val destination = headerAccessor.destination!! + if (!destinationAccessValidator.hasAccess(username, destination)) { + sendForbiddenSubscriptionError(username, destination) + return null + } + } + return message + } + + private fun sendForbiddenSubscriptionError(username: String, destination: String?) { + logger.error(String.format("Player '%s' is not allowed to access %s", username, destination)) + } + + companion object { + private val logger = LoggerFactory.getLogger(TopicSubscriptionInterceptor::class.java) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt new file mode 100644 index 00000000..06b2bc90 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt @@ -0,0 +1,12 @@ +package org.luxons.sevenwonders.config + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter + +@Configuration +class WebSecurityConfig : WebSecurityConfigurerAdapter() { + + // this disables default authentication settings + override fun configure(httpSecurity: HttpSecurity) = Unit +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt new file mode 100644 index 00000000..bebb3233 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt @@ -0,0 +1,42 @@ +package org.luxons.sevenwonders.config + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.messaging.simp.config.ChannelRegistration +import org.springframework.messaging.simp.config.MessageBrokerRegistry +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker +import org.springframework.web.socket.config.annotation.StompEndpointRegistry +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer +import org.springframework.web.socket.server.support.DefaultHandshakeHandler + +@Configuration +@EnableWebSocketMessageBroker +class WebSocketConfig @Autowired constructor(private val topicSubscriptionInterceptor: TopicSubscriptionInterceptor) : + WebSocketMessageBrokerConfigurer { + + override fun configureMessageBroker(config: MessageBrokerRegistry) { + // prefixes for all subscriptions + config.enableSimpleBroker("/queue", "/topic") + config.setUserDestinationPrefix("/user") + + // /app for normal calls, /topic for subscription events + config.setApplicationDestinationPrefixes("/app", "/topic") + } + + override fun registerStompEndpoints(registry: StompEndpointRegistry) { + registry.addEndpoint("/seven-wonders-websocket") + .setHandshakeHandler(handshakeHandler()) + .setAllowedOrigins("http://localhost:3000") // to allow frontend server proxy requests in dev mode + .withSockJS() + } + + @Bean + fun handshakeHandler(): DefaultHandshakeHandler { + return AnonymousUsersHandshakeHandler() + } + + override fun configureClientInboundChannel(registration: ChannelRegistration) { + registration.interceptors(topicSubscriptionInterceptor) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt new file mode 100644 index 00000000..b8e4e732 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt @@ -0,0 +1,109 @@ +package org.luxons.sevenwonders.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.actions.CreateGameAction +import org.luxons.sevenwonders.actions.JoinGameAction +import org.luxons.sevenwonders.errors.ApiMisuseException +import org.luxons.sevenwonders.lobby.Lobby +import org.luxons.sevenwonders.repositories.LobbyRepository +import org.luxons.sevenwonders.repositories.PlayerRepository +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.messaging.simp.annotation.SendToUser +import org.springframework.messaging.simp.annotation.SubscribeMapping +import org.springframework.stereotype.Controller +import org.springframework.validation.annotation.Validated +import java.security.Principal + +/** + * This is the place where the player looks for a game. + */ +@Api(name = "GameBrowser") +@Controller +class GameBrowserController @Autowired constructor( + private val lobbyController: LobbyController, + private val lobbyRepository: LobbyRepository, + private val playerRepository: PlayerRepository, + private val template: SimpMessagingTemplate +) { + + /** + * Gets the created or updated games. The list of existing games is received on this topic at once upon + * subscription, and then each time the list changes. + * + * @param principal the connected user's information + * + * @return the current list of [Lobby]s + */ + @SubscribeMapping("/games") // prefix /topic not shown + fun listGames(principal: Principal): Collection<Lobby> { + logger.info("Player '{}' subscribed to /topic/games", principal.name) + return lobbyRepository.list() + } + + /** + * Creates a new [Lobby]. + * + * @param action the action to create the game + * @param principal the connected user's information + * + * @return the newly created [Lobby] + */ + @MessageMapping("/lobby/create") + @SendToUser("/queue/lobby/joined") + fun createGame(@Validated action: CreateGameAction, principal: Principal): Lobby { + checkThatUserIsNotInAGame(principal, "cannot create another game") + + val gameOwner = playerRepository.find(principal.name) + val lobby = lobbyRepository.create(action.gameName, gameOwner) + + logger.info( + "Game '{}' ({}) created by {} ({})", lobby.name, lobby.id, gameOwner.displayName, gameOwner.username + ) + + // notify everyone that a new game exists + template.convertAndSend("/topic/games", listOf(lobby)) + return lobby + } + + /** + * Joins an existing [Lobby]. + * + * @param action the action to join the game + * @param principal the connected user's information + * + * @return the [Lobby] that has just been joined + */ + @MessageMapping("/lobby/join") + @SendToUser("/queue/lobby/joined") + fun joinGame(@Validated action: JoinGameAction, principal: Principal): Lobby { + checkThatUserIsNotInAGame(principal, "cannot join another game") + + val lobby = lobbyRepository.find(action.gameId!!) + val newPlayer = playerRepository.find(principal.name) + lobby.addPlayer(newPlayer) + + logger.info( + "Player '{}' ({}) joined game {}", newPlayer.displayName, newPlayer.username, lobby.name + ) + lobbyController.sendLobbyUpdateToPlayers(lobby) + return lobby + } + + private fun checkThatUserIsNotInAGame(principal: Principal, impossibleActionDescription: String) { + val player = playerRepository.find(principal.name) + if (player.isInLobby || player.isInGame) { + throw UserAlreadyInGameException(player.lobby.name, impossibleActionDescription) + } + } + + internal class UserAlreadyInGameException(gameName: String, impossibleActionDescription: String) : + ApiMisuseException("Client already in game '$gameName', $impossibleActionDescription") + + companion object { + + private val logger = LoggerFactory.getLogger(GameBrowserController::class.java) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt new file mode 100644 index 00000000..e05bf319 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt @@ -0,0 +1,99 @@ +package org.luxons.sevenwonders.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.actions.PrepareMoveAction +import org.luxons.sevenwonders.game.Game +import org.luxons.sevenwonders.game.api.Table +import org.luxons.sevenwonders.lobby.Player +import org.luxons.sevenwonders.output.PreparedCard +import org.luxons.sevenwonders.repositories.PlayerRepository +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Controller +import java.security.Principal + +/** + * This API is for in-game events management. + */ +@Api(name = "Game") +@Controller +class GameController @Autowired constructor( + private val template: SimpMessagingTemplate, + private val playerRepository: PlayerRepository +) { + private val Principal.player + get() = playerRepository.find(name) + + /** + * Notifies the game that the player is ready to receive his hand. + * + * @param principal + * the connected user's information + */ + @MessageMapping("/game/sayReady") + fun ready(principal: Principal) { + val player = principal.player + player.isReady = true + val game = player.game + logger.info("Game {}: player {} is ready for the next turn", game.id, player) + + val lobby = player.lobby + val players = lobby.getPlayers() + + val allReady = players.stream().allMatch { it.isReady } + if (allReady) { + logger.info("Game {}: all players ready, sending turn info", game.id) + players.forEach { it.isReady = false } + sendTurnInfo(players, game) + } else { + sendPlayerReady(game.id, player) + } + } + + private fun sendTurnInfo(players: List<Player>, game: Game) { + for (turnInfo in game.getCurrentTurnInfo()) { + val player = players[turnInfo.playerIndex] + template.convertAndSendToUser(player.username, "/queue/game/turn", turnInfo) + } + } + + private fun sendPlayerReady(gameId: Long, player: Player) = + template.convertAndSend("/topic/game/$gameId/playerReady", player.username) + + /** + * Prepares the player's next move. When all players have prepared their moves, all moves are executed. + * + * @param action + * the action to prepare the move + * @param principal + * the connected user's information + */ + @MessageMapping("/game/prepareMove") + fun prepareMove(action: PrepareMoveAction, principal: Principal) { + val player = principal.player + val game = player.game + val preparedCardBack = game.prepareMove(player.index, action.move) + val preparedCard = PreparedCard(player, preparedCardBack) + logger.info("Game {}: player {} prepared move {}", game.id, principal.name, action.move) + + if (game.allPlayersPreparedTheirMove()) { + logger.info("Game {}: all players have prepared their move, executing turn...", game.id) + val table = game.playTurn() + sendPlayedMoves(game.id, table) + } else { + sendPreparedCard(game.id, preparedCard) + } + } + + private fun sendPlayedMoves(gameId: Long, table: Table) = + template.convertAndSend("/topic/game/$gameId/tableUpdates", table) + + private fun sendPreparedCard(gameId: Long, preparedCard: PreparedCard) = + template.convertAndSend("/topic/game/$gameId/prepared", preparedCard) + + companion object { + private val logger = LoggerFactory.getLogger(GameController::class.java) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt new file mode 100644 index 00000000..e658a26b --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt @@ -0,0 +1,47 @@ +package org.luxons.sevenwonders.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.actions.ChooseNameAction +import org.luxons.sevenwonders.lobby.Player +import org.luxons.sevenwonders.repositories.PlayerRepository +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.simp.annotation.SendToUser +import org.springframework.stereotype.Controller +import org.springframework.validation.annotation.Validated +import java.security.Principal + +/** + * Handles actions in the homepage of the game. + */ +@Api(name = "Home") +@Controller +class HomeController @Autowired constructor( + private val playerRepository: PlayerRepository +) { + + /** + * Creates/updates the player's name (for the user's session). + * + * @param action + * the action to choose the name of the player + * @param principal + * the connected user's information + * + * @return the created [Player] object + */ + @MessageMapping("/chooseName") + @SendToUser("/queue/nameChoice") + fun chooseName(@Validated action: ChooseNameAction, principal: Principal): Player { + val username = principal.name + val player = playerRepository.createOrUpdate(username, action.playerName) + + logger.info("Player '{}' chose the name '{}'", username, player.displayName) + return player + } + + companion object { + private val logger = LoggerFactory.getLogger(HomeController::class.java) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt new file mode 100644 index 00000000..3b15d68a --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt @@ -0,0 +1,106 @@ +package org.luxons.sevenwonders.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.actions.ReorderPlayersAction +import org.luxons.sevenwonders.actions.UpdateSettingsAction +import org.luxons.sevenwonders.lobby.Lobby +import org.luxons.sevenwonders.lobby.Player +import org.luxons.sevenwonders.repositories.LobbyRepository +import org.luxons.sevenwonders.repositories.PlayerRepository +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Controller +import org.springframework.validation.annotation.Validated +import java.security.Principal + +/** + * Handles actions in the game's lobby. The lobby is the place where players gather before a game. + */ +@Api(name = "Lobby") +@Controller +class LobbyController @Autowired constructor( + private val lobbyRepository: LobbyRepository, + private val playerRepository: PlayerRepository, + private val template: SimpMessagingTemplate +) { + private val Principal.player: Player + get() = playerRepository.find(name) + + /** + * Leaves the current lobby. + * + * @param principal + * the connected user's information + */ + @MessageMapping("/lobby/leave") + fun leave(principal: Principal) { + val lobby = principal.player.lobby + val player = lobby.removePlayer(principal.name) + if (lobby.getPlayers().isEmpty()) { + lobbyRepository.remove(lobby.id) + } + + logger.info("Player {} left game '{}'", player, lobby.name) + sendLobbyUpdateToPlayers(lobby) + } + + /** + * Reorders the players in the current lobby. This can only be done by the lobby's owner. + * + * @param action + * the action to reorder the players + * @param principal + * the connected user's information + */ + @MessageMapping("/lobby/reorderPlayers") + fun reorderPlayers(@Validated action: ReorderPlayersAction, principal: Principal) { + val lobby = principal.player.ownedLobby + lobby.reorderPlayers(action.orderedPlayers) + + logger.info("Players in game '{}' reordered to {}", lobby.name, action.orderedPlayers) + sendLobbyUpdateToPlayers(lobby) + } + + /** + * Updates the game settings. This can only be done by the lobby's owner. + * + * @param action + * the action to update the settings + * @param principal + * the connected user's information + */ + @MessageMapping("/lobby/updateSettings") + fun updateSettings(@Validated action: UpdateSettingsAction, principal: Principal) { + val lobby = principal.player.ownedLobby + lobby.settings = action.settings + + logger.info("Updated settings of game '{}'", lobby.name) + sendLobbyUpdateToPlayers(lobby) + } + + internal fun sendLobbyUpdateToPlayers(lobby: Lobby) { + template.convertAndSend("/topic/lobby/" + lobby.id + "/updated", lobby) + template.convertAndSend("/topic/games", listOf(lobby)) + } + + /** + * Starts the game. + * + * @param principal + * the connected user's information + */ + @MessageMapping("/lobby/startGame") + fun startGame(principal: Principal) { + val lobby = principal.player.ownedLobby + val game = lobby.startGame() + + logger.info("Game {} successfully started", game.id) + template.convertAndSend("/topic/lobby/" + lobby.id + "/started", "") + } + + companion object { + private val logger = LoggerFactory.getLogger(LobbyController::class.java) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt new file mode 100644 index 00000000..3b04356a --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt @@ -0,0 +1,6 @@ +package org.luxons.sevenwonders.doc + +object Documentation { + + const val GROUP_ACTIONS = "Actions" +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt new file mode 100644 index 00000000..c3eae0b5 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt @@ -0,0 +1,29 @@ +package org.luxons.sevenwonders.errors + +import org.springframework.validation.FieldError +import org.springframework.validation.ObjectError + +enum class ErrorType { + VALIDATION, CLIENT, SERVER +} + +data class ErrorDTO( + val code: String, + val message: String, + val type: ErrorType, + val details: List<ValidationErrorDTO> = emptyList() +) + +data class ValidationErrorDTO( + val path: String, + val message: String, + val rejectedValue: Any? = null +) + +fun ObjectError.toDTO() = (this as? FieldError)?.fieldError() ?: objectError() + +fun FieldError.fieldError(): ValidationErrorDTO = + ValidationErrorDTO("$objectName.$field", "Invalid value for field '$field': $defaultMessage", rejectedValue) + +fun ObjectError.objectError(): ValidationErrorDTO = + ValidationErrorDTO(objectName, "Invalid value for object '$objectName': $defaultMessage") diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt new file mode 100644 index 00000000..76d01f5f --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt @@ -0,0 +1,44 @@ +package org.luxons.sevenwonders.errors + +import org.slf4j.LoggerFactory +import org.springframework.messaging.converter.MessageConversionException +import org.springframework.messaging.handler.annotation.MessageExceptionHandler +import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException +import org.springframework.messaging.simp.annotation.SendToUser +import org.springframework.web.bind.annotation.ControllerAdvice + +open class ApiMisuseException(message: String) : RuntimeException(message) + +@ControllerAdvice +@SendToUser("/queue/errors") +class ExceptionHandler { + + @MessageExceptionHandler + fun handleValidationError(exception: MethodArgumentNotValidException): ErrorDTO { + logger.error("Invalid input", exception) + val validationErrors = exception.bindingResult?.allErrors?.map { it.toDTO() } ?: emptyList() + return ErrorDTO("INVALID_DATA", "Invalid input data", ErrorType.VALIDATION, validationErrors) + } + + @MessageExceptionHandler + fun handleConversionError(exception: MessageConversionException): ErrorDTO { + logger.error("Error interpreting the message", exception) + return ErrorDTO("INVALID_MESSAGE_FORMAT", "Invalid input format", ErrorType.VALIDATION) + } + + @MessageExceptionHandler + fun handleApiError(exception: ApiMisuseException): ErrorDTO { + logger.error("Invalid API input", exception) + return ErrorDTO(exception.javaClass.simpleName, exception.message!!, ErrorType.CLIENT) + } + + @MessageExceptionHandler + fun handleUnexpectedInternalError(exception: Throwable): ErrorDTO { + logger.error("Uncaught exception thrown during message handling", exception) + return ErrorDTO(exception.javaClass.simpleName, exception.localizedMessage ?: "", ErrorType.SERVER) + } + + companion object { + private val logger = LoggerFactory.getLogger(ExceptionHandler::class.java) + } +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt new file mode 100644 index 00000000..aaafd517 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt @@ -0,0 +1,107 @@ +package org.luxons.sevenwonders.lobby + +import org.luxons.sevenwonders.game.Game +import org.luxons.sevenwonders.game.api.CustomizableSettings +import org.luxons.sevenwonders.game.data.GameDefinition +import java.util.ArrayList + +enum class State { + LOBBY, PLAYING +} + +class Lobby( + val id: Long, + val name: String, + private var _owner: Player, + @field:Transient private val gameDefinition: GameDefinition +) { + private var players: MutableList<Player> = ArrayList(gameDefinition.maxPlayers) + + var settings: CustomizableSettings = CustomizableSettings() + + var owner = _owner.username + + var state = State.LOBBY + private set + + init { + addPlayer(_owner) + } + + fun getPlayers(): List<Player> = players + + @Synchronized + fun addPlayer(player: Player) { + if (hasStarted()) { + throw GameAlreadyStartedException(name) + } + if (maxPlayersReached()) { + throw PlayerOverflowException(gameDefinition.maxPlayers) + } + if (playerNameAlreadyUsed(player.displayName)) { + throw PlayerNameAlreadyUsedException(player.displayName, name) + } + player.join(this) + players.add(player) + } + + private fun hasStarted(): Boolean = state != State.LOBBY + + private fun maxPlayersReached(): Boolean = players.size >= gameDefinition.maxPlayers + + private fun playerNameAlreadyUsed(name: String?): Boolean = players.any { it.displayName == name } + + @Synchronized + fun startGame(): Game { + if (!hasEnoughPlayers()) { + throw PlayerUnderflowException(gameDefinition.minPlayers) + } + state = State.PLAYING + val game = gameDefinition.initGame(id, settings, players.size) + players.forEachIndexed { index, player -> player.join(game, index) } + return game + } + + private fun hasEnoughPlayers(): Boolean = players.size >= gameDefinition.minPlayers + + @Synchronized + fun reorderPlayers(orderedUsernames: List<String>) { + players = orderedUsernames.map { find(it) }.toMutableList() + } + + private fun find(username: String): Player = + players.firstOrNull { it.username == username } ?: throw UnknownPlayerException(username) + + @Synchronized + fun isOwner(username: String?): Boolean = _owner.username == username + + @Synchronized + fun containsUser(username: String): Boolean = players.any { it.username == username } + + @Synchronized + fun removePlayer(username: String): Player { + val player = find(username) + players.remove(player) + player.leave() + + if (player == _owner && !players.isEmpty()) { + _owner = players[0] + } + return player + } + + internal class GameAlreadyStartedException(name: String) : + IllegalStateException("Game '$name' has already started") + + internal class PlayerOverflowException(max: Int) : + IllegalStateException("Maximum $max players allowed") + + internal class PlayerUnderflowException(min: Int) : + IllegalStateException("Minimum $min players required to start a game") + + internal class PlayerNameAlreadyUsedException(displayName: String, gameName: String) : + IllegalStateException("Name '$displayName' is already used by a player in game '$gameName'") + + internal class UnknownPlayerException(username: String) : + IllegalStateException("Unknown player '$username'") +} diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt new file mode 100644 index 00000000..48a31047 --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt @@ -0,0 +1,70 @@ +package org.luxons.sevenwonders.lobby + +import com.fasterxml.jackson.annotation.JsonIgnore +import org.luxons.sevenwonders.errors.ApiMisuseException +import org.luxons.sevenwonders.game.Game + +class Player( + val username: String, + var displayName: String +) { + var index: Int = -1 + + var isReady: Boolean = false + + val isGameOwner: Boolean + get() = _lobby?.isOwner(username) ?: false + + val isInLobby: Boolean + get() = _lobby != null + + val isInGame: Boolean + get() = _game != null + + @Transient + private var _lobby: Lobby? = null + + @get:JsonIgnore + val lobby: Lobby + get() = _lobby ?: throw PlayerNotInLobbyException(username) + + @get:JsonIgnore + val ownedLobby: Lobby + get() = if (isGameOwner) lobby else throw PlayerIsNotOwnerException(username) + + @Transient + private var _game: Game? = null + + @get:JsonIgnore + val game: Game + get() = _game ?: throw PlayerNotInGameException(username) + + fun join(lobby: Lobby) { + _lobby = lobby + } + + fun join(game: Game, index: Int) { + _game = game + this.index = index + } + + fun leave() { + _lobby = null + _game = null + index = -1 + } + + override fun toString(): String { + return "'$displayName' ($username)" + } +} + +internal class PlayerNotInLobbyException(username: String) : + ApiMisuseException("User $username is not in a lobby, create or join a game first") + +internal class PlayerIsNotOwnerException(username: String) : + ApiMisuseException("User $username does not own the lobby he's in") + +internal class PlayerNotInGameException(username: String) : + ApiMisuseException("User $username is not in a game, start a game first") + diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/output/PreparedCard.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/output/PreparedCard.kt new file mode 100644 index 00000000..956b1a2c --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/output/PreparedCard.kt @@ -0,0 +1,6 @@ +package org.luxons.sevenwonders.output + +import org.luxons.sevenwonders.game.cards.CardBack +import org.luxons.sevenwonders.lobby.Player + +class PreparedCard(val player: Player, val cardBack: CardBack) diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt new file mode 100644 index 00000000..261f723c --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt @@ -0,0 +1,33 @@ +package org.luxons.sevenwonders.repositories + +import org.luxons.sevenwonders.game.data.GameDefinitionLoader +import org.luxons.sevenwonders.lobby.Lobby +import org.luxons.sevenwonders.lobby.Player +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Repository +import java.util.HashMap + +@Repository +class LobbyRepository @Autowired constructor() { + + private val gameDefinitionLoader: GameDefinitionLoader = GameDefinitionLoader() + + private val lobbies = HashMap<Long, Lobby>() + + private var lastGameId: Long = 0 + + fun list(): Collection<Lobby> = lobbies.values + + fun create(gameName: String, owner: Player): Lobby { + val id = lastGameId++ + val lobby = Lobby(id, gameName, owner, gameDefinitionLoader.gameDefinition) + lobbies[id] = lobby + return lobby + } + + fun find(lobbyId: Long): Lobby = lobbies[lobbyId] ?: throw LobbyNotFoundException(lobbyId) + + fun remove(lobbyId: Long): Lobby = lobbies.remove(lobbyId) ?: throw LobbyNotFoundException(lobbyId) +} + +internal class LobbyNotFoundException(id: Long) : RuntimeException("Lobby not found for id '$id'") diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt new file mode 100644 index 00000000..4d552eaa --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt @@ -0,0 +1,41 @@ +package org.luxons.sevenwonders.repositories + +import org.luxons.sevenwonders.errors.ApiMisuseException +import org.luxons.sevenwonders.lobby.Player +import org.springframework.stereotype.Repository +import java.util.HashMap + +@Repository +class PlayerRepository { + + private val players = HashMap<String, Player>() + + operator fun contains(username: String): Boolean = players.containsKey(username) + + fun createOrUpdate(username: String, displayName: String): Player { + return if (players.containsKey(username)) { + update(username, displayName) + } else { + create(username, displayName) + } + } + + private fun create(username: String, displayName: String): Player { + val player = Player(username, displayName) + players[username] = player + return player + } + + private fun update(username: String, displayName: String): Player { + val player = find(username) + player.displayName = displayName + return player + } + + fun find(username: String): Player = players[username] ?: throw PlayerNotFoundException(username) + + fun remove(username: String): Player = players.remove(username) ?: throw PlayerNotFoundException(username) +} + +internal class PlayerNotFoundException(username: String) : + ApiMisuseException("Player '$username' doesn't exist") diff --git a/backend/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt b/backend/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt new file mode 100644 index 00000000..5800edbb --- /dev/null +++ b/backend/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt @@ -0,0 +1,55 @@ +package org.luxons.sevenwonders.validation + +import org.luxons.sevenwonders.repositories.LobbyRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import java.util.regex.Matcher +import java.util.regex.Pattern + +@Component +class DestinationAccessValidator @Autowired constructor(private val lobbyRepository: LobbyRepository) { + + fun hasAccess(username: String?, destination: String): Boolean { + return when { + username == null -> false // unnamed user cannot belong to anything + hasForbiddenGameReference(username, destination) -> false + hasForbiddenLobbyReference(username, destination) -> false + else -> true + } + } + + private fun hasForbiddenGameReference(username: String, destination: String): Boolean { + val gameMatcher = gameDestination.matcher(destination) + if (!gameMatcher.matches()) { + return false // no game reference is always OK + } + val gameId = extractId(gameMatcher) + return !isUserInLobby(username, gameId) + } + + private fun hasForbiddenLobbyReference(username: String, destination: String): Boolean { + val lobbyMatcher = lobbyDestination.matcher(destination) + if (!lobbyMatcher.matches()) { + return false // no lobby reference is always OK + } + val lobbyId = extractId(lobbyMatcher) + return !isUserInLobby(username, lobbyId) + } + + private fun isUserInLobby(username: String, lobbyId: Int): Boolean { + val lobby = lobbyRepository.find(lobbyId.toLong()) + return lobby.containsUser(username) + } + + companion object { + + private val lobbyDestination = Pattern.compile(".*?/lobby/(?<id>\\d+?)(/.*)?") + + private val gameDestination = Pattern.compile(".*?/game/(?<id>\\d+?)(/.*)?") + + private fun extractId(matcher: Matcher): Int { + val id = matcher.group("id") + return Integer.parseInt(id) + } + } +} diff --git a/backend/src/test/java/org/luxons/sevenwonders/SevenWondersTest.java b/backend/src/test/java/org/luxons/sevenwonders/SevenWondersTest.java deleted file mode 100644 index 8cd959e3..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/SevenWondersTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.luxons.sevenwonders; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.hildan.jackstomp.Channel; -import org.hildan.jackstomp.JackstompSession; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.luxons.sevenwonders.test.api.ApiLobby; -import org.luxons.sevenwonders.test.api.ApiPlayer; -import org.luxons.sevenwonders.test.api.ApiPlayerTurnInfo; -import org.luxons.sevenwonders.test.api.SevenWondersClient; -import org.luxons.sevenwonders.test.api.SevenWondersSession; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -public class SevenWondersTest { - - private static final String WEBSOCKET_URI = "ws://localhost:%d"; - - private SevenWondersClient client; - - @LocalServerPort - private int randomServerPort; - - private String serverUrl; - - @Before - public void setUpClientAndUrl() { - client = new SevenWondersClient(); - serverUrl = String.format(WEBSOCKET_URI, randomServerPort); - } - - private void disconnect(SevenWondersSession... sessions) { - for (SevenWondersSession session : sessions) { - session.disconnect(); - } - } - - @Test - public void chooseName() throws InterruptedException, ExecutionException, TimeoutException { - SevenWondersSession session = client.connect(serverUrl); - String playerName = "Test User"; - ApiPlayer player = session.chooseName(playerName); - assertNotNull(player); - assertEquals(playerName, player.getDisplayName()); - session.disconnect(); - } - - private SevenWondersSession newPlayer(String name) throws InterruptedException, TimeoutException, - ExecutionException { - SevenWondersSession otherSession = client.connect(serverUrl); - otherSession.chooseName(name); - return otherSession; - } - - @Test - public void lobbySubscription_ignoredForOutsiders() throws InterruptedException, ExecutionException, - TimeoutException { - SevenWondersSession ownerSession = newPlayer("GameOwner"); - SevenWondersSession session1 = newPlayer("Player1"); - SevenWondersSession session2 = newPlayer("Player2"); - String gameName = "Test Game"; - ApiLobby lobby = ownerSession.createGame(gameName); - session1.joinGame(lobby.getId()); - session2.joinGame(lobby.getId()); - - SevenWondersSession outsiderSession = newPlayer("Outsider"); - JackstompSession session = outsiderSession.getJackstompSession(); - Channel<Object> started = session.subscribeEmptyMsgs("/topic/lobby/" + lobby.getId() + "/started"); - - ownerSession.startGame(lobby.getId()); - Object nothing = started.next(1, TimeUnit.SECONDS); - assertNull(nothing); - disconnect(ownerSession, session1, session2, outsiderSession); - } - - @Test - public void createGame_success() throws InterruptedException, ExecutionException, TimeoutException { - SevenWondersSession ownerSession = newPlayer("GameOwner"); - - String gameName = "Test Game"; - ApiLobby lobby = ownerSession.createGame(gameName); - assertNotNull(lobby); - assertEquals(gameName, lobby.getName()); - - disconnect(ownerSession); - } - - @Test - public void createGame_seenByConnectedPlayers() throws InterruptedException, ExecutionException, TimeoutException { - SevenWondersSession otherSession = newPlayer("OtherPlayer"); - Channel<ApiLobby[]> games = otherSession.watchGames(); - - ApiLobby[] receivedLobbies = games.next(); - assertNotNull(receivedLobbies); - assertEquals(0, receivedLobbies.length); - - SevenWondersSession ownerSession = newPlayer("GameOwner"); - String gameName = "Test Game"; - ApiLobby createdLobby = ownerSession.createGame(gameName); - - receivedLobbies = games.next(); - assertNotNull(receivedLobbies); - assertEquals(1, receivedLobbies.length); - ApiLobby receivedLobby = receivedLobbies[0]; - assertEquals(createdLobby.getId(), receivedLobby.getId()); - assertEquals(createdLobby.getName(), receivedLobby.getName()); - - disconnect(ownerSession, otherSession); - } - - @Test - public void startGame_3players() throws Exception { - SevenWondersSession session1 = newPlayer("Player1"); - SevenWondersSession session2 = newPlayer("Player2"); - - ApiLobby lobby = session1.createGame("Test Game"); - session2.joinGame(lobby.getId()); - - SevenWondersSession session3 = newPlayer("Player3"); - session3.joinGame(lobby.getId()); - - session1.startGame(lobby.getId()); - - Channel<ApiPlayerTurnInfo> turns1 = session1.watchTurns(); - Channel<ApiPlayerTurnInfo> turns2 = session2.watchTurns(); - Channel<ApiPlayerTurnInfo> turns3 = session3.watchTurns(); - session1.sayReady(); - session2.sayReady(); - session3.sayReady(); - ApiPlayerTurnInfo turn1 = turns1.next(); - ApiPlayerTurnInfo turn2 = turns2.next(); - ApiPlayerTurnInfo turn3 = turns3.next(); - assertNotNull(turn1); - assertNotNull(turn2); - assertNotNull(turn3); - - disconnect(session1, session2, session3); - } - - @After - public void tearDown() { - client.stop(); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.java b/backend/src/test/java/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.java deleted file mode 100644 index 65d2aa04..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; -import java.util.Collection; - -import org.junit.Before; -import org.junit.Test; -import org.luxons.sevenwonders.actions.CreateGameAction; -import org.luxons.sevenwonders.actions.JoinGameAction; -import org.luxons.sevenwonders.controllers.GameBrowserController.UserAlreadyInGameException; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.LobbyRepository; -import org.luxons.sevenwonders.repositories.PlayerRepository; -import org.luxons.sevenwonders.repositories.PlayerRepository.PlayerNotFoundException; -import org.luxons.sevenwonders.test.TestUtils; -import org.springframework.messaging.simp.SimpMessagingTemplate; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -public class GameBrowserControllerTest { - - private PlayerRepository playerRepository; - - private GameBrowserController gameBrowserController; - - @Before - public void setUp() { - playerRepository = new PlayerRepository(); - LobbyRepository lobbyRepository = new LobbyRepository(); - SimpMessagingTemplate template = TestUtils.createSimpMessagingTemplate(); - LobbyController lobbyController = new LobbyController(playerRepository, template); - gameBrowserController = new GameBrowserController(lobbyController, lobbyRepository, playerRepository, template); - } - - @Test - public void listGames_initiallyEmpty() { - Principal principal = new TestPrincipal("testuser"); - Collection<Lobby> games = gameBrowserController.listGames(principal); - assertTrue(games.isEmpty()); - } - - @Test - public void createGame_success() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Principal principal = new TestPrincipal("testuser"); - - CreateGameAction action = new CreateGameAction(); - action.setGameName("Test Game"); - - Lobby createdLobby = gameBrowserController.createGame(action, principal); - - assertEquals("Test Game", createdLobby.getName()); - - Collection<Lobby> games = gameBrowserController.listGames(principal); - assertFalse(games.isEmpty()); - Lobby lobby = games.iterator().next(); - assertSame(lobby, createdLobby); - assertSame(player, lobby.getPlayers().get(0)); - } - - @Test(expected = PlayerNotFoundException.class) - public void createGame_failsForUnknownPlayer() { - Principal principal = new TestPrincipal("unknown"); - - CreateGameAction action = new CreateGameAction(); - action.setGameName("Test Game"); - - gameBrowserController.createGame(action, principal); - } - - @Test(expected = UserAlreadyInGameException.class) - public void createGame_failsWhenAlreadyInGame() { - playerRepository.createOrUpdate("testuser", "Test User"); - Principal principal = new TestPrincipal("testuser"); - - CreateGameAction createGameAction1 = new CreateGameAction(); - createGameAction1.setGameName("Test Game 1"); - - // auto-enters the game - gameBrowserController.createGame(createGameAction1, principal); - - CreateGameAction createGameAction2 = new CreateGameAction(); - createGameAction2.setGameName("Test Game 2"); - - // already in a game - gameBrowserController.createGame(createGameAction2, principal); - } - - @Test - public void joinGame_success() { - Player owner = playerRepository.createOrUpdate("testowner", "Test User Owner"); - Principal ownerPrincipal = new TestPrincipal("testowner"); - CreateGameAction createGameAction = new CreateGameAction(); - createGameAction.setGameName("Test Game"); - - Lobby createdLobby = gameBrowserController.createGame(createGameAction, ownerPrincipal); - - Player joiner = playerRepository.createOrUpdate("testjoiner", "Test User Joiner"); - Principal joinerPrincipal = new TestPrincipal("testjoiner"); - JoinGameAction joinGameAction = new JoinGameAction(); - joinGameAction.setGameId(createdLobby.getId()); - - Lobby joinedLobby = gameBrowserController.joinGame(joinGameAction, joinerPrincipal); - - assertSame(createdLobby, joinedLobby); - assertSame(owner, joinedLobby.getPlayers().get(0)); - assertSame(joiner, joinedLobby.getPlayers().get(1)); - } - - @Test(expected = UserAlreadyInGameException.class) - public void joinGame_failsWhenAlreadyInGame() { - playerRepository.createOrUpdate("testowner", "Test User Owner"); - Principal ownerPrincipal = new TestPrincipal("testowner"); - CreateGameAction createGameAction = new CreateGameAction(); - createGameAction.setGameName("Test Game"); - - Lobby createdLobby = gameBrowserController.createGame(createGameAction, ownerPrincipal); - - playerRepository.createOrUpdate("testjoiner", "Test User Joiner"); - Principal joinerPrincipal = new TestPrincipal("testjoiner"); - JoinGameAction joinGameAction = new JoinGameAction(); - joinGameAction.setGameId(createdLobby.getId()); - - // joins the game - gameBrowserController.joinGame(joinGameAction, joinerPrincipal); - // already in a game - gameBrowserController.joinGame(joinGameAction, joinerPrincipal); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/controllers/HomeControllerTest.java b/backend/src/test/java/org/luxons/sevenwonders/controllers/HomeControllerTest.java deleted file mode 100644 index 3104bf4a..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/controllers/HomeControllerTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; - -import org.junit.Test; -import org.luxons.sevenwonders.actions.ChooseNameAction; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.PlayerRepository; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; - -public class HomeControllerTest { - - @Test - public void chooseName() { - PlayerRepository playerRepository = new PlayerRepository(); - HomeController homeController = new HomeController(playerRepository); - - ChooseNameAction action = new ChooseNameAction(); - action.setPlayerName("Test User"); - - Principal principal = new TestPrincipal("testuser"); - - Player player = homeController.chooseName(action, principal); - - assertSame(player, playerRepository.find("testuser")); - assertEquals("testuser", player.getUsername()); - assertEquals("Test User", player.getDisplayName()); - assertEquals(null, player.getLobby()); - assertEquals(null, player.getGame()); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/controllers/LobbyControllerTest.java b/backend/src/test/java/org/luxons/sevenwonders/controllers/LobbyControllerTest.java deleted file mode 100644 index 2da7e3c1..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/controllers/LobbyControllerTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.Before; -import org.junit.Test; -import org.luxons.sevenwonders.actions.ReorderPlayersAction; -import org.luxons.sevenwonders.actions.UpdateSettingsAction; -import org.luxons.sevenwonders.controllers.LobbyController.PlayerIsNotOwnerException; -import org.luxons.sevenwonders.controllers.LobbyController.PlayerNotInLobbyException; -import org.luxons.sevenwonders.game.api.CustomizableSettings; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.lobby.State; -import org.luxons.sevenwonders.repositories.LobbyRepository; -import org.luxons.sevenwonders.repositories.PlayerRepository; -import org.luxons.sevenwonders.repositories.PlayerRepository.PlayerNotFoundException; -import org.luxons.sevenwonders.test.TestUtils; -import org.springframework.messaging.simp.SimpMessagingTemplate; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.luxons.sevenwonders.game.data.WonderSidePickMethod.ALL_A; - -public class LobbyControllerTest { - - private PlayerRepository playerRepository; - - private LobbyRepository lobbyRepository; - - private LobbyController lobbyController; - - @Before - public void setUp() { - playerRepository = new PlayerRepository(); - lobbyRepository = new LobbyRepository(); - SimpMessagingTemplate template = TestUtils.createSimpMessagingTemplate(); - lobbyController = new LobbyController(playerRepository, template); - } - - @Test(expected = PlayerNotFoundException.class) - public void leave_failsWhenPlayerDoesNotExist() { - Principal principal = new TestPrincipal("testuser"); - lobbyController.leave(principal); - } - - @Test(expected = PlayerNotInLobbyException.class) - public void leave_failsWhenNotInLobby() { - playerRepository.createOrUpdate("testuser", "Test User"); - Principal principal = new TestPrincipal("testuser"); - lobbyController.leave(principal); - } - - @Test - public void leave_succeedsWhenInALobby_asOwner() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - assertTrue(lobby.getPlayers().contains(player)); - assertSame(lobby, player.getLobby()); - - Principal principal = new TestPrincipal("testuser"); - lobbyController.leave(principal); - - assertFalse(lobby.getPlayers().contains(player)); - assertNull(player.getLobby()); - } - - @Test - public void leave_succeedsWhenInALobby_asPeasant() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - Player player2 = addPlayer(lobby, "testuser2"); - - assertTrue(lobby.getPlayers().contains(player2)); - assertSame(lobby, player2.getLobby()); - - Principal principal = new TestPrincipal("testuser2"); - lobbyController.leave(principal); - - assertFalse(lobby.getPlayers().contains(player2)); - assertNull(player2.getLobby()); - } - - @Test - public void reorderPlayers_succeedsForOwner() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - Player player2 = addPlayer(lobby, "testuser2"); - Player player3 = addPlayer(lobby, "testuser3"); - Player player4 = addPlayer(lobby, "testuser4"); - - List<Player> players = Arrays.asList(player, player2, player3, player4); - assertEquals(players, lobby.getPlayers()); - - ReorderPlayersAction reorderPlayersAction = new ReorderPlayersAction(); - List<Player> reorderedPlayers = Arrays.asList(player3, player, player2, player4); - List<String> playerNames = reorderedPlayers.stream().map(Player::getUsername).collect(Collectors.toList()); - reorderPlayersAction.setOrderedPlayers(playerNames); - - Principal principal = new TestPrincipal("testuser"); - lobbyController.reorderPlayers(reorderPlayersAction, principal); - - assertEquals(reorderedPlayers, lobby.getPlayers()); - } - - @Test(expected = PlayerIsNotOwnerException.class) - public void reorderPlayers_failsForPeasant() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - Player player2 = addPlayer(lobby, "testuser2"); - Player player3 = addPlayer(lobby, "testuser3"); - - ReorderPlayersAction reorderPlayersAction = new ReorderPlayersAction(); - List<Player> reorderedPlayers = Arrays.asList(player3, player, player2); - List<String> playerNames = reorderedPlayers.stream().map(Player::getUsername).collect(Collectors.toList()); - reorderPlayersAction.setOrderedPlayers(playerNames); - - Principal principal = new TestPrincipal("testuser2"); - lobbyController.reorderPlayers(reorderPlayersAction, principal); - } - - @Test - public void updateSettings_succeedsForOwner() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - addPlayer(lobby, "testuser2"); - addPlayer(lobby, "testuser3"); - addPlayer(lobby, "testuser4"); - - assertEquals(new CustomizableSettings(), lobby.getSettings()); - - UpdateSettingsAction updateSettingsAction = new UpdateSettingsAction(); - CustomizableSettings newSettings = new CustomizableSettings(12L, 5, ALL_A, 5, 5, 4, 10, 2, new HashMap<>()); - updateSettingsAction.setSettings(newSettings); - - Principal principal = new TestPrincipal("testuser"); - lobbyController.updateSettings(updateSettingsAction, principal); - - assertEquals(newSettings, lobby.getSettings()); - } - - @Test(expected = PlayerIsNotOwnerException.class) - public void updateSettings_failsForPeasant() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - addPlayer(lobby, "testuser2"); - addPlayer(lobby, "testuser3"); - - UpdateSettingsAction updateSettingsAction = new UpdateSettingsAction(); - updateSettingsAction.setSettings(new CustomizableSettings()); - - Principal principal = new TestPrincipal("testuser2"); - lobbyController.updateSettings(updateSettingsAction, principal); - } - - @Test - public void startGame_succeedsForOwner() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - addPlayer(lobby, "testuser2"); - addPlayer(lobby, "testuser3"); - addPlayer(lobby, "testuser4"); - - Principal principal = new TestPrincipal("testuser"); - lobbyController.startGame(principal); - - assertSame(State.PLAYING, lobby.getState()); - } - - @Test(expected = PlayerIsNotOwnerException.class) - public void startGame_failsForPeasant() { - Player player = playerRepository.createOrUpdate("testuser", "Test User"); - Lobby lobby = lobbyRepository.create("Test Game", player); - - addPlayer(lobby, "testuser2"); - addPlayer(lobby, "testuser3"); - - Principal principal = new TestPrincipal("testuser2"); - lobbyController.startGame(principal); - } - - private Player addPlayer(Lobby lobby, String username) { - Player player = playerRepository.createOrUpdate(username, username); - lobby.addPlayer(player); - - assertTrue(lobby.getPlayers().contains(player)); - assertSame(lobby, player.getLobby()); - return player; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/controllers/TestPrincipal.java b/backend/src/test/java/org/luxons/sevenwonders/controllers/TestPrincipal.java deleted file mode 100644 index db601cd2..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/controllers/TestPrincipal.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.luxons.sevenwonders.controllers; - -import java.security.Principal; - -public class TestPrincipal implements Principal { - - private final String name; - - TestPrincipal(String name) { - this.name = name; - } - - @Override - public String getName() { - return name; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/lobby/LobbyTest.java b/backend/src/test/java/org/luxons/sevenwonders/lobby/LobbyTest.java deleted file mode 100644 index 5747eb5a..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/lobby/LobbyTest.java +++ /dev/null @@ -1,225 +0,0 @@ -package org.luxons.sevenwonders.lobby; - -import java.util.Arrays; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.experimental.theories.DataPoints; -import org.junit.experimental.theories.Theories; -import org.junit.experimental.theories.Theory; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.luxons.sevenwonders.game.Game; -import org.luxons.sevenwonders.game.api.CustomizableSettings; -import org.luxons.sevenwonders.game.data.GameDefinition; -import org.luxons.sevenwonders.game.data.GameDefinitionLoader; -import org.luxons.sevenwonders.lobby.Lobby.GameAlreadyStartedException; -import org.luxons.sevenwonders.lobby.Lobby.PlayerNameAlreadyUsedException; -import org.luxons.sevenwonders.lobby.Lobby.PlayerOverflowException; -import org.luxons.sevenwonders.lobby.Lobby.PlayerUnderflowException; -import org.luxons.sevenwonders.lobby.Lobby.UnknownPlayerException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeTrue; - -@RunWith(Theories.class) -public class LobbyTest { - - private static GameDefinition gameDefinition; - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private Player gameOwner; - - private Lobby lobby; - - @DataPoints - public static int[] nbPlayers() { - return new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - } - - @BeforeClass - public static void loadDefinition() { - gameDefinition = new GameDefinitionLoader().getGameDefinition(); - } - - @Before - public void setUp() { - gameOwner = new Player("gameowner", "Game owner"); - lobby = new Lobby(0, "Test Game", gameOwner, gameDefinition); - } - - @Test - public void testId() { - Lobby l = new Lobby(5, "Test Game", gameOwner, gameDefinition); - assertEquals(5, l.getId()); - } - - @Test - public void testName() { - Lobby l = new Lobby(5, "Test Game", gameOwner, gameDefinition); - assertEquals("Test Game", l.getName()); - } - - @Test - public void testOwner() { - gameOwner.setIndex(42); - Lobby l = new Lobby(5, "Test Game", gameOwner, gameDefinition); - assertSame(gameOwner, l.getPlayers().get(0)); - assertSame(l, gameOwner.getLobby()); - assertEquals(0, gameOwner.getIndex()); - } - - @Test - public void isOwner_falseWhenNull() { - assertFalse(lobby.isOwner(null)); - } - - @Test - public void isOwner_falseWhenEmptyString() { - assertFalse(lobby.isOwner("")); - } - - @Test - public void isOwner_falseWhenGarbageString() { - assertFalse(lobby.isOwner("this is garbage")); - } - - @Test - public void isOwner_trueWhenOwnerUsername() { - assertTrue(lobby.isOwner(gameOwner.getUsername())); - } - - @Test - public void isOwner_falseWhenOtherPlayerName() { - Player player = new Player("testuser", "Test User"); - lobby.addPlayer(player); - assertFalse(lobby.isOwner(player.getUsername())); - } - - @Test - public void addPlayer_success() { - Player player = new Player("testuser", "Test User"); - lobby.addPlayer(player); - assertTrue(lobby.containsUser("testuser")); - assertSame(lobby, player.getLobby()); - assertSame(1, player.getIndex()); // the owner is 0 - } - - @Test(expected = PlayerNameAlreadyUsedException.class) - public void addPlayer_failsOnSameName() { - Player player = new Player("testuser", "Test User"); - Player player2 = new Player("testuser2", "Test User"); - lobby.addPlayer(player); - lobby.addPlayer(player2); - } - - @Test(expected = PlayerOverflowException.class) - public void addPlayer_playerOverflowWhenTooMany() { - // the owner + the max number gives an overflow - addPlayers(gameDefinition.getMaxPlayers()); - } - - @Test(expected = GameAlreadyStartedException.class) - public void addPlayer_failWhenGameStarted() { - // total with owner is the minimum - addPlayers(gameDefinition.getMinPlayers() - 1); - lobby.startGame(); - lobby.addPlayer(new Player("soonerNextTime", "The Late Guy")); - } - - private void addPlayers(int nbPlayers) { - for (int i = 0; i < nbPlayers; i++) { - Player player = new Player("testuser" + i, "Test User " + i); - lobby.addPlayer(player); - } - } - - @Test(expected = UnknownPlayerException.class) - public void removePlayer_failsWhenNotPresent() { - lobby.removePlayer("anyname"); - } - - @Test - public void removePlayer_success() { - Player player = new Player("testuser", "Test User"); - lobby.addPlayer(player); - lobby.removePlayer("testuser"); - assertFalse(lobby.containsUser("testuser")); - assertNull(player.getLobby()); - assertNull(player.getGame()); - assertEquals(-1, player.getIndex()); - } - - @Test - public void reorderPlayers_success() { - Player player1 = new Player("testuser1", "Test User 1"); - Player player2 = new Player("testuser2", "Test User 2"); - Player player3 = new Player("testuser3", "Test User 3"); - lobby.addPlayer(player1); - lobby.addPlayer(player2); - lobby.addPlayer(player3); - lobby.reorderPlayers(Arrays.asList("testuser3", "testuser1", "testuser2")); - assertEquals("testuser3", lobby.getPlayers().get(0).getUsername()); - assertEquals("testuser1", lobby.getPlayers().get(1).getUsername()); - assertEquals("testuser2", lobby.getPlayers().get(2).getUsername()); - assertEquals(0, lobby.getPlayers().get(0).getIndex()); - assertEquals(1, lobby.getPlayers().get(1).getIndex()); - assertEquals(2, lobby.getPlayers().get(2).getIndex()); - } - - @Test(expected = UnknownPlayerException.class) - public void reorderPlayers_failsOnUnknownPlayer() { - Player player1 = new Player("testuser1", "Test User 1"); - Player player2 = new Player("testuser2", "Test User 2"); - Player player3 = new Player("testuser3", "Test User 3"); - lobby.addPlayer(player1); - lobby.addPlayer(player2); - lobby.addPlayer(player3); - lobby.reorderPlayers(Arrays.asList("unknown", "testuser1", "testuser2")); - } - - @Theory - public void startGame_failsBelowMinPlayers(int nbPlayers) { - assumeTrue(nbPlayers < gameDefinition.getMinPlayers()); - thrown.expect(PlayerUnderflowException.class); - // there is already the owner - addPlayers(nbPlayers - 1); - lobby.startGame(); - } - - @Theory - public void startGame_succeedsAboveMinPlayers(int nbPlayers) { - assumeTrue(nbPlayers >= gameDefinition.getMinPlayers()); - assumeTrue(nbPlayers <= gameDefinition.getMaxPlayers()); - // there is already the owner - addPlayers(nbPlayers - 1); - Game game = lobby.startGame(); - assertNotNull(game); - lobby.getPlayers().forEach(p -> assertSame(game, p.getGame())); - } - - @Test - public void startGame_switchesState() { - assertTrue(lobby.getState() == State.LOBBY); - // there is already the owner - addPlayers(gameDefinition.getMinPlayers() - 1); - lobby.startGame(); - assertTrue(lobby.getState() == State.PLAYING); - } - - @Test - public void setSettings() { - CustomizableSettings settings = new CustomizableSettings(); - lobby.setSettings(settings); - assertSame(settings, lobby.getSettings()); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java b/backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java deleted file mode 100644 index 444e7aaa..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.luxons.sevenwonders.repositories; - -import org.junit.Before; -import org.junit.Test; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.LobbyRepository.LobbyNotFoundException; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class LobbyRepositoryTest { - - private LobbyRepository repository; - - @Before - public void setUp() { - repository = new LobbyRepository(); - } - - @Test - public void list_initiallyEmpty() { - assertTrue(repository.list().isEmpty()); - } - - @Test - public void list_returnsAllLobbies() { - Player owner = new Player("owner", "The Owner"); - Lobby lobby1 = repository.create("Test Name 1", owner); - Lobby lobby2 = repository.create("Test Name 2", owner); - assertTrue(repository.list().contains(lobby1)); - assertTrue(repository.list().contains(lobby2)); - } - - @Test - public void create_withCorrectOwner() { - Player owner = new Player("owner", "The Owner"); - Lobby lobby = repository.create("Test Name", owner); - assertTrue(lobby.isOwner(owner.getUsername())); - } - - @Test(expected = LobbyNotFoundException.class) - public void find_failsOnUnknownId() { - repository.find(123); - } - - @Test - public void find_returnsTheSameObject() { - Player owner = new Player("owner", "The Owner"); - Lobby lobby1 = repository.create("Test Name 1", owner); - Lobby lobby2 = repository.create("Test Name 2", owner); - assertSame(lobby1, repository.find(lobby1.getId())); - assertSame(lobby2, repository.find(lobby2.getId())); - } - - @Test(expected = LobbyNotFoundException.class) - public void remove_failsOnUnknownId() { - repository.remove(123); - } - - @Test - public void remove_succeeds() { - Player owner = new Player("owner", "The Owner"); - Lobby lobby1 = repository.create("Test Name 1", owner); - assertNotNull(repository.find(lobby1.getId())); - repository.remove(lobby1.getId()); - try { - repository.find(lobby1.getId()); - fail(); // the call to find() should have failed - } catch (LobbyNotFoundException e) { - // the lobby has been properly removed - } - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java b/backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java deleted file mode 100644 index 95fd80fd..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.luxons.sevenwonders.repositories; - -import org.junit.Before; -import org.junit.Test; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.PlayerRepository.PlayerNotFoundException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -public class PlayerRepositoryTest { - - private PlayerRepository repository; - - @Before - public void setUp() { - repository = new PlayerRepository(); - } - - @Test - public void contains_falseIfNoUserAdded() { - assertFalse(repository.contains("anyUsername")); - } - - @Test - public void contains_trueForCreatedPlayer() { - repository.createOrUpdate("player1", "Player 1"); - assertTrue(repository.contains("player1")); - } - - @Test - public void createOrUpdate_createsProperly() { - Player player1 = repository.createOrUpdate("player1", "Player 1"); - assertEquals("player1", player1.getUsername()); - assertEquals("Player 1", player1.getDisplayName()); - } - - @Test - public void createOrUpdate_updatesDisplayName() { - Player player1 = repository.createOrUpdate("player1", "Player 1"); - Player player1Updated = repository.createOrUpdate("player1", "Much Better Name"); - assertSame(player1, player1Updated); - assertEquals("Much Better Name", player1Updated.getDisplayName()); - } - - @Test(expected = PlayerNotFoundException.class) - public void find_failsOnUnknownUsername() { - repository.find("anyUsername"); - } - - @Test - public void find_returnsTheSameObject() { - Player player1 = repository.createOrUpdate("player1", "Player 1"); - Player player2 = repository.createOrUpdate("player2", "Player 2"); - assertSame(player1, repository.find("player1")); - assertSame(player2, repository.find("player2")); - } - - @Test(expected = PlayerNotFoundException.class) - public void remove_failsOnUnknownUsername() { - repository.remove("anyUsername"); - } - - @Test - public void remove_succeeds() { - repository.createOrUpdate("player1", "Player 1"); - assertTrue(repository.contains("player1")); - repository.remove("player1"); - assertFalse(repository.contains("player1")); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/TestUtils.java b/backend/src/test/java/org/luxons/sevenwonders/test/TestUtils.java deleted file mode 100644 index b29f970e..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/TestUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.luxons.sevenwonders.test; - -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.simp.SimpMessagingTemplate; - -public class TestUtils { - - public static SimpMessagingTemplate createSimpMessagingTemplate() { - MessageChannel messageChannel = new MessageChannel() { - @Override - public boolean send(Message<?> message) { - return true; - } - - @Override - public boolean send(Message<?> message, long timeout) { - return true; - } - }; - return new SimpMessagingTemplate(messageChannel); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiBoard.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiBoard.java deleted file mode 100644 index 56a67482..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiBoard.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.luxons.sevenwonders.game.cards.Card; -import org.luxons.sevenwonders.game.effects.SpecialAbility; - -public class ApiBoard { - - private ApiWonder wonder; - - private int playerIndex; - - private List<ApiCard> playedCards; - - private ApiProduction production; - - private ApiProduction publicProduction; - - private ApiScience science; - - private ApiTradingRules tradingRules; - - private ApiMilitary military; - - private Set<SpecialAbility> specialAbilities; - - private Map<Integer, Boolean> consumedFreeCards; - - private Card copiedGuild; - - private int gold; - - private int pointsPer3Gold; - - public ApiWonder getWonder() { - return wonder; - } - - public void setWonder(ApiWonder wonder) { - this.wonder = wonder; - } - - public int getPlayerIndex() { - return playerIndex; - } - - public void setPlayerIndex(int playerIndex) { - this.playerIndex = playerIndex; - } - - public List<ApiCard> getPlayedCards() { - return playedCards; - } - - public void setPlayedCards(List<ApiCard> playedCards) { - this.playedCards = playedCards; - } - - public ApiProduction getProduction() { - return production; - } - - public void setProduction(ApiProduction production) { - this.production = production; - } - - public ApiProduction getPublicProduction() { - return publicProduction; - } - - public void setPublicProduction(ApiProduction publicProduction) { - this.publicProduction = publicProduction; - } - - public ApiScience getScience() { - return science; - } - - public void setScience(ApiScience science) { - this.science = science; - } - - public ApiTradingRules getTradingRules() { - return tradingRules; - } - - public void setTradingRules(ApiTradingRules tradingRules) { - this.tradingRules = tradingRules; - } - - public ApiMilitary getMilitary() { - return military; - } - - public void setMilitary(ApiMilitary military) { - this.military = military; - } - - public Set<SpecialAbility> getSpecialAbilities() { - return specialAbilities; - } - - public void setSpecialAbilities(Set<SpecialAbility> specialAbilities) { - this.specialAbilities = specialAbilities; - } - - public Map<Integer, Boolean> getConsumedFreeCards() { - return consumedFreeCards; - } - - public void setConsumedFreeCards(Map<Integer, Boolean> consumedFreeCards) { - this.consumedFreeCards = consumedFreeCards; - } - - public Card getCopiedGuild() { - return copiedGuild; - } - - public void setCopiedGuild(Card copiedGuild) { - this.copiedGuild = copiedGuild; - } - - public int getGold() { - return gold; - } - - public void setGold(int gold) { - this.gold = gold; - } - - public int getPointsPer3Gold() { - return pointsPer3Gold; - } - - public void setPointsPer3Gold(int pointsPer3Gold) { - this.pointsPer3Gold = pointsPer3Gold; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ApiBoard apiBoard = (ApiBoard) o; - return playerIndex == apiBoard.playerIndex && gold == apiBoard.gold && pointsPer3Gold == apiBoard.pointsPer3Gold - && Objects.equals(wonder, apiBoard.wonder) && Objects.equals(playedCards, apiBoard.playedCards) - && Objects.equals(production, apiBoard.production) && Objects.equals(publicProduction, - apiBoard.publicProduction) && Objects.equals(science, apiBoard.science) && Objects.equals(tradingRules, - apiBoard.tradingRules) && Objects.equals(military, apiBoard.military) && Objects.equals( - specialAbilities, apiBoard.specialAbilities) && Objects.equals(consumedFreeCards, - apiBoard.consumedFreeCards) && Objects.equals(copiedGuild, apiBoard.copiedGuild); - } - - @Override - public int hashCode() { - return Objects.hash(wonder, playerIndex, playedCards, production, publicProduction, science, tradingRules, - military, specialAbilities, consumedFreeCards, copiedGuild, gold, pointsPer3Gold); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiCard.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiCard.java deleted file mode 100644 index f927dd4c..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiCard.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.List; -import java.util.Objects; - -import org.luxons.sevenwonders.game.cards.Color; -import org.luxons.sevenwonders.game.cards.Requirements; - -public class ApiCard { - - private String name; - - private Color color; - - private Requirements requirements; - - private String chainParent; - - private List<String> chainChildren; - - private String image; - - private ApiCardBack back; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Color getColor() { - return color; - } - - public void setColor(Color color) { - this.color = color; - } - - public Requirements getRequirements() { - return requirements; - } - - public void setRequirements(Requirements requirements) { - this.requirements = requirements; - } - - public String getChainParent() { - return chainParent; - } - - public void setChainParent(String chainParent) { - this.chainParent = chainParent; - } - - public List<String> getChainChildren() { - return chainChildren; - } - - public void setChainChildren(List<String> chainChildren) { - this.chainChildren = chainChildren; - } - - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } - - public ApiCardBack getBack() { - return back; - } - - public void setBack(ApiCardBack back) { - this.back = back; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ApiCard apiCard = (ApiCard) o; - return Objects.equals(name, apiCard.name) && color == apiCard.color && Objects.equals(requirements, - apiCard.requirements) && Objects.equals(chainParent, apiCard.chainParent) && Objects.equals( - chainChildren, apiCard.chainChildren) && Objects.equals(image, apiCard.image) && Objects.equals(back, - apiCard.back); - } - - @Override - public int hashCode() { - return Objects.hash(name, color, requirements, chainParent, chainChildren, image, back); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiCardBack.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiCardBack.java deleted file mode 100644 index e0f5a50e..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiCardBack.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -public class ApiCardBack { - - private String image; - - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiEffect.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiEffect.java deleted file mode 100644 index df71365d..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiEffect.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -public class ApiEffect { -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiHandCard.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiHandCard.java deleted file mode 100644 index 0cb143c1..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiHandCard.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -/** - * A card with contextual information relative to the hand it is sitting in. The extra information is especially - * useful because it frees the client from a painful business logic implementation. - */ -public class ApiHandCard { - - private ApiCard card; - - private boolean chainable; - - private boolean free; - - private boolean playable; - - public ApiCard getCard() { - return card; - } - - public void setCard(ApiCard card) { - this.card = card; - } - - public boolean isChainable() { - return chainable; - } - - public void setChainable(boolean chainable) { - this.chainable = chainable; - } - - public boolean isFree() { - return free; - } - - public void setFree(boolean free) { - this.free = free; - } - - public boolean isPlayable() { - return playable; - } - - public void setPlayable(boolean playable) { - this.playable = playable; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiLobby.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiLobby.java deleted file mode 100644 index e17369b1..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiLobby.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.List; -import java.util.Objects; - -import org.luxons.sevenwonders.game.api.CustomizableSettings; -import org.luxons.sevenwonders.lobby.State; - -public class ApiLobby { - - private long id; - - private String name; - - private String owner; - - private List<ApiPlayer> players; - - private CustomizableSettings settings; - - private State state; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getOwner() { - return owner; - } - - public void setOwner(String owner) { - this.owner = owner; - } - - public List<ApiPlayer> getPlayers() { - return players; - } - - public void setPlayers(List<ApiPlayer> players) { - this.players = players; - } - - public CustomizableSettings getSettings() { - return settings; - } - - public void setSettings(CustomizableSettings settings) { - this.settings = settings; - } - - public State getState() { - return state; - } - - public void setState(State state) { - this.state = state; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ApiLobby apiLobby = (ApiLobby) o; - return id == apiLobby.id && Objects.equals(name, apiLobby.name) && Objects.equals(owner, apiLobby.owner) - && Objects.equals(players, apiLobby.players) && Objects.equals(settings, apiLobby.settings) - && state == apiLobby.state; - } - - @Override - public int hashCode() { - return Objects.hash(id, name, owner, players, settings, state); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiMilitary.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiMilitary.java deleted file mode 100644 index 11ba2b1c..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiMilitary.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -public class ApiMilitary { - - private int nbShields = 0; - - private int totalPoints = 0; - - private int nbDefeatTokens = 0; - - public int getNbShields() { - return nbShields; - } - - public void setNbShields(int nbShields) { - this.nbShields = nbShields; - } - - public int getTotalPoints() { - return totalPoints; - } - - public void setTotalPoints(int totalPoints) { - this.totalPoints = totalPoints; - } - - public int getNbDefeatTokens() { - return nbDefeatTokens; - } - - public void setNbDefeatTokens(int nbDefeatTokens) { - this.nbDefeatTokens = nbDefeatTokens; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiPlayer.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiPlayer.java deleted file mode 100644 index 940e410b..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiPlayer.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.Objects; - -public class ApiPlayer { - - private String username; - - private String displayName; - - private int index; - - public String getUsername() { - return username; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public int getIndex() { - return index; - } - - public void setIndex(int index) { - this.index = index; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ApiPlayer apiPlayer = (ApiPlayer) o; - return index == apiPlayer.index && Objects.equals(username, apiPlayer.username) && Objects.equals(displayName, - apiPlayer.displayName); - } - - @Override - public int hashCode() { - return Objects.hash(username, displayName, index); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiPlayerTurnInfo.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiPlayerTurnInfo.java deleted file mode 100644 index ae4d06ad..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiPlayerTurnInfo.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.List; -import java.util.Objects; - -import org.luxons.sevenwonders.game.api.Action; - -public class ApiPlayerTurnInfo { - - private int playerIndex; - - private ApiTable table; - - private int currentAge; - - private Action action; - - private List<ApiHandCard> hand; - - private List<ApiCard> neighbourGuildCards; - - private String message; - - public int getPlayerIndex() { - return playerIndex; - } - - public void setPlayerIndex(int playerIndex) { - this.playerIndex = playerIndex; - } - - public ApiTable getTable() { - return table; - } - - public void setTable(ApiTable table) { - this.table = table; - } - - public int getCurrentAge() { - return currentAge; - } - - public void setCurrentAge(int currentAge) { - this.currentAge = currentAge; - } - - public List<ApiHandCard> getHand() { - return hand; - } - - public void setHand(List<ApiHandCard> hand) { - this.hand = hand; - } - - public List<ApiCard> getNeighbourGuildCards() { - return neighbourGuildCards; - } - - public void setNeighbourGuildCards(List<ApiCard> neighbourGuildCards) { - this.neighbourGuildCards = neighbourGuildCards; - } - - public Action getAction() { - return action; - } - - public void setAction(Action action) { - this.action = action; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ApiPlayerTurnInfo that = (ApiPlayerTurnInfo) o; - return playerIndex == that.playerIndex && currentAge == that.currentAge && Objects.equals(table, that.table) - && action == that.action && Objects.equals(hand, that.hand) && Objects.equals(neighbourGuildCards, - that.neighbourGuildCards) && Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - return Objects.hash(playerIndex, table, currentAge, action, hand, neighbourGuildCards, message); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiProduction.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiProduction.java deleted file mode 100644 index 0c3bf8b3..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiProduction.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.Set; - -import org.luxons.sevenwonders.game.resources.ResourceType; -import org.luxons.sevenwonders.game.resources.Resources; - -public class ApiProduction { - - private Resources fixedResources; - - private Set<Set<ResourceType>> alternativeResources; - - public Resources getFixedResources() { - return fixedResources; - } - - public void setFixedResources(Resources fixedResources) { - this.fixedResources = fixedResources; - } - - public Set<Set<ResourceType>> getAlternativeResources() { - return alternativeResources; - } - - public void setAlternativeResources(Set<Set<ResourceType>> alternativeResources) { - this.alternativeResources = alternativeResources; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiScience.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiScience.java deleted file mode 100644 index b2808c83..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiScience.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.Map; - -import org.luxons.sevenwonders.game.boards.ScienceType; - -public class ApiScience { - - private Map<ScienceType, Integer> quantities; - - private int jokers; - - public Map<ScienceType, Integer> getQuantities() { - return quantities; - } - - public void setQuantities(Map<ScienceType, Integer> quantities) { - this.quantities = quantities; - } - - public int getJokers() { - return jokers; - } - - public void setJokers(int jokers) { - this.jokers = jokers; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiTable.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiTable.java deleted file mode 100644 index d0d991aa..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiTable.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.List; - -import org.luxons.sevenwonders.game.cards.HandRotationDirection; -import org.luxons.sevenwonders.game.moves.Move; - -public class ApiTable { - - private int nbPlayers; - - private List<ApiBoard> boards; - - private int currentAge = 0; - - private HandRotationDirection handRotationDirection; - - private List<Move> lastPlayedMoves; - - private List<ApiCard> neighbourGuildCards; - - public int getNbPlayers() { - return nbPlayers; - } - - public void setNbPlayers(int nbPlayers) { - this.nbPlayers = nbPlayers; - } - - public List<ApiBoard> getBoards() { - return boards; - } - - public void setBoards(List<ApiBoard> boards) { - this.boards = boards; - } - - public int getCurrentAge() { - return currentAge; - } - - public void setCurrentAge(int currentAge) { - this.currentAge = currentAge; - } - - public HandRotationDirection getHandRotationDirection() { - return handRotationDirection; - } - - public void setHandRotationDirection(HandRotationDirection handRotationDirection) { - this.handRotationDirection = handRotationDirection; - } - - public List<Move> getLastPlayedMoves() { - return lastPlayedMoves; - } - - public void setLastPlayedMoves(List<Move> lastPlayedMoves) { - this.lastPlayedMoves = lastPlayedMoves; - } - - public void increaseCurrentAge() { - this.currentAge++; - } - - public List<ApiCard> getNeighbourGuildCards() { - return neighbourGuildCards; - } - - public void setNeighbourGuildCards(List<ApiCard> neighbourGuildCards) { - this.neighbourGuildCards = neighbourGuildCards; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiTradingRules.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiTradingRules.java deleted file mode 100644 index 324b7d0e..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiTradingRules.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.Map; - -import org.luxons.sevenwonders.game.resources.Provider; -import org.luxons.sevenwonders.game.resources.ResourceType; - -public class ApiTradingRules { - - private Map<ResourceType, Map<Provider, Integer>> costs; - - public Map<ResourceType, Map<Provider, Integer>> getCosts() { - return costs; - } - - public void setCosts(Map<ResourceType, Map<Provider, Integer>> costs) { - this.costs = costs; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiWonder.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiWonder.java deleted file mode 100644 index d671b2f7..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiWonder.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.List; - -import org.luxons.sevenwonders.game.resources.ResourceType; - -public class ApiWonder { - - private String name; - - private ResourceType initialResource; - - private List<ApiWonderStage> stages; - - private String image; - - private int nbBbuiltStages; -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiWonderStage.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiWonderStage.java deleted file mode 100644 index 6a6e0884..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/ApiWonderStage.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import org.luxons.sevenwonders.game.cards.CardBack; -import org.luxons.sevenwonders.game.cards.Requirements; - -public class ApiWonderStage { - - private Requirements requirements; - - private CardBack cardBack; - - private boolean built; - - public Requirements getRequirements() { - return requirements; - } - - public void setRequirements(Requirements requirements) { - this.requirements = requirements; - } - - public CardBack getCardBack() { - return cardBack; - } - - public void setCardBack(CardBack cardBack) { - this.cardBack = cardBack; - } - - public boolean isBuilt() { - return built; - } - - public void setBuilt(boolean built) { - this.built = built; - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/SevenWondersClient.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/SevenWondersClient.java deleted file mode 100644 index ade43677..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/SevenWondersClient.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import org.hildan.jackstomp.JackstompClient; -import org.hildan.jackstomp.JackstompSession; - -public class SevenWondersClient { - - private static final String WEBSOCKET_ENDPOINT = "/seven-wonders-websocket"; - - private final JackstompClient client; - - public SevenWondersClient() { - this(new JackstompClient()); - } - - public SevenWondersClient(JackstompClient client) { - this.client = client; - } - - public SevenWondersSession connect(String serverUrl) - throws InterruptedException, ExecutionException, TimeoutException { - JackstompSession session = client.connect(serverUrl + WEBSOCKET_ENDPOINT); - return new SevenWondersSession(session); - } - - public void stop() { - client.stop(); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/test/api/SevenWondersSession.java b/backend/src/test/java/org/luxons/sevenwonders/test/api/SevenWondersSession.java deleted file mode 100644 index b6c36f8a..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/test/api/SevenWondersSession.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.luxons.sevenwonders.test.api; - -import org.hildan.jackstomp.Channel; -import org.hildan.jackstomp.JackstompSession; -import org.luxons.sevenwonders.actions.ChooseNameAction; -import org.luxons.sevenwonders.actions.CreateGameAction; -import org.luxons.sevenwonders.actions.JoinGameAction; -import org.luxons.sevenwonders.errors.UIError; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -public class SevenWondersSession { - - private final JackstompSession session; - - public SevenWondersSession(JackstompSession session) { - this.session = session; - } - - public JackstompSession getJackstompSession() { - return session; - } - - public void disconnect() { - session.disconnect(); - } - - public Channel<UIError> watchErrors() { - return session.subscribe("/user/queue/errors", UIError.class); - } - - public ApiPlayer chooseName(String displayName) throws InterruptedException { - ChooseNameAction action = new ChooseNameAction(); - action.setPlayerName(displayName); - return session.request(action, ApiPlayer.class, "/app/chooseName", "/user/queue/nameChoice"); - } - - public Channel<ApiLobby[]> watchGames() { - return session.subscribe("/topic/games", ApiLobby[].class); - } - - public ApiLobby createGame(String gameName) throws InterruptedException { - CreateGameAction action = new CreateGameAction(); - action.setGameName(gameName); - - return session.request(action, ApiLobby.class, "/app/lobby/create", "/user/queue/lobby/joined"); - } - - public ApiLobby joinGame(long gameId) throws InterruptedException { - JoinGameAction action = new JoinGameAction(); - action.setGameId(gameId); - - ApiLobby lobby = session.request(action, ApiLobby.class, "/app/lobby/join", "/user/queue/lobby/joined"); - assertNotNull(lobby); - assertEquals(gameId, lobby.getId()); - return lobby; - } - - public Channel<ApiLobby> watchLobbyUpdates(long gameId) { - return session.subscribe("/topic/lobby/" + gameId + "/updated", ApiLobby.class); - } - - public Channel<ApiLobby> watchLobbyStart(long gameId) { - return session.subscribe("/topic/lobby/" + gameId + "/started", ApiLobby.class); - } - - public void startGame(long gameId) throws InterruptedException { - String sendDestination = "/app/lobby/startGame"; - String receiveDestination = "/topic/lobby/" + gameId + "/started"; - boolean received = session.request(null, sendDestination, receiveDestination); - assertTrue(received); - } - - public void sayReady() { - session.send("/app/game/sayReady", null); - } - - public Channel<ApiPlayerTurnInfo> watchTurns() { - return session.subscribe("/user/queue/game/turn", ApiPlayerTurnInfo.class); - } -} diff --git a/backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java b/backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java deleted file mode 100644 index 604b927d..00000000 --- a/backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.luxons.sevenwonders.validation; - -import org.junit.Before; -import org.junit.Test; -import org.luxons.sevenwonders.lobby.Lobby; -import org.luxons.sevenwonders.lobby.Player; -import org.luxons.sevenwonders.repositories.LobbyRepository; -import org.luxons.sevenwonders.repositories.LobbyRepository.LobbyNotFoundException; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class DestinationAccessValidatorTest { - - private LobbyRepository lobbyRepository; - - private DestinationAccessValidator destinationAccessValidator; - - @Before - public void setup() { - lobbyRepository = new LobbyRepository(); - destinationAccessValidator = new DestinationAccessValidator(lobbyRepository); - } - - private Lobby createLobby(String gameName, String ownerUsername, String... otherPlayers) { - Player owner = new Player(ownerUsername, ownerUsername); - Lobby lobby = lobbyRepository.create(gameName, owner); - for (String playerName : otherPlayers) { - Player player = new Player(playerName, playerName); - lobby.addPlayer(player); - } - return lobby; - } - - private void createGame(String gameName, String ownerUsername, String... otherPlayers) { - Lobby lobby = createLobby(gameName, ownerUsername, otherPlayers); - lobby.startGame(); - } - - @Test - public void validate_failsOnNullUser() { - assertFalse(destinationAccessValidator.hasAccess(null, "doesNotMatter")); - } - - @Test - public void validate_successWhenNoReference() { - assertTrue(destinationAccessValidator.hasAccess("", "")); - assertTrue(destinationAccessValidator.hasAccess("", "test")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "test")); - } - - @Test - public void validate_successWhenNoRefFollows() { - assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/game/")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/lobby/")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "/game//suffix")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby//suffix")); - } - - @Test - public void validate_successWhenRefIsNotANumber() { - assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/notANumber")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/notANumber")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/game/notANumber")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/lobby/notANumber")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/notANumber/suffix")); - assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/notANumber/suffix")); - } - - @Test(expected = LobbyNotFoundException.class) - public void validate_failWhenNoLobbyExist() { - destinationAccessValidator.hasAccess("", "/lobby/0"); - } - - @Test(expected = LobbyNotFoundException.class) - public void validate_failWhenNoGameExist() { - destinationAccessValidator.hasAccess("", "/game/0"); - } - - @Test(expected = LobbyNotFoundException.class) - public void validate_failWhenReferencedLobbyDoesNotExist() { - createLobby("Test Game", "ownerUser1"); - createLobby("Test Game 2", "ownerUser2"); - destinationAccessValidator.hasAccess("doesNotMatter", "/lobby/3"); - } - - @Test(expected = LobbyNotFoundException.class) - public void validate_failWhenReferencedGameDoesNotExist() { - createGame("Test Game 1", "user1", "user2", "user3"); - createGame("Test Game 2", "user4", "user5", "user6"); - destinationAccessValidator.hasAccess("doesNotMatter", "/game/3"); - } - - @Test - public void validate_failWhenUserIsNotPartOfReferencedLobby() { - createLobby("Test Game", "ownerUser"); - destinationAccessValidator.hasAccess("userNotInLobby", "/lobby/0"); - } - - @Test - public void validate_failWhenUserIsNotPartOfReferencedGame() { - createGame("Test Game", "ownerUser", "otherUser1", "otherUser2"); - destinationAccessValidator.hasAccess("userNotInGame", "/game/0"); - } - - @Test - public void validate_successWhenUserIsOwnerOfReferencedLobby() { - createLobby("Test Game 1", "user1"); - assertTrue(destinationAccessValidator.hasAccess("user1", "/lobby/0")); - createLobby("Test Game 2", "user2"); - assertTrue(destinationAccessValidator.hasAccess("user2", "/lobby/1")); - } - - @Test - public void validate_successWhenUserIsMemberOfReferencedLobby() { - createLobby("Test Game 1", "user1", "user2"); - assertTrue(destinationAccessValidator.hasAccess("user2", "/lobby/0")); - createLobby("Test Game 2", "user3", "user4"); - assertTrue(destinationAccessValidator.hasAccess("user4", "/lobby/1")); - } - - @Test - public void validate_successWhenUserIsOwnerOfReferencedGame() { - createGame("Test Game 1", "owner1", "user2", "user3"); - assertTrue(destinationAccessValidator.hasAccess("owner1", "/game/0")); - createGame("Test Game 2", "owner4", "user5", "user6"); - assertTrue(destinationAccessValidator.hasAccess("owner4", "/game/1")); - } - - @Test - public void validate_successWhenUserIsMemberOfReferencedGame() { - createGame("Test Game 1", "owner1", "user2", "user3"); - assertTrue(destinationAccessValidator.hasAccess("user2", "/game/0")); - createGame("Test Game 2", "owner4", "user5", "user6"); - assertTrue(destinationAccessValidator.hasAccess("user6", "/game/1")); - } - -} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt new file mode 100644 index 00000000..cf6c7ce8 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt @@ -0,0 +1,151 @@ +package org.luxons.sevenwonders + +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.luxons.sevenwonders.test.api.SevenWondersClient +import org.luxons.sevenwonders.test.api.SevenWondersSession +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment +import org.springframework.boot.web.server.LocalServerPort +import org.springframework.test.context.junit4.SpringRunner +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +@RunWith(SpringRunner::class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class SevenWondersTest { + + @LocalServerPort + private val randomServerPort: Int = 0 + + private lateinit var client: SevenWondersClient + + private lateinit var serverUrl: String + + @Before + fun setUpClientAndUrl() { + client = SevenWondersClient() + serverUrl = "ws://localhost:$randomServerPort" + } + + private fun disconnect(vararg sessions: SevenWondersSession) { + for (session in sessions) { + session.disconnect() + } + } + + @Test + @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) + fun chooseName() { + val session = client.connect(serverUrl) + val playerName = "Test User" + val player = session.chooseName(playerName) + assertNotNull(player) + assertEquals(playerName, player.displayName) + session.disconnect() + } + + @Throws(InterruptedException::class, TimeoutException::class, ExecutionException::class) + private fun newPlayer(name: String): SevenWondersSession { + val otherSession = client.connect(serverUrl) + otherSession.chooseName(name) + return otherSession + } + + @Test + @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) + fun lobbySubscription_ignoredForOutsiders() { + val ownerSession = newPlayer("GameOwner") + val session1 = newPlayer("Player1") + val session2 = newPlayer("Player2") + val gameName = "Test Game" + val lobby = ownerSession.createGame(gameName) + session1.joinGame(lobby.id) + session2.joinGame(lobby.id) + + val outsiderSession = newPlayer("Outsider") + val session = outsiderSession.jackstompSession + val started = session.subscribeEmptyMsgs("/topic/lobby/" + lobby.id + "/started") + + ownerSession.startGame(lobby.id) + val nothing = started.next(1, TimeUnit.SECONDS) + assertNull(nothing) + disconnect(ownerSession, session1, session2, outsiderSession) + } + + @Test + @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) + fun createGame_success() { + val ownerSession = newPlayer("GameOwner") + + val gameName = "Test Game" + val lobby = ownerSession.createGame(gameName) + assertNotNull(lobby) + assertEquals(gameName, lobby.name) + + disconnect(ownerSession) + } + + @Test + @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) + fun createGame_seenByConnectedPlayers() { + val otherSession = newPlayer("OtherPlayer") + val games = otherSession.watchGames() + + var receivedLobbies = games.next() + assertNotNull(receivedLobbies) + assertEquals(0, receivedLobbies.size.toLong()) + + val ownerSession = newPlayer("GameOwner") + val gameName = "Test Game" + val createdLobby = ownerSession.createGame(gameName) + + receivedLobbies = games.next() + assertNotNull(receivedLobbies) + assertEquals(1, receivedLobbies.size.toLong()) + val receivedLobby = receivedLobbies[0] + assertEquals(createdLobby.id, receivedLobby.id) + assertEquals(createdLobby.name, receivedLobby.name) + + disconnect(ownerSession, otherSession) + } + + @Test + @Throws(Exception::class) + fun startGame_3players() { + val session1 = newPlayer("Player1") + val session2 = newPlayer("Player2") + + val lobby = session1.createGame("Test Game") + session2.joinGame(lobby.id) + + val session3 = newPlayer("Player3") + session3.joinGame(lobby.id) + + session1.startGame(lobby.id) + + val turns1 = session1.watchTurns() + val turns2 = session2.watchTurns() + val turns3 = session3.watchTurns() + session1.sayReady() + session2.sayReady() + session3.sayReady() + val turn1 = turns1.next() + val turn2 = turns2.next() + val turn3 = turns3.next() + assertNotNull(turn1) + assertNotNull(turn2) + assertNotNull(turn3) + + disconnect(session1, session2, session3) + } + + @After + fun tearDown() { + client.stop() + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt new file mode 100644 index 00000000..96d4bc85 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt @@ -0,0 +1,114 @@ +package org.luxons.sevenwonders.controllers + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.actions.CreateGameAction +import org.luxons.sevenwonders.actions.JoinGameAction +import org.luxons.sevenwonders.controllers.GameBrowserController.UserAlreadyInGameException +import org.luxons.sevenwonders.repositories.LobbyRepository +import org.luxons.sevenwonders.repositories.PlayerNotFoundException +import org.luxons.sevenwonders.repositories.PlayerRepository +import org.luxons.sevenwonders.test.TestUtils + +class GameBrowserControllerTest { + + private lateinit var playerRepository: PlayerRepository + + private lateinit var gameBrowserController: GameBrowserController + + @Before + fun setUp() { + playerRepository = PlayerRepository() + val lobbyRepository = LobbyRepository() + val template = TestUtils.createSimpMessagingTemplate() + val lobbyController = LobbyController(lobbyRepository, playerRepository, template) + gameBrowserController = GameBrowserController(lobbyController, lobbyRepository, playerRepository, template) + } + + @Test + fun listGames_initiallyEmpty() { + val principal = TestPrincipal("testuser") + val games = gameBrowserController.listGames(principal) + assertTrue(games.isEmpty()) + } + + @Test + fun createGame_success() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val principal = TestPrincipal("testuser") + + val action = CreateGameAction("Test Game") + + val createdLobby = gameBrowserController.createGame(action, principal) + + assertEquals("Test Game", createdLobby.name) + + val games = gameBrowserController.listGames(principal) + assertFalse(games.isEmpty()) + val lobby = games.iterator().next() + assertSame(lobby, createdLobby) + assertSame(player, lobby.getPlayers()[0]) + } + + @Test(expected = PlayerNotFoundException::class) + fun createGame_failsForUnknownPlayer() { + val principal = TestPrincipal("unknown") + + val action = CreateGameAction("Test Game") + gameBrowserController.createGame(action, principal) + } + + @Test(expected = UserAlreadyInGameException::class) + fun createGame_failsWhenAlreadyInGame() { + playerRepository.createOrUpdate("testuser", "Test User") + val principal = TestPrincipal("testuser") + + val createGameAction1 = CreateGameAction("Test Game 1") + + // auto-enters the game + gameBrowserController.createGame(createGameAction1, principal) + + val createGameAction2 = CreateGameAction("Test Game 2") + + // already in a game + gameBrowserController.createGame(createGameAction2, principal) + } + + @Test + fun joinGame_success() { + val owner = playerRepository.createOrUpdate("testowner", "Test User Owner") + val ownerPrincipal = TestPrincipal("testowner") + val createGameAction = CreateGameAction("Test Game") + + val createdLobby = gameBrowserController.createGame(createGameAction, ownerPrincipal) + + val joiner = playerRepository.createOrUpdate("testjoiner", "Test User Joiner") + val joinerPrincipal = TestPrincipal("testjoiner") + val joinGameAction = JoinGameAction(createdLobby.id) + + val joinedLobby = gameBrowserController.joinGame(joinGameAction, joinerPrincipal) + + assertSame(createdLobby, joinedLobby) + assertSame(owner, joinedLobby.getPlayers()[0]) + assertSame(joiner, joinedLobby.getPlayers()[1]) + } + + @Test(expected = UserAlreadyInGameException::class) + fun joinGame_failsWhenAlreadyInGame() { + playerRepository.createOrUpdate("testowner", "Test User Owner") + val ownerPrincipal = TestPrincipal("testowner") + val createGameAction = CreateGameAction("Test Game") + + val createdLobby = gameBrowserController.createGame(createGameAction, ownerPrincipal) + + playerRepository.createOrUpdate("testjoiner", "Test User Joiner") + val joinerPrincipal = TestPrincipal("testjoiner") + val joinGameAction = JoinGameAction(createdLobby.id) + + // joins the game + gameBrowserController.joinGame(joinGameAction, joinerPrincipal) + // should fail because already in a game + gameBrowserController.joinGame(joinGameAction, joinerPrincipal) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt new file mode 100644 index 00000000..b11ef878 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt @@ -0,0 +1,26 @@ +package org.luxons.sevenwonders.controllers + +import org.junit.Assert.* +import org.junit.Test +import org.luxons.sevenwonders.actions.ChooseNameAction +import org.luxons.sevenwonders.repositories.PlayerRepository + +class HomeControllerTest { + + @Test + fun chooseName() { + val playerRepository = PlayerRepository() + val homeController = HomeController(playerRepository) + + val action = ChooseNameAction("Test User") + val principal = TestPrincipal("testuser") + + val player = homeController.chooseName(action, principal) + + assertSame(player, playerRepository.find("testuser")) + assertEquals("testuser", player.username) + assertEquals("Test User", player.displayName) + assertFalse(player.isInLobby) + assertFalse(player.isInGame) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt new file mode 100644 index 00000000..6269ae40 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt @@ -0,0 +1,199 @@ +package org.luxons.sevenwonders.controllers + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.actions.ReorderPlayersAction +import org.luxons.sevenwonders.actions.UpdateSettingsAction +import org.luxons.sevenwonders.game.api.CustomizableSettings +import org.luxons.sevenwonders.game.data.WonderSidePickMethod.ALL_A +import org.luxons.sevenwonders.lobby.Lobby +import org.luxons.sevenwonders.lobby.Player +import org.luxons.sevenwonders.lobby.PlayerIsNotOwnerException +import org.luxons.sevenwonders.lobby.PlayerNotInLobbyException +import org.luxons.sevenwonders.lobby.State +import org.luxons.sevenwonders.repositories.LobbyRepository +import org.luxons.sevenwonders.repositories.PlayerNotFoundException +import org.luxons.sevenwonders.repositories.PlayerRepository +import org.luxons.sevenwonders.test.TestUtils +import java.util.Arrays +import java.util.HashMap + +class LobbyControllerTest { + + private lateinit var playerRepository: PlayerRepository + + private lateinit var lobbyRepository: LobbyRepository + + private lateinit var lobbyController: LobbyController + + @Before + fun setUp() { + val template = TestUtils.createSimpMessagingTemplate() + playerRepository = PlayerRepository() + lobbyRepository = LobbyRepository() + lobbyController = LobbyController(lobbyRepository, playerRepository, template) + } + + @Test + fun init_succeeds() { + val owner = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", owner) + + assertTrue(lobby.getPlayers().contains(owner)) + assertSame(lobby, owner.lobby) + assertEquals(owner.username, lobby.owner) + assertTrue(owner.isInLobby) + assertFalse(owner.isInGame) + } + + @Test(expected = PlayerNotFoundException::class) + fun leave_failsWhenPlayerDoesNotExist() { + val principal = TestPrincipal("I don't exist") + lobbyController.leave(principal) + } + + @Test(expected = PlayerNotInLobbyException::class) + fun leave_failsWhenNotInLobby() { + playerRepository.createOrUpdate("testuser", "Test User") + val principal = TestPrincipal("testuser") + lobbyController.leave(principal) + } + + @Test + fun leave_succeedsWhenInALobby_asOwner() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + val principal = TestPrincipal("testuser") + lobbyController.leave(principal) + + assertFalse(lobbyRepository.list().contains(lobby)) + assertFalse(player.isInLobby) + assertFalse(player.isInGame) + } + + @Test + fun leave_succeedsWhenInALobby_asPeasant() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + val player2 = addPlayer(lobby, "testuser2") + + val principal = TestPrincipal("testuser2") + lobbyController.leave(principal) + + assertFalse(lobby.getPlayers().contains(player2)) + assertFalse(player2.isInLobby) + assertFalse(player2.isInGame) + } + + @Test + fun reorderPlayers_succeedsForOwner() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + val player2 = addPlayer(lobby, "testuser2") + val player3 = addPlayer(lobby, "testuser3") + val player4 = addPlayer(lobby, "testuser4") + + val players = Arrays.asList(player, player2, player3, player4) + assertEquals(players, lobby.getPlayers()) + + val reorderedPlayers = Arrays.asList(player3, player, player2, player4) + val playerNames = reorderedPlayers.map { it.username } + val reorderPlayersAction = ReorderPlayersAction(playerNames) + + val principal = TestPrincipal("testuser") + lobbyController.reorderPlayers(reorderPlayersAction, principal) + + assertEquals(reorderedPlayers, lobby.getPlayers()) + } + + @Test(expected = PlayerIsNotOwnerException::class) + fun reorderPlayers_failsForPeasant() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + val player2 = addPlayer(lobby, "testuser2") + val player3 = addPlayer(lobby, "testuser3") + + val reorderedPlayers = Arrays.asList(player3, player, player2) + val playerNames = reorderedPlayers.map { it.username } + val reorderPlayersAction = ReorderPlayersAction(playerNames) + + val principal = TestPrincipal("testuser2") + lobbyController.reorderPlayers(reorderPlayersAction, principal) + } + + @Test + fun updateSettings_succeedsForOwner() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + addPlayer(lobby, "testuser2") + addPlayer(lobby, "testuser3") + addPlayer(lobby, "testuser4") + + assertEquals(CustomizableSettings(), lobby.settings) + + val newSettings = CustomizableSettings(12L, 5, ALL_A, 5, 5, 4, 10, 2, HashMap()) + val updateSettingsAction = UpdateSettingsAction(newSettings) + + val principal = TestPrincipal("testuser") + lobbyController.updateSettings(updateSettingsAction, principal) + + assertEquals(newSettings, lobby.settings) + } + + @Test(expected = PlayerIsNotOwnerException::class) + fun updateSettings_failsForPeasant() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + addPlayer(lobby, "testuser2") + addPlayer(lobby, "testuser3") + + val updateSettingsAction = UpdateSettingsAction(CustomizableSettings()) + + val principal = TestPrincipal("testuser2") + lobbyController.updateSettings(updateSettingsAction, principal) + } + + @Test + fun startGame_succeedsForOwner() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + addPlayer(lobby, "testuser2") + addPlayer(lobby, "testuser3") + addPlayer(lobby, "testuser4") + + val principal = TestPrincipal("testuser") + lobbyController.startGame(principal) + + assertSame(State.PLAYING, lobby.state) + } + + @Test(expected = PlayerIsNotOwnerException::class) + fun startGame_failsForPeasant() { + val player = playerRepository.createOrUpdate("testuser", "Test User") + val lobby = lobbyRepository.create("Test Game", player) + + addPlayer(lobby, "testuser2") + addPlayer(lobby, "testuser3") + + val principal = TestPrincipal("testuser2") + lobbyController.startGame(principal) + } + + private fun addPlayer(lobby: Lobby, username: String): Player { + val player = playerRepository.createOrUpdate(username, username) + lobby.addPlayer(player) + + assertTrue(lobby.getPlayers().contains(player)) + assertSame(lobby, player.lobby) + assertTrue(player.isInLobby) + assertFalse(player.isInGame) + return player + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt new file mode 100644 index 00000000..ce3cf317 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt @@ -0,0 +1,10 @@ +package org.luxons.sevenwonders.controllers + +import java.security.Principal + +internal class TestPrincipal(private val name: String) : Principal { + + override fun getName(): String { + return name + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt new file mode 100644 index 00000000..3abdde3b --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt @@ -0,0 +1,229 @@ +package org.luxons.sevenwonders.lobby + +import org.junit.Assert.* +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.experimental.theories.DataPoints +import org.junit.experimental.theories.Theories +import org.junit.experimental.theories.Theory +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import org.luxons.sevenwonders.game.api.CustomizableSettings +import org.luxons.sevenwonders.game.data.GameDefinition +import org.luxons.sevenwonders.game.data.GameDefinitionLoader +import org.luxons.sevenwonders.lobby.Lobby.* +import java.util.Arrays + +@RunWith(Theories::class) +class LobbyTest { + + @JvmField + @Rule + var thrown = ExpectedException.none() + + private lateinit var gameOwner: Player + + private lateinit var lobby: Lobby + + @Before + fun setUp() { + gameOwner = Player("gameowner", "Game owner") + lobby = Lobby(0, "Test Game", gameOwner, gameDefinition) + } + + @Test + fun testId() { + val lobby = Lobby(5, "Test Game", gameOwner, gameDefinition) + assertEquals(5, lobby.id) + } + + @Test + fun testName() { + val lobby = Lobby(5, "Test Game", gameOwner, gameDefinition) + assertEquals("Test Game", lobby.name) + } + + @Test + fun testOwner() { + val lobby = Lobby(5, "Test Game", gameOwner, gameDefinition) + assertSame(gameOwner, lobby.getPlayers()[0]) + assertSame(lobby, gameOwner.lobby) + } + + @Test + fun isOwner_falseWhenNull() { + assertFalse(lobby.isOwner(null)) + } + + @Test + fun isOwner_falseWhenEmptyString() { + assertFalse(lobby.isOwner("")) + } + + @Test + fun isOwner_falseWhenGarbageString() { + assertFalse(lobby.isOwner("this is garbage")) + } + + @Test + fun isOwner_trueWhenOwnerUsername() { + assertTrue(lobby.isOwner(gameOwner.username)) + } + + @Test + fun isOwner_falseWhenOtherPlayerName() { + val player = Player("testuser", "Test User") + lobby.addPlayer(player) + assertFalse(lobby.isOwner(player.username)) + } + + @Test + fun addPlayer_success() { + val player = Player("testuser", "Test User") + lobby.addPlayer(player) + assertTrue(lobby.containsUser("testuser")) + assertSame(lobby, player.lobby) + } + + @Test(expected = PlayerNameAlreadyUsedException::class) + fun addPlayer_failsOnSameName() { + val player = Player("testuser", "Test User") + val player2 = Player("testuser2", "Test User") + lobby.addPlayer(player) + lobby.addPlayer(player2) + } + + @Test(expected = PlayerOverflowException::class) + fun addPlayer_playerOverflowWhenTooMany() { + // the owner + the max number gives an overflow + addPlayers(gameDefinition.maxPlayers) + } + + @Test(expected = GameAlreadyStartedException::class) + fun addPlayer_failWhenGameStarted() { + // total with owner is the minimum + addPlayers(gameDefinition.minPlayers - 1) + lobby.startGame() + lobby.addPlayer(Player("soonerNextTime", "The Late Guy")) + } + + private fun addPlayers(nbPlayers: Int) { + for (i in 0 until nbPlayers) { + val player = Player("testuser$i", "Test User $i") + lobby.addPlayer(player) + } + } + + @Test(expected = UnknownPlayerException::class) + fun removePlayer_failsWhenNotPresent() { + lobby.removePlayer("anyname") + } + + @Test + fun removePlayer_success() { + val player = Player("testuser", "Test User") + lobby.addPlayer(player) + assertTrue(player.isInLobby) + assertFalse(player.isInGame) + + lobby.removePlayer("testuser") + assertFalse(lobby.containsUser("testuser")) + assertFalse(player.isInLobby) + assertFalse(player.isInGame) + } + + @Test + fun reorderPlayers_success() { + val player1 = Player("testuser1", "Test User 1") + val player2 = Player("testuser2", "Test User 2") + val player3 = Player("testuser3", "Test User 3") + lobby.addPlayer(player1) + lobby.addPlayer(player2) + lobby.addPlayer(player3) + lobby.reorderPlayers(Arrays.asList("testuser3", "testuser1", "testuser2")) + assertEquals("testuser3", lobby.getPlayers()[0].username) + assertEquals("testuser1", lobby.getPlayers()[1].username) + assertEquals("testuser2", lobby.getPlayers()[2].username) + } + + @Test(expected = UnknownPlayerException::class) + fun reorderPlayers_failsOnUnknownPlayer() { + val player1 = Player("testuser1", "Test User 1") + val player2 = Player("testuser2", "Test User 2") + val player3 = Player("testuser3", "Test User 3") + lobby.addPlayer(player1) + lobby.addPlayer(player2) + lobby.addPlayer(player3) + lobby.reorderPlayers(Arrays.asList("unknown", "testuser1", "testuser2")) + } + + @Theory + fun startGame_failsBelowMinPlayers(nbPlayers: Int) { + assumeTrue(nbPlayers < gameDefinition.minPlayers) + thrown.expect(PlayerUnderflowException::class.java) + // there is already the owner + addPlayers(nbPlayers - 1) + lobby.startGame() + } + + @Theory + fun startGame_succeedsAboveMinPlayers(nbPlayers: Int) { + assumeTrue(nbPlayers >= gameDefinition.minPlayers) + assumeTrue(nbPlayers <= gameDefinition.maxPlayers) + // there is already the owner + addPlayers(nbPlayers - 1) + + assertEquals(nbPlayers, lobby.getPlayers().size) + lobby.getPlayers().forEach { + assertSame(lobby, it.lobby) + assertTrue(it.isInLobby) + assertFalse(it.isInGame) + } + + val game = lobby.startGame() + assertNotNull(game) + lobby.getPlayers().forEachIndexed { index, it -> + assertSame(index, it.index) + assertSame(lobby, it.lobby) + assertSame(game, it.game) + assertTrue(it.isInLobby) + assertTrue(it.isInGame) + } + } + + @Test + fun startGame_switchesState() { + assertTrue(lobby.state === State.LOBBY) + // there is already the owner + addPlayers(gameDefinition.minPlayers - 1) + lobby.startGame() + assertTrue(lobby.state === State.PLAYING) + } + + @Test + fun setSettings() { + val settings = CustomizableSettings() + lobby.settings = settings + assertSame(settings, lobby.settings) + } + + companion object { + + private lateinit var gameDefinition: GameDefinition + + @JvmStatic + @DataPoints + fun nbPlayers(): IntArray { + return intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + } + + @JvmStatic + @BeforeClass + fun loadDefinition() { + gameDefinition = GameDefinitionLoader().gameDefinition + } + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt new file mode 100644 index 00000000..a5009d2a --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt @@ -0,0 +1,70 @@ +package org.luxons.sevenwonders.repositories + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.lobby.Player + +class LobbyRepositoryTest { + + private lateinit var repository: LobbyRepository + + @Before + fun setUp() { + repository = LobbyRepository() + } + + @Test + fun list_initiallyEmpty() { + assertTrue(repository.list().isEmpty()) + } + + @Test + fun list_returnsAllLobbies() { + val owner = Player("owner", "The Owner") + val lobby1 = repository.create("Test Name 1", owner) + val lobby2 = repository.create("Test Name 2", owner) + assertTrue(repository.list().contains(lobby1)) + assertTrue(repository.list().contains(lobby2)) + } + + @Test + fun create_withCorrectOwner() { + val owner = Player("owner", "The Owner") + val lobby = repository.create("Test Name", owner) + assertTrue(lobby.isOwner(owner.username)) + } + + @Test(expected = LobbyNotFoundException::class) + fun find_failsOnUnknownId() { + repository.find(123) + } + + @Test + fun find_returnsTheSameObject() { + val owner = Player("owner", "The Owner") + val lobby1 = repository.create("Test Name 1", owner) + val lobby2 = repository.create("Test Name 2", owner) + assertSame(lobby1, repository.find(lobby1.id)) + assertSame(lobby2, repository.find(lobby2.id)) + } + + @Test(expected = LobbyNotFoundException::class) + fun remove_failsOnUnknownId() { + repository.remove(123) + } + + @Test + fun remove_succeeds() { + val owner = Player("owner", "The Owner") + val lobby1 = repository.create("Test Name 1", owner) + assertNotNull(repository.find(lobby1.id)) + repository.remove(lobby1.id) + try { + repository.find(lobby1.id) + fail() // the call to find() should have failed + } catch (e: LobbyNotFoundException) { + // the lobby has been properly removed + } + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt new file mode 100644 index 00000000..64e3d8fd --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt @@ -0,0 +1,67 @@ +package org.luxons.sevenwonders.repositories + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class PlayerRepositoryTest { + + private lateinit var repository: PlayerRepository + + @Before + fun setUp() { + repository = PlayerRepository() + } + + @Test + fun contains_falseIfNoUserAdded() { + assertFalse(repository.contains("anyUsername")) + } + + @Test + fun contains_trueForCreatedPlayer() { + repository.createOrUpdate("player1", "Player 1") + assertTrue(repository.contains("player1")) + } + + @Test + fun createOrUpdate_createsProperly() { + val player1 = repository.createOrUpdate("player1", "Player 1") + assertEquals("player1", player1.username) + assertEquals("Player 1", player1.displayName) + } + + @Test + fun createOrUpdate_updatesDisplayName() { + val player1 = repository.createOrUpdate("player1", "Player 1") + val player1Updated = repository.createOrUpdate("player1", "Much Better Name") + assertSame(player1, player1Updated) + assertEquals("Much Better Name", player1Updated.displayName) + } + + @Test(expected = PlayerNotFoundException::class) + fun find_failsOnUnknownUsername() { + repository.find("anyUsername") + } + + @Test + fun find_returnsTheSameObject() { + val player1 = repository.createOrUpdate("player1", "Player 1") + val player2 = repository.createOrUpdate("player2", "Player 2") + assertSame(player1, repository.find("player1")) + assertSame(player2, repository.find("player2")) + } + + @Test(expected = PlayerNotFoundException::class) + fun remove_failsOnUnknownUsername() { + repository.remove("anyUsername") + } + + @Test + fun remove_succeeds() { + repository.createOrUpdate("player1", "Player 1") + assertTrue(repository.contains("player1")) + repository.remove("player1") + assertFalse(repository.contains("player1")) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt new file mode 100644 index 00000000..51db8d91 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt @@ -0,0 +1,21 @@ +package org.luxons.sevenwonders.test + +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.simp.SimpMessagingTemplate + +object TestUtils { + + fun createSimpMessagingTemplate(): SimpMessagingTemplate { + val messageChannel = object : MessageChannel { + override fun send(message: Message<*>): Boolean { + return true + } + + override fun send(message: Message<*>, timeout: Long): Boolean { + return true + } + } + return SimpMessagingTemplate(messageChannel) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiBoard.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiBoard.kt new file mode 100644 index 00000000..a3d54773 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiBoard.kt @@ -0,0 +1,64 @@ +package org.luxons.sevenwonders.test.api + +import java.util.Objects + +import org.luxons.sevenwonders.game.cards.Card +import org.luxons.sevenwonders.game.effects.SpecialAbility + +class ApiBoard { + + var wonder: ApiWonder? = null + + var playerIndex: Int = 0 + + var playedCards: List<ApiCard>? = null + + var production: ApiProduction? = null + + var publicProduction: ApiProduction? = null + + var science: ApiScience? = null + + var tradingRules: ApiTradingRules? = null + + var military: ApiMilitary? = null + + var specialAbilities: Set<SpecialAbility>? = null + + var consumedFreeCards: Map<Int, Boolean>? = null + + var copiedGuild: Card? = null + + var gold: Int = 0 + + var pointsPer3Gold: Int = 0 + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val apiBoard = o as ApiBoard? + return (playerIndex == apiBoard!!.playerIndex && gold == apiBoard.gold && pointsPer3Gold == apiBoard.pointsPer3Gold && wonder == apiBoard.wonder && playedCards == apiBoard.playedCards && production == apiBoard.production && publicProduction == apiBoard.publicProduction && science == apiBoard.science && tradingRules == apiBoard.tradingRules && military == apiBoard.military && specialAbilities == apiBoard.specialAbilities && consumedFreeCards == apiBoard.consumedFreeCards && copiedGuild == apiBoard.copiedGuild) + } + + override fun hashCode(): Int { + return Objects.hash( + wonder, + playerIndex, + playedCards, + production, + publicProduction, + science, + tradingRules, + military, + specialAbilities, + consumedFreeCards, + copiedGuild, + gold, + pointsPer3Gold + ) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiCard.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiCard.kt new file mode 100644 index 00000000..2f473367 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiCard.kt @@ -0,0 +1,38 @@ +package org.luxons.sevenwonders.test.api + +import java.util.Objects + +import org.luxons.sevenwonders.game.cards.Color +import org.luxons.sevenwonders.game.cards.Requirements + +class ApiCard { + + var name: String? = null + + var color: Color? = null + + var requirements: Requirements? = null + + var chainParent: String? = null + + var chainChildren: List<String>? = null + + var image: String? = null + + var back: ApiCardBack? = null + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val apiCard = o as ApiCard? + return name == apiCard!!.name && color === apiCard.color && requirements == apiCard.requirements && chainParent == apiCard.chainParent && chainChildren == apiCard.chainChildren && image == apiCard.image && back == apiCard.back + } + + override fun hashCode(): Int { + return Objects.hash(name, color, requirements, chainParent, chainChildren, image, back) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiCardBack.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiCardBack.kt new file mode 100644 index 00000000..15bfd009 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiCardBack.kt @@ -0,0 +1,6 @@ +package org.luxons.sevenwonders.test.api + +class ApiCardBack { + + var image: String? = null +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiEffect.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiEffect.kt new file mode 100644 index 00000000..afe8809f --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiEffect.kt @@ -0,0 +1,3 @@ +package org.luxons.sevenwonders.test.api + +class ApiEffect diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiHandCard.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiHandCard.kt new file mode 100644 index 00000000..003c3783 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiHandCard.kt @@ -0,0 +1,16 @@ +package org.luxons.sevenwonders.test.api + +/** + * A card with contextual information relative to the hand it is sitting in. The extra information is especially + * useful because it frees the client from a painful business logic implementation. + */ +class ApiHandCard { + + var card: ApiCard? = null + + var isChainable: Boolean = false + + var isFree: Boolean = false + + var isPlayable: Boolean = false +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiLobby.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiLobby.kt new file mode 100644 index 00000000..5bc4dad6 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiLobby.kt @@ -0,0 +1,36 @@ +package org.luxons.sevenwonders.test.api + +import java.util.Objects + +import org.luxons.sevenwonders.game.api.CustomizableSettings +import org.luxons.sevenwonders.lobby.State + +class ApiLobby { + + var id: Long = 0 + + var name: String? = null + + var owner: String? = null + + var players: List<ApiPlayer>? = null + + var settings: CustomizableSettings? = null + + var state: State? = null + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val apiLobby = o as ApiLobby? + return (id == apiLobby!!.id && name == apiLobby.name && owner == apiLobby.owner && players == apiLobby.players && settings == apiLobby.settings && state === apiLobby.state) + } + + override fun hashCode(): Int { + return Objects.hash(id, name, owner, players, settings, state) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiMilitary.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiMilitary.kt new file mode 100644 index 00000000..23e4f34c --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiMilitary.kt @@ -0,0 +1,10 @@ +package org.luxons.sevenwonders.test.api + +class ApiMilitary { + + var nbShields = 0 + + var totalPoints = 0 + + var nbDefeatTokens = 0 +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiPlayer.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiPlayer.kt new file mode 100644 index 00000000..fc4c01fb --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiPlayer.kt @@ -0,0 +1,27 @@ +package org.luxons.sevenwonders.test.api + +import java.util.Objects + +class ApiPlayer { + + val username: String? = null + + var displayName: String? = null + + var index: Int = 0 + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val apiPlayer = o as ApiPlayer? + return index == apiPlayer!!.index && username == apiPlayer.username && displayName == apiPlayer.displayName + } + + override fun hashCode(): Int { + return Objects.hash(username, displayName, index) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiPlayerTurnInfo.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiPlayerTurnInfo.kt new file mode 100644 index 00000000..ae22cdb6 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiPlayerTurnInfo.kt @@ -0,0 +1,37 @@ +package org.luxons.sevenwonders.test.api + +import java.util.Objects + +import org.luxons.sevenwonders.game.api.Action + +class ApiPlayerTurnInfo { + + var playerIndex: Int = 0 + + var table: ApiTable? = null + + var currentAge: Int = 0 + + var action: Action? = null + + var hand: List<ApiHandCard>? = null + + var neighbourGuildCards: List<ApiCard>? = null + + var message: String? = null + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val that = o as ApiPlayerTurnInfo? + return (playerIndex == that!!.playerIndex && currentAge == that.currentAge && table == that.table && action === that.action && hand == that.hand && neighbourGuildCards == that.neighbourGuildCards && message == that.message) + } + + override fun hashCode(): Int { + return Objects.hash(playerIndex, table, currentAge, action, hand, neighbourGuildCards, message) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiProduction.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiProduction.kt new file mode 100644 index 00000000..2ab7c343 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiProduction.kt @@ -0,0 +1,11 @@ +package org.luxons.sevenwonders.test.api + +import org.luxons.sevenwonders.game.resources.ResourceType +import org.luxons.sevenwonders.game.resources.Resources + +class ApiProduction { + + var fixedResources: Resources? = null + + var alternativeResources: Set<Set<ResourceType>>? = null +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiScience.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiScience.kt new file mode 100644 index 00000000..5e619904 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiScience.kt @@ -0,0 +1,10 @@ +package org.luxons.sevenwonders.test.api + +import org.luxons.sevenwonders.game.boards.ScienceType + +class ApiScience { + + var quantities: Map<ScienceType, Int>? = null + + var jokers: Int = 0 +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiTable.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiTable.kt new file mode 100644 index 00000000..cffbdf2f --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiTable.kt @@ -0,0 +1,23 @@ +package org.luxons.sevenwonders.test.api + +import org.luxons.sevenwonders.game.cards.HandRotationDirection +import org.luxons.sevenwonders.game.moves.Move + +class ApiTable { + + var nbPlayers: Int = 0 + + var boards: List<ApiBoard>? = null + + var currentAge = 0 + + var handRotationDirection: HandRotationDirection? = null + + var lastPlayedMoves: List<Move>? = null + + var neighbourGuildCards: List<ApiCard>? = null + + fun increaseCurrentAge() { + this.currentAge++ + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiTradingRules.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiTradingRules.kt new file mode 100644 index 00000000..fcebe59e --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiTradingRules.kt @@ -0,0 +1,9 @@ +package org.luxons.sevenwonders.test.api + +import org.luxons.sevenwonders.game.resources.Provider +import org.luxons.sevenwonders.game.resources.ResourceType + +class ApiTradingRules { + + var costs: Map<ResourceType, Map<Provider, Int>>? = null +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiWonder.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiWonder.kt new file mode 100644 index 00000000..1efe7917 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiWonder.kt @@ -0,0 +1,16 @@ +package org.luxons.sevenwonders.test.api + +import org.luxons.sevenwonders.game.resources.ResourceType + +class ApiWonder { + + private val name: String? = null + + private val initialResource: ResourceType? = null + + private val stages: List<ApiWonderStage>? = null + + private val image: String? = null + + private val nbBbuiltStages: Int = 0 +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiWonderStage.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiWonderStage.kt new file mode 100644 index 00000000..41925a55 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/ApiWonderStage.kt @@ -0,0 +1,13 @@ +package org.luxons.sevenwonders.test.api + +import org.luxons.sevenwonders.game.cards.CardBack +import org.luxons.sevenwonders.game.cards.Requirements + +class ApiWonderStage { + + var requirements: Requirements? = null + + var cardBack: CardBack? = null + + var isBuilt: Boolean = false +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt new file mode 100644 index 00000000..d98e932e --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt @@ -0,0 +1,25 @@ +package org.luxons.sevenwonders.test.api + +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException + +import org.hildan.jackstomp.JackstompClient +import org.hildan.jackstomp.JackstompSession + +class SevenWondersClient @JvmOverloads constructor(private val client: JackstompClient = JackstompClient()) { + + @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) + fun connect(serverUrl: String): SevenWondersSession { + val session = client.connect(serverUrl + WEBSOCKET_ENDPOINT) + return SevenWondersSession(session) + } + + fun stop() { + client.stop() + } + + companion object { + + private val WEBSOCKET_ENDPOINT = "/seven-wonders-websocket" + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt new file mode 100644 index 00000000..90b40cb0 --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt @@ -0,0 +1,70 @@ +package org.luxons.sevenwonders.test.api + +import org.hildan.jackstomp.Channel +import org.hildan.jackstomp.JackstompSession +import org.junit.Assert.* +import org.luxons.sevenwonders.actions.ChooseNameAction +import org.luxons.sevenwonders.actions.CreateGameAction +import org.luxons.sevenwonders.actions.JoinGameAction +import org.luxons.sevenwonders.errors.ErrorDTO + +class SevenWondersSession(val jackstompSession: JackstompSession) { + + fun disconnect() { + jackstompSession.disconnect() + } + + fun watchErrors(): Channel<ErrorDTO> { + return jackstompSession.subscribe("/user/queue/errors", ErrorDTO::class.java) + } + + @Throws(InterruptedException::class) + fun chooseName(displayName: String): ApiPlayer { + val action = ChooseNameAction(displayName) + return jackstompSession.request(action, ApiPlayer::class.java, "/app/chooseName", "/user/queue/nameChoice") + } + + fun watchGames(): Channel<Array<ApiLobby>> { + return jackstompSession.subscribe("/topic/games", Array<ApiLobby>::class.java) + } + + @Throws(InterruptedException::class) + fun createGame(gameName: String): ApiLobby { + val action = CreateGameAction(gameName) + return jackstompSession.request(action, ApiLobby::class.java, "/app/lobby/create", "/user/queue/lobby/joined") + } + + @Throws(InterruptedException::class) + fun joinGame(gameId: Long): ApiLobby { + val action = JoinGameAction(gameId) + val lobby = + jackstompSession.request(action, ApiLobby::class.java, "/app/lobby/join", "/user/queue/lobby/joined") + assertNotNull(lobby) + assertEquals(gameId, lobby.id) + return lobby + } + + fun watchLobbyUpdates(gameId: Long): Channel<ApiLobby> { + return jackstompSession.subscribe("/topic/lobby/$gameId/updated", ApiLobby::class.java) + } + + fun watchLobbyStart(gameId: Long): Channel<ApiLobby> { + return jackstompSession.subscribe("/topic/lobby/$gameId/started", ApiLobby::class.java) + } + + @Throws(InterruptedException::class) + fun startGame(gameId: Long) { + val sendDestination = "/app/lobby/startGame" + val receiveDestination = "/topic/lobby/$gameId/started" + val received = jackstompSession.request(null, sendDestination, receiveDestination) + assertTrue(received) + } + + fun sayReady() { + jackstompSession.send("/app/game/sayReady", "") + } + + fun watchTurns(): Channel<ApiPlayerTurnInfo> { + return jackstompSession.subscribe("/user/queue/game/turn", ApiPlayerTurnInfo::class.java) + } +} diff --git a/backend/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt b/backend/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt new file mode 100644 index 00000000..45e58b7f --- /dev/null +++ b/backend/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt @@ -0,0 +1,138 @@ +package org.luxons.sevenwonders.validation + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.lobby.Lobby +import org.luxons.sevenwonders.lobby.Player +import org.luxons.sevenwonders.repositories.LobbyNotFoundException +import org.luxons.sevenwonders.repositories.LobbyRepository + +class DestinationAccessValidatorTest { + + private lateinit var lobbyRepository: LobbyRepository + + private lateinit var destinationAccessValidator: DestinationAccessValidator + + @Before + fun setup() { + lobbyRepository = LobbyRepository() + destinationAccessValidator = DestinationAccessValidator(lobbyRepository) + } + + private fun createLobby(gameName: String, ownerUsername: String, vararg otherPlayers: String): Lobby { + val owner = Player(ownerUsername, ownerUsername) + val lobby = lobbyRepository.create(gameName, owner) + for (playerName in otherPlayers) { + val player = Player(playerName, playerName) + lobby.addPlayer(player) + } + return lobby + } + + private fun createGame(gameName: String, ownerUsername: String, vararg otherPlayers: String) { + val lobby = createLobby(gameName, ownerUsername, *otherPlayers) + lobby.startGame() + } + + @Test + fun validate_failsOnNullUser() { + assertFalse(destinationAccessValidator.hasAccess(null, "doesNotMatter")) + } + + @Test + fun validate_successWhenNoReference() { + assertTrue(destinationAccessValidator.hasAccess("", "")) + assertTrue(destinationAccessValidator.hasAccess("", "test")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "test")) + } + + @Test + fun validate_successWhenNoRefFollows() { + assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/game/")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/lobby/")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "/game//suffix")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby//suffix")) + } + + @Test + fun validate_successWhenRefIsNotANumber() { + assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/notANumber")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/notANumber")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/game/notANumber")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/lobby/notANumber")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/notANumber/suffix")) + assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/notANumber/suffix")) + } + + @Test(expected = LobbyNotFoundException::class) + fun validate_failWhenNoLobbyExist() { + destinationAccessValidator.hasAccess("", "/lobby/0") + } + + @Test(expected = LobbyNotFoundException::class) + fun validate_failWhenNoGameExist() { + destinationAccessValidator.hasAccess("", "/game/0") + } + + @Test(expected = LobbyNotFoundException::class) + fun validate_failWhenReferencedLobbyDoesNotExist() { + createLobby("Test Game", "ownerUser1") + createLobby("Test Game 2", "ownerUser2") + destinationAccessValidator.hasAccess("doesNotMatter", "/lobby/3") + } + + @Test(expected = LobbyNotFoundException::class) + fun validate_failWhenReferencedGameDoesNotExist() { + createGame("Test Game 1", "user1", "user2", "user3") + createGame("Test Game 2", "user4", "user5", "user6") + destinationAccessValidator.hasAccess("doesNotMatter", "/game/3") + } + + @Test + fun validate_failWhenUserIsNotPartOfReferencedLobby() { + createLobby("Test Game", "ownerUser") + destinationAccessValidator.hasAccess("userNotInLobby", "/lobby/0") + } + + @Test + fun validate_failWhenUserIsNotPartOfReferencedGame() { + createGame("Test Game", "ownerUser", "otherUser1", "otherUser2") + destinationAccessValidator.hasAccess("userNotInGame", "/game/0") + } + + @Test + fun validate_successWhenUserIsOwnerOfReferencedLobby() { + createLobby("Test Game 1", "user1") + assertTrue(destinationAccessValidator.hasAccess("user1", "/lobby/0")) + createLobby("Test Game 2", "user2") + assertTrue(destinationAccessValidator.hasAccess("user2", "/lobby/1")) + } + + @Test + fun validate_successWhenUserIsMemberOfReferencedLobby() { + createLobby("Test Game 1", "user1", "user2") + assertTrue(destinationAccessValidator.hasAccess("user2", "/lobby/0")) + createLobby("Test Game 2", "user3", "user4") + assertTrue(destinationAccessValidator.hasAccess("user4", "/lobby/1")) + } + + @Test + fun validate_successWhenUserIsOwnerOfReferencedGame() { + createGame("Test Game 1", "owner1", "user2", "user3") + assertTrue(destinationAccessValidator.hasAccess("owner1", "/game/0")) + createGame("Test Game 2", "owner4", "user5", "user6") + assertTrue(destinationAccessValidator.hasAccess("owner4", "/game/1")) + } + + @Test + fun validate_successWhenUserIsMemberOfReferencedGame() { + createGame("Test Game 1", "owner1", "user2", "user3") + assertTrue(destinationAccessValidator.hasAccess("user2", "/game/0")) + createGame("Test Game 2", "owner4", "user5", "user6") + assertTrue(destinationAccessValidator.hasAccess("user6", "/game/1")) + } +} |