From c3fc62e7117b2e970eea4eb84137b6624063fb6d Mon Sep 17 00:00:00 2001 From: Joffrey BION Date: Fri, 31 May 2019 20:19:34 +0200 Subject: Create multiplatform SevenWondersClient based on Krossbow --- build.gradle.kts | 2 +- settings.gradle | 3 + sw-client/build.gradle.kts | 49 ++++ .../sevenwonders/client/SevenWondersClient.kt | 118 +++++++++ .../org/luxons/sevenwonders/model/api/Api.kt | 23 ++ .../sevenwonders/model/api/actions/Actions.kt | 65 +++++ .../luxons/sevenwonders/model/api/errors/Errors.kt | 18 ++ .../org/luxons/sevenwonders/model/cards/Cards.kt | 6 + sw-server/build.gradle.kts | 2 + .../kotlin/org/luxons/sevenwonders/SevenWonders.kt | 13 - .../sevenwonders/actions/ChooseNameAction.kt | 19 -- .../sevenwonders/actions/CreateGameAction.kt | 19 -- .../luxons/sevenwonders/actions/JoinGameAction.kt | 17 -- .../sevenwonders/actions/PrepareMoveAction.kt | 18 -- .../sevenwonders/actions/ReorderPlayersAction.kt | 18 -- .../sevenwonders/actions/UpdateSettingsAction.kt | 18 -- .../kotlin/org/luxons/sevenwonders/api/LobbyDTO.kt | 17 -- .../org/luxons/sevenwonders/api/PlayerDTO.kt | 14 -- .../config/AnonymousUsersHandshakeHandler.kt | 27 --- .../config/TopicSubscriptionInterceptor.kt | 38 --- .../sevenwonders/config/WebSecurityConfig.kt | 12 - .../luxons/sevenwonders/config/WebSocketConfig.kt | 44 ---- .../controllers/GameBrowserController.kt | 112 --------- .../sevenwonders/controllers/GameController.kt | 102 -------- .../sevenwonders/controllers/HomeController.kt | 46 ---- .../sevenwonders/controllers/LobbyController.kt | 107 --------- .../org/luxons/sevenwonders/doc/Documentation.kt | 6 - .../org/luxons/sevenwonders/errors/ErrorDTO.kt | 29 --- .../luxons/sevenwonders/errors/ExceptionHandler.kt | 44 ---- .../kotlin/org/luxons/sevenwonders/lobby/Lobby.kt | 112 --------- .../kotlin/org/luxons/sevenwonders/lobby/Player.kt | 61 ----- .../sevenwonders/repositories/LobbyRepository.kt | 31 --- .../sevenwonders/repositories/PlayerRepository.kt | 41 ---- .../org/luxons/sevenwonders/server/Converters.kt | 13 + .../luxons/sevenwonders/server/ExceptionHandler.kt | 46 ++++ .../org/luxons/sevenwonders/server/SevenWonders.kt | 13 + .../luxons/sevenwonders/server/api/Converters.kt | 14 ++ .../config/AnonymousUsersHandshakeHandler.kt | 27 +++ .../server/config/TopicSubscriptionInterceptor.kt | 38 +++ .../server/config/WebSecurityConfig.kt | 12 + .../sevenwonders/server/config/WebSocketConfig.kt | 44 ++++ .../server/controllers/GameBrowserController.kt | 112 +++++++++ .../server/controllers/GameController.kt | 99 ++++++++ .../server/controllers/HomeController.kt | 46 ++++ .../server/controllers/LobbyController.kt | 107 +++++++++ .../sevenwonders/server/doc/Documentation.kt | 6 + .../org/luxons/sevenwonders/server/lobby/Lobby.kt | 109 +++++++++ .../org/luxons/sevenwonders/server/lobby/Player.kt | 61 +++++ .../server/repositories/LobbyRepository.kt | 31 +++ .../server/repositories/PlayerRepository.kt | 41 ++++ .../validation/DestinationAccessValidator.kt | 47 ++++ .../validation/DestinationAccessValidator.kt | 47 ---- .../org/luxons/sevenwonders/SevenWondersTest.kt | 145 ----------- .../controllers/GameBrowserControllerTest.kt | 124 ---------- .../sevenwonders/controllers/HomeControllerTest.kt | 25 -- .../controllers/LobbyControllerTest.kt | 217 ----------------- .../sevenwonders/controllers/TestPrincipal.kt | 8 - .../org/luxons/sevenwonders/lobby/LobbyTest.kt | 266 -------------------- .../repositories/LobbyRepositoryTest.kt | 78 ------ .../repositories/PlayerRepositoryTest.kt | 75 ------ .../luxons/sevenwonders/server/SevenWondersTest.kt | 146 +++++++++++ .../controllers/GameBrowserControllerTest.kt | 124 ++++++++++ .../server/controllers/HomeControllerTest.kt | 25 ++ .../server/controllers/LobbyControllerTest.kt | 217 +++++++++++++++++ .../server/controllers/TestPrincipal.kt | 8 + .../luxons/sevenwonders/server/lobby/LobbyTest.kt | 267 +++++++++++++++++++++ .../server/repositories/LobbyRepositoryTest.kt | 78 ++++++ .../server/repositories/PlayerRepositoryTest.kt | 75 ++++++ .../luxons/sevenwonders/server/test/TestUtils.kt | 10 + .../validation/DestinationAccessValidatorTest.kt | 147 ++++++++++++ .../org/luxons/sevenwonders/test/TestUtils.kt | 10 - .../sevenwonders/test/api/SevenWondersClient.kt | 31 --- .../sevenwonders/test/api/SevenWondersSession.kt | 70 ------ .../validation/DestinationAccessValidatorTest.kt | 147 ------------ sw-ui/build.gradle | 11 + 75 files changed, 2259 insertions(+), 2209 deletions(-) create mode 100644 sw-client/build.gradle.kts create mode 100644 sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt create mode 100644 sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt create mode 100644 sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt create mode 100644 sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/errors/Errors.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/api/LobbyDTO.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/api/PlayerDTO.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/Converters.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/ExceptionHandler.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/SevenWonders.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/AnonymousUsersHandshakeHandler.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/TopicSubscriptionInterceptor.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSecurityConfig.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSocketConfig.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/HomeController.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/doc/Documentation.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Player.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepository.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepository.kt create mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidator.kt delete mode 100644 sw-server/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/HomeControllerTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/TestPrincipal.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/lobby/LobbyTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepositoryTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepositoryTest.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/test/TestUtils.kt create mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidatorTest.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt delete mode 100644 sw-server/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index a27e4680..c52d8dcb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - val kotlinVersion = "1.3.40" + val kotlinVersion = "1.3.41" kotlin("jvm") version kotlinVersion apply false kotlin("multiplatform") version kotlinVersion apply false kotlin("plugin.spring") version kotlinVersion apply false diff --git a/settings.gradle b/settings.gradle index ee1cb240..11887e42 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,4 +13,7 @@ rootProject.name = "seven-wonders" include 'sw-common-model' include 'sw-engine' include 'sw-server' +include 'sw-client' include 'sw-ui' + +enableFeaturePreview("GRADLE_METADATA") diff --git a/sw-client/build.gradle.kts b/sw-client/build.gradle.kts new file mode 100644 index 00000000..35cebd82 --- /dev/null +++ b/sw-client/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + kotlin("multiplatform") + id("org.jlleitschuh.gradle.ktlint") +} + +val krossbowVersion = "0.3.1" + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting { + dependencies { + api(project(":sw-common-model")) + implementation(kotlin("stdlib-common")) + implementation("org.hildan.krossbow:krossbow-client-metadata:$krossbowVersion") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting { + dependencies { + implementation(kotlin("stdlib-jdk8")) + implementation("org.hildan.krossbow:krossbow-client-jvm:$krossbowVersion") + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-junit")) + } + } + val jsMain by getting { + dependencies { + implementation(kotlin("stdlib-js")) + implementation("org.hildan.krossbow:krossbow-client-js:$krossbowVersion") + } + } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} diff --git a/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt b/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt new file mode 100644 index 00000000..544c870b --- /dev/null +++ b/sw-client/src/commonMain/kotlin/org/luxons/sevenwonders/client/SevenWondersClient.kt @@ -0,0 +1,118 @@ +package org.luxons.sevenwonders.client + +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.map +import org.hildan.krossbow.client.KrossbowClient +import org.hildan.krossbow.engines.KrossbowSession +import org.hildan.krossbow.engines.KrossbowSubscription +import org.luxons.sevenwonders.model.CustomizableSettings +import org.luxons.sevenwonders.model.GameState +import org.luxons.sevenwonders.model.PlayerTurnInfo +import org.luxons.sevenwonders.model.api.LobbyDTO +import org.luxons.sevenwonders.model.api.PlayerDTO +import org.luxons.sevenwonders.model.api.SEVEN_WONDERS_WS_ENDPOINT +import org.luxons.sevenwonders.model.api.actions.ChooseNameAction +import org.luxons.sevenwonders.model.api.actions.CreateGameAction +import org.luxons.sevenwonders.model.api.actions.JoinGameAction +import org.luxons.sevenwonders.model.api.actions.ReorderPlayersAction +import org.luxons.sevenwonders.model.api.actions.UpdateSettingsAction +import org.luxons.sevenwonders.model.api.errors.ErrorDTO +import org.luxons.sevenwonders.model.cards.PreparedCard + +class SevenWondersClient { + + private val stompClient = KrossbowClient() + + suspend fun connect(serverUrl: String): SevenWondersSession { + val session = stompClient.connect("$serverUrl$SEVEN_WONDERS_WS_ENDPOINT") + return SevenWondersSession(session) + } +} + +suspend inline fun KrossbowSession.request( + sendDestination: String, + receiveDestination: String, + payload: Any? = null +): T { + val sub = subscribe(receiveDestination) + send(sendDestination, payload) + val msg = sub.messages.receive() + sub.unsubscribe() + return msg.payload +} + +data class SevenWondersSubscription( + val messages: ReceiveChannel, + val unsubscribe: suspend () -> Unit +) + +private fun KrossbowSubscription.ignoreHeaders() = SevenWondersSubscription( + messages = messages.map { it.payload }, + unsubscribe = { unsubscribe() } +) + +class SevenWondersSession(private val stompSession: KrossbowSession) { + + suspend fun disconnect() = stompSession.disconnect() + + suspend fun watchErrors(): KrossbowSubscription = + stompSession.subscribe("/user/queue/errors") + + suspend fun chooseName(displayName: String): PlayerDTO { + val action = ChooseNameAction(displayName) + return stompSession.request("/app/chooseName", "/user/queue/nameChoice", action) + } + + suspend fun watchGames(): SevenWondersSubscription> = + stompSession.subscribe>("/topic/games").ignoreHeaders() + + suspend fun createGame(gameName: String): LobbyDTO { + val action = CreateGameAction(gameName) + return stompSession.request("/app/lobby/create", "/user/queue/lobby/joined", action) + } + + suspend fun joinGame(gameId: Long): LobbyDTO { + val action = JoinGameAction(gameId) + return stompSession.request("/app/lobby/join", "/user/queue/lobby/joined", action) + } + + suspend fun leaveGame() { + stompSession.send("/app/lobby/leave") + } + + suspend fun reorderPlayers(players: List) { + stompSession.send("/app/lobby/reorderPlayers", ReorderPlayersAction(players)) + } + + suspend fun updateSettings(settings: CustomizableSettings) { + stompSession.send("/app/lobby/reorderPlayers", UpdateSettingsAction(settings)) + } + + suspend fun watchLobbyUpdates(gameId: Long): SevenWondersSubscription = + stompSession.subscribe("/topic/lobby/$gameId/updated").ignoreHeaders() + + suspend fun watchGameStart(gameId: Long): SevenWondersSubscription = + stompSession.subscribe("/topic/lobby/$gameId/started").ignoreHeaders() + + suspend fun startGame(gameId: Long) { + val sendDestination = "/app/lobby/startGame" + val receiveDestination = "/topic/lobby/$gameId/started" + stompSession.request(sendDestination, receiveDestination) + } + + suspend fun watchPlayerReady(gameId: Long): SevenWondersSubscription = + stompSession.subscribe("/topic/game/$gameId/playerReady").ignoreHeaders() + + suspend fun watchTableUpdates(gameId: Long): SevenWondersSubscription = + stompSession.subscribe("/topic/game/$gameId/tableUpdates").ignoreHeaders() + + suspend fun watchPreparedCards(gameId: Long): SevenWondersSubscription = + stompSession.subscribe("/topic/game/$gameId/prepared").ignoreHeaders() + + suspend fun watchTurns(): SevenWondersSubscription = + stompSession.subscribe("/user/queue/game/turn").ignoreHeaders() + + suspend fun sayReady() { + stompSession.send("/app/game/sayReady") + } +} diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt new file mode 100644 index 00000000..04c7ef2f --- /dev/null +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/Api.kt @@ -0,0 +1,23 @@ +package org.luxons.sevenwonders.model.api + +const val SEVEN_WONDERS_WS_ENDPOINT = "/seven-wonders-websocket" + +enum class State { + LOBBY, PLAYING +} + +data class LobbyDTO( + val id: Long, + val name: String, + val owner: String, + val players: List, + val state: State +) + +data class PlayerDTO( + val username: String, + val displayName: String, + val index: Int, + val isGameOwner: Boolean, + val isUser: Boolean +) diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt new file mode 100644 index 00000000..59bc2e5b --- /dev/null +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/actions/Actions.kt @@ -0,0 +1,65 @@ +package org.luxons.sevenwonders.model.api.actions + +import org.luxons.sevenwonders.model.CustomizableSettings +import org.luxons.sevenwonders.model.PlayerMove + +/** + * The action to choose the player's name. This is the first action that should be called. + */ +class ChooseNameAction( + /** + * The display name of the player. May contain spaces and special characters. + */ + val playerName: String +) + +/** + * The action to create a game. + */ +class CreateGameAction( + /** + * The name of the game to create. + */ + val gameName: String +) + +/** + * The action to join a game. + */ +class JoinGameAction( + /** + * The ID of the game to join. + */ + val gameId: Long +) + +/** + * The action to prepare the next move during a game. + */ +class PrepareMoveAction( + /** + * The move to prepare. + */ + val move: PlayerMove +) + +/** + * 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. + */ +class ReorderPlayersAction( + /** + * The list of usernames of the players, in the new order. + */ + val orderedPlayers: List +) + +/** + * The action to update the settings of the game. Can only be called in the lobby by the owner of the game. + */ +class UpdateSettingsAction( + /** + * The new values for the settings. + */ + val settings: CustomizableSettings +) diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/errors/Errors.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/errors/Errors.kt new file mode 100644 index 00000000..f668f020 --- /dev/null +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/api/errors/Errors.kt @@ -0,0 +1,18 @@ +package org.luxons.sevenwonders.model.api.errors + +enum class ErrorType { + VALIDATION, CLIENT, SERVER +} + +data class ErrorDTO( + val code: String, + val message: String, + val type: ErrorType, + val details: List = emptyList() +) + +data class ValidationErrorDTO( + val path: String, + val message: String, + val rejectedValue: Any? = null +) diff --git a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt index 57cf3a00..11a3e8d8 100644 --- a/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt +++ b/sw-common-model/src/commonMain/kotlin/org/luxons/sevenwonders/model/cards/Cards.kt @@ -1,5 +1,6 @@ package org.luxons.sevenwonders.model.cards +import org.luxons.sevenwonders.model.api.PlayerDTO import org.luxons.sevenwonders.model.boards.Requirements import org.luxons.sevenwonders.model.resources.ResourceTransactions @@ -29,6 +30,11 @@ data class HandCard( val playability: CardPlayability ) +class PreparedCard( + val player: PlayerDTO, + val cardBack: CardBack +) + data class CardBack(val image: String) enum class PlayabilityLevel { diff --git a/sw-server/build.gradle.kts b/sw-server/build.gradle.kts index ecfb5380..59a533df 100644 --- a/sw-server/build.gradle.kts +++ b/sw-server/build.gradle.kts @@ -28,9 +28,11 @@ dependencies { testImplementation(kotlin("test")) testImplementation(kotlin("test-junit")) + testImplementation(project(":sw-client")) testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.hildan.jackstomp:jackstomp:2.0.0") testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-M1") } // packages the frontend app within the jar diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt deleted file mode 100644 index 04f03956..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/SevenWonders.kt +++ /dev/null @@ -1,13 +0,0 @@ -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) { - runApplication(*args) -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt deleted file mode 100644 index ab444780..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ChooseNameAction.kt +++ /dev/null @@ -1,19 +0,0 @@ -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/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt deleted file mode 100644 index c10f9c34..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/CreateGameAction.kt +++ /dev/null @@ -1,19 +0,0 @@ -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/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt deleted file mode 100644 index 002309b3..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/JoinGameAction.kt +++ /dev/null @@ -1,17 +0,0 @@ -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/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt deleted file mode 100644 index ed420cfc..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/PrepareMoveAction.kt +++ /dev/null @@ -1,18 +0,0 @@ -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.model.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/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt deleted file mode 100644 index 79a32137..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/ReorderPlayersAction.kt +++ /dev/null @@ -1,18 +0,0 @@ -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 -) diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt deleted file mode 100644 index 33303044..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/actions/UpdateSettingsAction.kt +++ /dev/null @@ -1,18 +0,0 @@ -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.model.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/sw-server/src/main/kotlin/org/luxons/sevenwonders/api/LobbyDTO.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/api/LobbyDTO.kt deleted file mode 100644 index b4445f32..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/api/LobbyDTO.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.luxons.sevenwonders.api - -import org.luxons.sevenwonders.lobby.Lobby -import org.luxons.sevenwonders.lobby.State - -data class LobbyDTO( - val id: Long, - val name: String, - val owner: String, - val players: List, - val state: State -) - -fun Lobby.toDTO(currentUser: String): LobbyDTO { - val players = getPlayers().map { it.toDTO(currentUser) } - return LobbyDTO(id, name, owner.username, players, state) -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/api/PlayerDTO.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/api/PlayerDTO.kt deleted file mode 100644 index 54c69122..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/api/PlayerDTO.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.luxons.sevenwonders.api - -import org.luxons.sevenwonders.lobby.Player - -data class PlayerDTO( - val username: String, - val displayName: String, - val index: Int, - val isGameOwner: Boolean, - val isUser: Boolean -) - -fun Player.toDTO(currentUser: String) = - PlayerDTO(username, displayName, index, isGameOwner, username === currentUser) diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt deleted file mode 100644 index db707d1b..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -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 - ): Principal? { - var p = super.determineUser(request, wsHandler, attributes) - if (p == null) { - p = UsernamePasswordAuthenticationToken("player" + playerId++, null) - } - return p - } -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt deleted file mode 100644 index f4c55c2c..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.kt +++ /dev/null @@ -1,38 +0,0 @@ -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/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt deleted file mode 100644 index 06b2bc90..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSecurityConfig.kt +++ /dev/null @@ -1,12 +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 -class WebSecurityConfig : WebSecurityConfigurerAdapter() { - - // this disables default authentication settings - override fun configure(httpSecurity: HttpSecurity) = Unit -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt deleted file mode 100644 index b49d39cc..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/config/WebSocketConfig.kt +++ /dev/null @@ -1,44 +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 - -const val SEVEN_WONDERS_WS_ENDPOINT = "/seven-wonders-websocket" - -@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_WS_ENDPOINT) - .setHandshakeHandler(handshakeHandler()) - .setAllowedOrigins("*") // to allow any client to use the API - .withSockJS() - } - - @Bean - fun handshakeHandler(): DefaultHandshakeHandler { - return AnonymousUsersHandshakeHandler() - } - - override fun configureClientInboundChannel(registration: ChannelRegistration) { - registration.interceptors(topicSubscriptionInterceptor) - } -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt deleted file mode 100644 index f856365f..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameBrowserController.kt +++ /dev/null @@ -1,112 +0,0 @@ -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.api.LobbyDTO -import org.luxons.sevenwonders.api.toDTO -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 { - logger.info("Player '{}' subscribed to /topic/games", principal.name) - return lobbyRepository.list().map { it.toDTO(principal.name) } - } - - /** - * 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): LobbyDTO { - 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 - val lobbyDto = lobby.toDTO(principal.name) - template.convertAndSend("/topic/games", listOf(lobbyDto)) - return lobbyDto - } - - /** - * 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): LobbyDTO { - 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 - ) - val lobbyDTO = lobby.toDTO(principal.name) - lobbyController.sendLobbyUpdateToPlayers(lobbyDTO) - return lobbyDTO - } - - 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt deleted file mode 100644 index a923f845..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/GameController.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.luxons.sevenwonders.controllers - -import org.hildan.livedoc.core.annotations.Api -import org.luxons.sevenwonders.actions.PrepareMoveAction -import org.luxons.sevenwonders.api.PlayerDTO -import org.luxons.sevenwonders.api.toDTO -import org.luxons.sevenwonders.engine.Game -import org.luxons.sevenwonders.model.GameState -import org.luxons.sevenwonders.model.cards.CardBack -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.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() - - sendPlayerReady(game.id, player) - - val allReady = players.all { it.isReady } - if (allReady) { - logger.info("Game {}: all players ready, sending turn info", game.id) - players.forEach { it.isReady = false } - sendTurnInfo(players, game) - } - } - - private fun sendTurnInfo(players: List, 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.toDTO(principal.name), preparedCardBack) - logger.info("Game {}: player {} prepared move {}", game.id, principal.name, action.move) - sendPreparedCard(game.id, preparedCard) - - if (game.allPlayersPreparedTheirMove()) { - logger.info("Game {}: all players have prepared their move, executing turn...", game.id) - val table = game.playTurn() - sendPlayedMoves(game.id, table) - } - } - - private fun sendPlayedMoves(gameId: Long, gameState: GameState) = - template.convertAndSend("/topic/game/$gameId/tableUpdates", gameState) - - private fun sendPreparedCard(gameId: Long, preparedCard: PreparedCard) = - template.convertAndSend("/topic/game/$gameId/prepared", preparedCard) - - companion object { - private val logger = LoggerFactory.getLogger(GameController::class.java) - } -} - -class PreparedCard(val player: PlayerDTO, val cardBack: CardBack) diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt deleted file mode 100644 index bd672000..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/HomeController.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.luxons.sevenwonders.controllers - -import org.hildan.livedoc.core.annotations.Api -import org.luxons.sevenwonders.actions.ChooseNameAction -import org.luxons.sevenwonders.api.PlayerDTO -import org.luxons.sevenwonders.api.toDTO -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 [PlayerDTO] object - */ - @MessageMapping("/chooseName") - @SendToUser("/queue/nameChoice") - fun chooseName(@Validated action: ChooseNameAction, principal: Principal): PlayerDTO { - val username = principal.name - val player = playerRepository.createOrUpdate(username, action.playerName) - - logger.info("Player '{}' chose the name '{}'", username, player.displayName) - return player.toDTO(username) - } - - companion object { - private val logger = LoggerFactory.getLogger(HomeController::class.java) - } -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt deleted file mode 100644 index 4e4120a9..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/controllers/LobbyController.kt +++ /dev/null @@ -1,107 +0,0 @@ -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.api.LobbyDTO -import org.luxons.sevenwonders.api.toDTO -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.toDTO(principal.name)) - } - - /** - * 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.toDTO(principal.name)) - } - - /** - * 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.toDTO(principal.name)) - } - - internal fun sendLobbyUpdateToPlayers(lobby: LobbyDTO) { - 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt deleted file mode 100644 index 3b04356a..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/doc/Documentation.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.luxons.sevenwonders.doc - -object Documentation { - - const val GROUP_ACTIONS = "Actions" -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt deleted file mode 100644 index c3eae0b5..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ErrorDTO.kt +++ /dev/null @@ -1,29 +0,0 @@ -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 = 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt deleted file mode 100644 index 76d01f5f..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/errors/ExceptionHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -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/sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt deleted file mode 100644 index 4b9f5bc3..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Lobby.kt +++ /dev/null @@ -1,112 +0,0 @@ -package org.luxons.sevenwonders.lobby - -import org.luxons.sevenwonders.engine.Game -import org.luxons.sevenwonders.model.CustomizableSettings -import org.luxons.sevenwonders.engine.data.GameDefinition - -enum class State { - LOBBY, PLAYING -} - -class Lobby( - val id: Long, - val name: String, - var owner: Player, - private val gameDefinition: GameDefinition -) { - private val players: MutableList = ArrayList(gameDefinition.maxPlayers) - - var settings: CustomizableSettings = - CustomizableSettings() - - var state = State.LOBBY - private set - - init { - addPlayer(owner) - } - - fun getPlayers(): List = 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) { - val usernames = players.map { it.username } - if (orderedUsernames.toSet() != usernames.toSet()) { - throw PlayerListMismatchException(orderedUsernames) - } - players.sortBy { orderedUsernames.indexOf(it.username) } - } - - 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) : - IllegalArgumentException("Name '$displayName' is already used by a player in game '$gameName'") - - internal class UnknownPlayerException(username: String) : - IllegalArgumentException("Unknown player '$username'") - - internal class PlayerListMismatchException(usernames: List) : - IllegalArgumentException("Newly ordered usernames $usernames don't match the current player list") -} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt deleted file mode 100644 index d057fcb2..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/lobby/Player.kt +++ /dev/null @@ -1,61 +0,0 @@ -package org.luxons.sevenwonders.lobby - -import org.luxons.sevenwonders.errors.ApiMisuseException -import org.luxons.sevenwonders.engine.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 - - private var _lobby: Lobby? = null - - val lobby: Lobby - get() = _lobby ?: throw PlayerNotInLobbyException(username) - - val ownedLobby: Lobby - get() = if (isGameOwner) lobby else throw PlayerIsNotOwnerException(username) - - private var _game: Game? = null - - 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 = "'$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/sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt deleted file mode 100644 index 02866ffc..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/LobbyRepository.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.luxons.sevenwonders.repositories - -import org.luxons.sevenwonders.engine.data.GameDefinition -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 lobbies = HashMap() - - private var lastGameId: Long = 0 - - fun list(): Collection = lobbies.values - - fun create(gameName: String, owner: Player): Lobby { - val id = lastGameId++ - val lobby = Lobby(id, gameName, owner, GameDefinition.load()) - 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt deleted file mode 100644 index 4d552eaa..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/repositories/PlayerRepository.kt +++ /dev/null @@ -1,41 +0,0 @@ -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() - - 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/Converters.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/Converters.kt new file mode 100644 index 00000000..7e1a6fdf --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/Converters.kt @@ -0,0 +1,13 @@ +package org.luxons.sevenwonders.server + +import org.luxons.sevenwonders.model.api.errors.ValidationErrorDTO +import org.springframework.validation.FieldError +import org.springframework.validation.ObjectError + +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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/ExceptionHandler.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/ExceptionHandler.kt new file mode 100644 index 00000000..f006505b --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/ExceptionHandler.kt @@ -0,0 +1,46 @@ +package org.luxons.sevenwonders.server + +import org.luxons.sevenwonders.model.api.errors.ErrorDTO +import org.luxons.sevenwonders.model.api.errors.ErrorType +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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/SevenWonders.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/SevenWonders.kt new file mode 100644 index 00000000..51710115 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/SevenWonders.kt @@ -0,0 +1,13 @@ +package org.luxons.sevenwonders.server + +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) { + runApplication(*args) +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt new file mode 100644 index 00000000..6f0fc180 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/api/Converters.kt @@ -0,0 +1,14 @@ +package org.luxons.sevenwonders.server.api + +import org.luxons.sevenwonders.model.api.LobbyDTO +import org.luxons.sevenwonders.model.api.PlayerDTO +import org.luxons.sevenwonders.server.lobby.Lobby +import org.luxons.sevenwonders.server.lobby.Player + +fun Lobby.toDTO(currentUser: String): LobbyDTO { + val players = getPlayers().map { it.toDTO(currentUser) } + return LobbyDTO(id, name, owner.username, players, state) +} + +fun Player.toDTO(currentUser: String) = + PlayerDTO(username, displayName, index, isGameOwner, username === currentUser) diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/AnonymousUsersHandshakeHandler.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/AnonymousUsersHandshakeHandler.kt new file mode 100644 index 00000000..56b3400f --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/AnonymousUsersHandshakeHandler.kt @@ -0,0 +1,27 @@ +package org.luxons.sevenwonders.server.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 + ): Principal? { + var p = super.determineUser(request, wsHandler, attributes) + if (p == null) { + p = UsernamePasswordAuthenticationToken("player" + playerId++, null) + } + return p + } +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/TopicSubscriptionInterceptor.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/TopicSubscriptionInterceptor.kt new file mode 100644 index 00000000..5342503b --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/TopicSubscriptionInterceptor.kt @@ -0,0 +1,38 @@ +package org.luxons.sevenwonders.server.config + +import org.luxons.sevenwonders.server.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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSecurityConfig.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSecurityConfig.kt new file mode 100644 index 00000000..9557ba1a --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSecurityConfig.kt @@ -0,0 +1,12 @@ +package org.luxons.sevenwonders.server.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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSocketConfig.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSocketConfig.kt new file mode 100644 index 00000000..72312b70 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/config/WebSocketConfig.kt @@ -0,0 +1,44 @@ +package org.luxons.sevenwonders.server.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 + +const val SEVEN_WONDERS_WS_ENDPOINT = "/seven-wonders-websocket" + +@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_WS_ENDPOINT) + .setHandshakeHandler(handshakeHandler()) + .setAllowedOrigins("*") // to allow any client to use the API + .withSockJS() + } + + @Bean + fun handshakeHandler(): DefaultHandshakeHandler { + return AnonymousUsersHandshakeHandler() + } + + override fun configureClientInboundChannel(registration: ChannelRegistration) { + registration.interceptors(topicSubscriptionInterceptor) + } +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt new file mode 100644 index 00000000..e2f0458e --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserController.kt @@ -0,0 +1,112 @@ +package org.luxons.sevenwonders.server.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.model.api.LobbyDTO +import org.luxons.sevenwonders.model.api.actions.CreateGameAction +import org.luxons.sevenwonders.model.api.actions.JoinGameAction +import org.luxons.sevenwonders.server.ApiMisuseException +import org.luxons.sevenwonders.server.api.toDTO +import org.luxons.sevenwonders.server.lobby.Lobby +import org.luxons.sevenwonders.server.repositories.LobbyRepository +import org.luxons.sevenwonders.server.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 { + logger.info("Player '{}' subscribed to /topic/games", principal.name) + return lobbyRepository.list().map { it.toDTO(principal.name) } + } + + /** + * 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): LobbyDTO { + 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 + val lobbyDto = lobby.toDTO(principal.name) + template.convertAndSend("/topic/games", listOf(lobbyDto)) + return lobbyDto + } + + /** + * 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): LobbyDTO { + 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 + ) + val lobbyDTO = lobby.toDTO(principal.name) + lobbyController.sendLobbyUpdateToPlayers(lobbyDTO) + return lobbyDTO + } + + 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt new file mode 100644 index 00000000..e1fae39b --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/GameController.kt @@ -0,0 +1,99 @@ +package org.luxons.sevenwonders.server.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.engine.Game +import org.luxons.sevenwonders.model.GameState +import org.luxons.sevenwonders.model.api.actions.PrepareMoveAction +import org.luxons.sevenwonders.model.cards.PreparedCard +import org.luxons.sevenwonders.server.api.toDTO +import org.luxons.sevenwonders.server.lobby.Player +import org.luxons.sevenwonders.server.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() + + sendPlayerReady(game.id, player) + + val allReady = players.all { it.isReady } + if (allReady) { + logger.info("Game {}: all players ready, sending turn info", game.id) + players.forEach { it.isReady = false } + sendTurnInfo(players, game) + } + } + + private fun sendTurnInfo(players: List, 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.toDTO(principal.name), preparedCardBack) + logger.info("Game {}: player {} prepared move {}", game.id, principal.name, action.move) + sendPreparedCard(game.id, preparedCard) + + if (game.allPlayersPreparedTheirMove()) { + logger.info("Game {}: all players have prepared their move, executing turn...", game.id) + val table = game.playTurn() + sendPlayedMoves(game.id, table) + } + } + + private fun sendPlayedMoves(gameId: Long, gameState: GameState) = + template.convertAndSend("/topic/game/$gameId/tableUpdates", gameState) + + 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/HomeController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/HomeController.kt new file mode 100644 index 00000000..a0d53f8b --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/HomeController.kt @@ -0,0 +1,46 @@ +package org.luxons.sevenwonders.server.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.model.api.PlayerDTO +import org.luxons.sevenwonders.model.api.actions.ChooseNameAction +import org.luxons.sevenwonders.server.api.toDTO +import org.luxons.sevenwonders.server.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 [PlayerDTO] object + */ + @MessageMapping("/chooseName") + @SendToUser("/queue/nameChoice") + fun chooseName(@Validated action: ChooseNameAction, principal: Principal): PlayerDTO { + val username = principal.name + val player = playerRepository.createOrUpdate(username, action.playerName) + + logger.info("Player '{}' chose the name '{}'", username, player.displayName) + return player.toDTO(username) + } + + companion object { + private val logger = LoggerFactory.getLogger(HomeController::class.java) + } +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt new file mode 100644 index 00000000..19834ef1 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/controllers/LobbyController.kt @@ -0,0 +1,107 @@ +package org.luxons.sevenwonders.server.controllers + +import org.hildan.livedoc.core.annotations.Api +import org.luxons.sevenwonders.model.api.LobbyDTO +import org.luxons.sevenwonders.model.api.actions.ReorderPlayersAction +import org.luxons.sevenwonders.model.api.actions.UpdateSettingsAction +import org.luxons.sevenwonders.server.api.toDTO +import org.luxons.sevenwonders.server.lobby.Player +import org.luxons.sevenwonders.server.repositories.LobbyRepository +import org.luxons.sevenwonders.server.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.toDTO(principal.name)) + } + + /** + * 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.toDTO(principal.name)) + } + + /** + * 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.toDTO(principal.name)) + } + + internal fun sendLobbyUpdateToPlayers(lobby: LobbyDTO) { + 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", Unit) + } + + companion object { + private val logger = LoggerFactory.getLogger(LobbyController::class.java) + } +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/doc/Documentation.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/doc/Documentation.kt new file mode 100644 index 00000000..f033ddd0 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/doc/Documentation.kt @@ -0,0 +1,6 @@ +package org.luxons.sevenwonders.server.doc + +object Documentation { + + const val GROUP_ACTIONS = "Actions" +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt new file mode 100644 index 00000000..7158a417 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Lobby.kt @@ -0,0 +1,109 @@ +package org.luxons.sevenwonders.server.lobby + +import org.luxons.sevenwonders.engine.Game +import org.luxons.sevenwonders.engine.data.GameDefinition +import org.luxons.sevenwonders.model.CustomizableSettings +import org.luxons.sevenwonders.model.api.State + +class Lobby( + val id: Long, + val name: String, + var owner: Player, + private val gameDefinition: GameDefinition +) { + private val players: MutableList = ArrayList(gameDefinition.maxPlayers) + + var settings: CustomizableSettings = + CustomizableSettings() + + var state = State.LOBBY + private set + + init { + addPlayer(owner) + } + + fun getPlayers(): List = 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) { + val usernames = players.map { it.username } + if (orderedUsernames.toSet() != usernames.toSet()) { + throw PlayerListMismatchException(orderedUsernames) + } + players.sortBy { orderedUsernames.indexOf(it.username) } + } + + 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) : + IllegalArgumentException("Name '$displayName' is already used by a player in game '$gameName'") + + internal class UnknownPlayerException(username: String) : + IllegalArgumentException("Unknown player '$username'") + + internal class PlayerListMismatchException(usernames: List) : + IllegalArgumentException("Newly ordered usernames $usernames don't match the current player list") +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Player.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Player.kt new file mode 100644 index 00000000..8ed7d048 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/lobby/Player.kt @@ -0,0 +1,61 @@ +package org.luxons.sevenwonders.server.lobby + +import org.luxons.sevenwonders.engine.Game +import org.luxons.sevenwonders.server.ApiMisuseException + +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 + + private var _lobby: Lobby? = null + + val lobby: Lobby + get() = _lobby ?: throw PlayerNotInLobbyException(username) + + val ownedLobby: Lobby + get() = if (isGameOwner) lobby else throw PlayerIsNotOwnerException(username) + + private var _game: Game? = null + + 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 = "'$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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepository.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepository.kt new file mode 100644 index 00000000..ff06f549 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepository.kt @@ -0,0 +1,31 @@ +package org.luxons.sevenwonders.server.repositories + +import org.luxons.sevenwonders.engine.data.GameDefinition +import org.luxons.sevenwonders.server.lobby.Lobby +import org.luxons.sevenwonders.server.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 lobbies = HashMap() + + private var lastGameId: Long = 0 + + fun list(): Collection = lobbies.values + + fun create(gameName: String, owner: Player): Lobby { + val id = lastGameId++ + val lobby = Lobby(id, gameName, owner, GameDefinition.load()) + 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepository.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepository.kt new file mode 100644 index 00000000..00badf75 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepository.kt @@ -0,0 +1,41 @@ +package org.luxons.sevenwonders.server.repositories + +import org.luxons.sevenwonders.server.ApiMisuseException +import org.luxons.sevenwonders.server.lobby.Player +import org.springframework.stereotype.Repository +import java.util.HashMap + +@Repository +class PlayerRepository { + + private val players = HashMap() + + 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/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidator.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidator.kt new file mode 100644 index 00000000..04daf311 --- /dev/null +++ b/sw-server/src/main/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidator.kt @@ -0,0 +1,47 @@ +package org.luxons.sevenwonders.server.validation + +import org.luxons.sevenwonders.server.repositories.LobbyRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +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 = gameMatcher.group("id").toLong() + 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 = lobbyMatcher.group("id").toLong() + return !isUserInLobby(username, lobbyId) + } + + private fun isUserInLobby(username: String, lobbyId: Long): Boolean = + lobbyRepository.find(lobbyId).containsUser(username) + + companion object { + + private val lobbyDestination = Pattern.compile(".*?/lobby/(?\\d+?)(/.*)?") + + private val gameDestination = Pattern.compile(".*?/game/(?\\d+?)(/.*)?") + } +} diff --git a/sw-server/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt b/sw-server/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt deleted file mode 100644 index 5f704357..00000000 --- a/sw-server/src/main/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidator.kt +++ /dev/null @@ -1,47 +0,0 @@ -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.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 = gameMatcher.group("id").toLong() - 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 = lobbyMatcher.group("id").toLong() - return !isUserInLobby(username, lobbyId) - } - - private fun isUserInLobby(username: String, lobbyId: Long): Boolean = - lobbyRepository.find(lobbyId).containsUser(username) - - companion object { - - private val lobbyDestination = Pattern.compile(".*?/lobby/(?\\d+?)(/.*)?") - - private val gameDestination = Pattern.compile(".*?/game/(?\\d+?)(/.*)?") - } -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt deleted file mode 100644 index 01de366a..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/SevenWondersTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -package org.luxons.sevenwonders - -import org.junit.After -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.TimeUnit -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -@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 - fun chooseName() { - val session = client.connect(serverUrl) - val playerName = "Test User" - val player = session.chooseName(playerName) - assertNotNull(player) - assertEquals(playerName, player.displayName) - session.disconnect() - } - - private fun newPlayer(name: String): SevenWondersSession { - val otherSession = client.connect(serverUrl) - otherSession.chooseName(name) - return otherSession - } - - @Test - 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 - 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 - fun createGame_seenByConnectedPlayers() { - val otherSession = newPlayer("OtherPlayer") - val games = otherSession.watchGames() - - var receivedLobbies = games.next() - assertNotNull(receivedLobbies) - assertEquals(0, receivedLobbies.size) - - val ownerSession = newPlayer("GameOwner") - val gameName = "Test Game" - val createdLobby = ownerSession.createGame(gameName) - - receivedLobbies = games.next() - assertNotNull(receivedLobbies) - assertEquals(1, receivedLobbies.size) - val receivedLobby = receivedLobbies[0] - assertEquals(createdLobby.id, receivedLobby.id) - assertEquals(createdLobby.name, receivedLobby.name) - - disconnect(ownerSession, otherSession) - } - - @Test - 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt deleted file mode 100644 index 343b7f34..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/GameBrowserControllerTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -package org.luxons.sevenwonders.controllers - -import org.junit.Before -import org.junit.Test -import org.luxons.sevenwonders.actions.CreateGameAction -import org.luxons.sevenwonders.actions.JoinGameAction -import org.luxons.sevenwonders.api.toDTO -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.mockSimpMessagingTemplate -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class GameBrowserControllerTest { - - private lateinit var playerRepository: PlayerRepository - - private lateinit var gameBrowserController: GameBrowserController - - @Before - fun setUp() { - playerRepository = PlayerRepository() - val lobbyRepository = LobbyRepository() - val template = mockSimpMessagingTemplate() - 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() - assertEquals(lobby, createdLobby) - assertEquals(player.toDTO(principal.name), lobby.players[0]) - } - - @Test - fun createGame_failsForUnknownPlayer() { - val principal = TestPrincipal("unknown") - val action = CreateGameAction("Test Game") - - assertFailsWith { - gameBrowserController.createGame(action, principal) - } - } - - @Test - 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 - assertFailsWith { - 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) - assertEquals(owner.toDTO(ownerPrincipal.name), createdLobby.players[0]) - - val joiner = playerRepository.createOrUpdate("testjoiner", "Test User Joiner") - val joinerPrincipal = TestPrincipal("testjoiner") - val joinGameAction = JoinGameAction(createdLobby.id) - - val joinedLobby = gameBrowserController.joinGame(joinGameAction, joinerPrincipal) - - assertEquals(owner.toDTO(joinerPrincipal.name), joinedLobby.players[0]) - assertEquals(joiner.toDTO(joinerPrincipal.name), joinedLobby.players[1]) - } - - @Test - 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 - assertFailsWith { - gameBrowserController.joinGame(joinGameAction, joinerPrincipal) - } - } -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt deleted file mode 100644 index 3374a025..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/HomeControllerTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.luxons.sevenwonders.controllers - -import org.junit.Test -import org.luxons.sevenwonders.actions.ChooseNameAction -import org.luxons.sevenwonders.repositories.PlayerRepository -import kotlin.test.assertEquals - -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) - - assertEquals("testuser", player.username) - assertEquals("Test User", player.displayName) - assertEquals(false, player.isGameOwner) - assertEquals(true, player.isUser) - } -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt deleted file mode 100644 index 90db48f5..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/LobbyControllerTest.kt +++ /dev/null @@ -1,217 +0,0 @@ -package org.luxons.sevenwonders.controllers - -import org.junit.Before -import org.junit.Test -import org.luxons.sevenwonders.actions.ReorderPlayersAction -import org.luxons.sevenwonders.actions.UpdateSettingsAction -import org.luxons.sevenwonders.model.CustomizableSettings -import org.luxons.sevenwonders.model.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.mockSimpMessagingTemplate -import java.util.HashMap -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertSame -import kotlin.test.assertTrue - -class LobbyControllerTest { - - private lateinit var playerRepository: PlayerRepository - - private lateinit var lobbyRepository: LobbyRepository - - private lateinit var lobbyController: LobbyController - - @Before - fun setUp() { - val template = mockSimpMessagingTemplate() - 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, lobby.owner) - assertTrue(owner.isInLobby) - assertFalse(owner.isInGame) - } - - @Test - fun leave_failsWhenPlayerDoesNotExist() { - val principal = TestPrincipal("I don't exist") - - assertFailsWith { - lobbyController.leave(principal) - } - } - - @Test - fun leave_failsWhenNotInLobby() { - playerRepository.createOrUpdate("testuser", "Test User") - val principal = TestPrincipal("testuser") - - assertFailsWith { - 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 = listOf(player, player2, player3, player4) - assertEquals(players, lobby.getPlayers()) - - val reorderedPlayers = listOf(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 - 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 = listOf(player3, player, player2) - val playerNames = reorderedPlayers.map { it.username } - val reorderPlayersAction = ReorderPlayersAction(playerNames) - - val principal = TestPrincipal("testuser2") - - assertFailsWith { - 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 - 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") - - assertFailsWith { - 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 - 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") - - assertFailsWith { - 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt deleted file mode 100644 index 76b0f8fa..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/controllers/TestPrincipal.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.luxons.sevenwonders.controllers - -import java.security.Principal - -internal class TestPrincipal(private val name: String) : Principal { - - override fun getName(): String = name -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt deleted file mode 100644 index bf1ea4bf..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/lobby/LobbyTest.kt +++ /dev/null @@ -1,266 +0,0 @@ -package org.luxons.sevenwonders.lobby - -import org.junit.Assume.assumeTrue -import org.junit.Before -import org.junit.BeforeClass -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.runner.RunWith -import org.luxons.sevenwonders.model.CustomizableSettings -import org.luxons.sevenwonders.engine.data.GameDefinition -import org.luxons.sevenwonders.lobby.Lobby.GameAlreadyStartedException -import org.luxons.sevenwonders.lobby.Lobby.PlayerListMismatchException -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 kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertTrue - -@RunWith(Theories::class) -class LobbyTest { - - 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 - fun addPlayer_failsOnSameName() { - val player = Player("testuser", "Test User") - val player2 = Player("testuser2", "Test User") - lobby.addPlayer(player) - assertFailsWith { - lobby.addPlayer(player2) - } - } - - @Test - fun addPlayer_playerOverflowWhenTooMany() { - assertFailsWith { - // the owner + the max number gives an overflow - addPlayers(gameDefinition.maxPlayers) - } - } - - @Test - fun addPlayer_failWhenGameStarted() { - // total with owner is the minimum - addPlayers(gameDefinition.minPlayers - 1) - lobby.startGame() - assertFailsWith { - lobby.addPlayer(Player("soonerNextTime", "The Late Guy")) - } - } - - private fun addPlayers(nbPlayers: Int) { - repeat(nbPlayers) { - val player = Player("testuser$it", "Test User $it") - lobby.addPlayer(player) - } - } - - @Test - fun removePlayer_failsWhenNotPresent() { - assertFailsWith { - 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) - - val reorderedUsernames = listOf("testuser3", "gameowner", "testuser1", "testuser2") - lobby.reorderPlayers(reorderedUsernames) - - assertEquals(reorderedUsernames, lobby.getPlayers().map { it.username }) - } - - @Test - fun reorderPlayers_failsOnUnknownPlayer() { - val player1 = Player("testuser1", "Test User 1") - val player2 = Player("testuser2", "Test User 2") - lobby.addPlayer(player1) - lobby.addPlayer(player2) - - assertFailsWith { - lobby.reorderPlayers(listOf("unknown", "testuser2", "gameowner")) - } - } - - @Test - fun reorderPlayers_failsOnExtraPlayer() { - val player1 = Player("testuser1", "Test User 1") - val player2 = Player("testuser2", "Test User 2") - lobby.addPlayer(player1) - lobby.addPlayer(player2) - - assertFailsWith { - lobby.reorderPlayers(listOf("testuser2", "onemore", "testuser1", "gameowner")) - } - } - - @Test - fun reorderPlayers_failsOnMissingPlayer() { - val player1 = Player("testuser1", "Test User 1") - val player2 = Player("testuser2", "Test User 2") - lobby.addPlayer(player1) - lobby.addPlayer(player2) - - assertFailsWith { - lobby.reorderPlayers(listOf("testuser2", "gameowner")) - } - } - - @Theory - fun startGame_failsBelowMinPlayers(nbPlayers: Int) { - assumeTrue(nbPlayers < gameDefinition.minPlayers) - - // there is already the owner - addPlayers(nbPlayers - 1) - - assertFailsWith { - 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() { - assertEquals(State.LOBBY, lobby.state) - // there is already the owner - addPlayers(gameDefinition.minPlayers - 1) - lobby.startGame() - assertEquals(State.PLAYING, lobby.state) - } - - @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 = intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - - @JvmStatic - @BeforeClass - fun loadDefinition() { - gameDefinition = GameDefinition.load() - } - } -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt deleted file mode 100644 index 446feee6..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.luxons.sevenwonders.repositories - -import org.junit.Before -import org.junit.Test -import org.luxons.sevenwonders.lobby.Player -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlin.test.assertSame -import kotlin.test.assertTrue -import kotlin.test.fail - -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 - fun find_failsOnUnknownId() { - assertFailsWith { - 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 - fun remove_failsOnUnknownId() { - assertFailsWith { - 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt deleted file mode 100644 index aeedc54c..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.luxons.sevenwonders.repositories - -import org.junit.Before -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertSame -import kotlin.test.assertTrue - -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 - fun find_failsOnUnknownUsername() { - assertFailsWith { - 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 - fun remove_failsOnUnknownUsername() { - assertFailsWith { - 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt new file mode 100644 index 00000000..4771076d --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/SevenWondersTest.kt @@ -0,0 +1,146 @@ +package org.luxons.sevenwonders.server + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.luxons.sevenwonders.client.SevenWondersClient +import org.luxons.sevenwonders.client.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 kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@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 suspend fun disconnect(vararg sessions: SevenWondersSession) { + for (session in sessions) { + session.disconnect() + } + } + + @Test + fun chooseName() { + runBlocking { + val session = client.connect(serverUrl) + val playerName = "Test User" + val player = session.chooseName(playerName) + assertNotNull(player) + assertEquals(playerName, player.displayName) + session.disconnect() + } + } + + private suspend fun newPlayer(name: String): SevenWondersSession = client.connect(serverUrl).apply { + chooseName(name) + } + + @Test + fun lobbySubscription_ignoredForOutsiders() { + runBlocking { + 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 (started) = outsiderSession.watchGameStart(lobby.id) + + ownerSession.startGame(lobby.id) + val nothing = withTimeoutOrNull(30) { started.receive() } + assertNull(nothing) + disconnect(ownerSession, session1, session2, outsiderSession) + } + } + + @Test + fun createGame_success() { + runBlocking { + val ownerSession = newPlayer("GameOwner") + + val gameName = "Test Game" + val lobby = ownerSession.createGame(gameName) + assertNotNull(lobby) + assertEquals(gameName, lobby.name) + + disconnect(ownerSession) + } + } + + @Test + fun createGame_seenByConnectedPlayers() { + runBlocking { + val otherSession = newPlayer("OtherPlayer") + val (games) = otherSession.watchGames() + + var receivedLobbies = withTimeout(500) { games.receive() } + assertNotNull(receivedLobbies) + assertEquals(0, receivedLobbies.size) + + val ownerSession = newPlayer("GameOwner") + val gameName = "Test Game" + val createdLobby = ownerSession.createGame(gameName) + + receivedLobbies = withTimeout(500) { games.receive() } + assertNotNull(receivedLobbies) + assertEquals(1, receivedLobbies.size) + val receivedLobby = receivedLobbies[0] + assertEquals(createdLobby.id, receivedLobby.id) + assertEquals(createdLobby.name, receivedLobby.name) + + disconnect(ownerSession, otherSession) + } + } + + @Test + fun startGame_3players() = runBlocking { + 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.receive() + val turn2 = turns2.receive() + val turn3 = turns3.receive() + assertNotNull(turn1) + assertNotNull(turn2) + assertNotNull(turn3) + + disconnect(session1, session2, session3) + } +} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt new file mode 100644 index 00000000..804b5f6e --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/GameBrowserControllerTest.kt @@ -0,0 +1,124 @@ +package org.luxons.sevenwonders.server.controllers + +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.model.api.actions.CreateGameAction +import org.luxons.sevenwonders.model.api.actions.JoinGameAction +import org.luxons.sevenwonders.server.api.toDTO +import org.luxons.sevenwonders.server.controllers.GameBrowserController.UserAlreadyInGameException +import org.luxons.sevenwonders.server.repositories.LobbyRepository +import org.luxons.sevenwonders.server.repositories.PlayerNotFoundException +import org.luxons.sevenwonders.server.repositories.PlayerRepository +import org.luxons.sevenwonders.server.test.mockSimpMessagingTemplate +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class GameBrowserControllerTest { + + private lateinit var playerRepository: PlayerRepository + + private lateinit var gameBrowserController: GameBrowserController + + @Before + fun setUp() { + playerRepository = PlayerRepository() + val lobbyRepository = LobbyRepository() + val template = mockSimpMessagingTemplate() + 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() + assertEquals(lobby, createdLobby) + assertEquals(player.toDTO(principal.name), lobby.players[0]) + } + + @Test + fun createGame_failsForUnknownPlayer() { + val principal = TestPrincipal("unknown") + val action = CreateGameAction("Test Game") + + assertFailsWith { + gameBrowserController.createGame(action, principal) + } + } + + @Test + 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 + assertFailsWith { + 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) + assertEquals(owner.toDTO(ownerPrincipal.name), createdLobby.players[0]) + + val joiner = playerRepository.createOrUpdate("testjoiner", "Test User Joiner") + val joinerPrincipal = TestPrincipal("testjoiner") + val joinGameAction = JoinGameAction(createdLobby.id) + + val joinedLobby = gameBrowserController.joinGame(joinGameAction, joinerPrincipal) + + assertEquals(owner.toDTO(joinerPrincipal.name), joinedLobby.players[0]) + assertEquals(joiner.toDTO(joinerPrincipal.name), joinedLobby.players[1]) + } + + @Test + 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 + assertFailsWith { + gameBrowserController.joinGame(joinGameAction, joinerPrincipal) + } + } +} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/HomeControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/HomeControllerTest.kt new file mode 100644 index 00000000..c2c3c05d --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/HomeControllerTest.kt @@ -0,0 +1,25 @@ +package org.luxons.sevenwonders.server.controllers + +import org.junit.Test +import org.luxons.sevenwonders.model.api.actions.ChooseNameAction +import org.luxons.sevenwonders.server.repositories.PlayerRepository +import kotlin.test.assertEquals + +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) + + assertEquals("testuser", player.username) + assertEquals("Test User", player.displayName) + assertEquals(false, player.isGameOwner) + assertEquals(true, player.isUser) + } +} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt new file mode 100644 index 00000000..e8b2a9c0 --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/LobbyControllerTest.kt @@ -0,0 +1,217 @@ +package org.luxons.sevenwonders.server.controllers + +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.model.api.actions.ReorderPlayersAction +import org.luxons.sevenwonders.model.api.actions.UpdateSettingsAction +import org.luxons.sevenwonders.model.CustomizableSettings +import org.luxons.sevenwonders.model.WonderSidePickMethod.ALL_A +import org.luxons.sevenwonders.server.lobby.Lobby +import org.luxons.sevenwonders.server.lobby.Player +import org.luxons.sevenwonders.server.lobby.PlayerIsNotOwnerException +import org.luxons.sevenwonders.server.lobby.PlayerNotInLobbyException +import org.luxons.sevenwonders.model.api.State +import org.luxons.sevenwonders.server.repositories.LobbyRepository +import org.luxons.sevenwonders.server.repositories.PlayerNotFoundException +import org.luxons.sevenwonders.server.repositories.PlayerRepository +import org.luxons.sevenwonders.server.test.mockSimpMessagingTemplate +import java.util.HashMap +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class LobbyControllerTest { + + private lateinit var playerRepository: PlayerRepository + + private lateinit var lobbyRepository: LobbyRepository + + private lateinit var lobbyController: LobbyController + + @Before + fun setUp() { + val template = mockSimpMessagingTemplate() + 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, lobby.owner) + assertTrue(owner.isInLobby) + assertFalse(owner.isInGame) + } + + @Test + fun leave_failsWhenPlayerDoesNotExist() { + val principal = TestPrincipal("I don't exist") + + assertFailsWith { + lobbyController.leave(principal) + } + } + + @Test + fun leave_failsWhenNotInLobby() { + playerRepository.createOrUpdate("testuser", "Test User") + val principal = TestPrincipal("testuser") + + assertFailsWith { + 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 = listOf(player, player2, player3, player4) + assertEquals(players, lobby.getPlayers()) + + val reorderedPlayers = listOf(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 + 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 = listOf(player3, player, player2) + val playerNames = reorderedPlayers.map { it.username } + val reorderPlayersAction = ReorderPlayersAction(playerNames) + + val principal = TestPrincipal("testuser2") + + assertFailsWith { + 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 + 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") + + assertFailsWith { + 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 + 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") + + assertFailsWith { + 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/TestPrincipal.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/TestPrincipal.kt new file mode 100644 index 00000000..6a57c570 --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/controllers/TestPrincipal.kt @@ -0,0 +1,8 @@ +package org.luxons.sevenwonders.server.controllers + +import java.security.Principal + +internal class TestPrincipal(private val name: String) : Principal { + + override fun getName(): String = name +} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/lobby/LobbyTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/lobby/LobbyTest.kt new file mode 100644 index 00000000..80f5e866 --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/lobby/LobbyTest.kt @@ -0,0 +1,267 @@ +package org.luxons.sevenwonders.server.lobby + +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.BeforeClass +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.runner.RunWith +import org.luxons.sevenwonders.engine.data.GameDefinition +import org.luxons.sevenwonders.model.CustomizableSettings +import org.luxons.sevenwonders.model.api.State +import org.luxons.sevenwonders.server.lobby.Lobby.GameAlreadyStartedException +import org.luxons.sevenwonders.server.lobby.Lobby.PlayerListMismatchException +import org.luxons.sevenwonders.server.lobby.Lobby.PlayerNameAlreadyUsedException +import org.luxons.sevenwonders.server.lobby.Lobby.PlayerOverflowException +import org.luxons.sevenwonders.server.lobby.Lobby.PlayerUnderflowException +import org.luxons.sevenwonders.server.lobby.Lobby.UnknownPlayerException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@RunWith(Theories::class) +class LobbyTest { + + 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 + fun addPlayer_failsOnSameName() { + val player = Player("testuser", "Test User") + val player2 = Player("testuser2", "Test User") + lobby.addPlayer(player) + assertFailsWith { + lobby.addPlayer(player2) + } + } + + @Test + fun addPlayer_playerOverflowWhenTooMany() { + assertFailsWith { + // the owner + the max number gives an overflow + addPlayers(gameDefinition.maxPlayers) + } + } + + @Test + fun addPlayer_failWhenGameStarted() { + // total with owner is the minimum + addPlayers(gameDefinition.minPlayers - 1) + lobby.startGame() + assertFailsWith { + lobby.addPlayer(Player("soonerNextTime", "The Late Guy")) + } + } + + private fun addPlayers(nbPlayers: Int) { + repeat(nbPlayers) { + val player = Player("testuser$it", "Test User $it") + lobby.addPlayer(player) + } + } + + @Test + fun removePlayer_failsWhenNotPresent() { + assertFailsWith { + 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) + + val reorderedUsernames = listOf("testuser3", "gameowner", "testuser1", "testuser2") + lobby.reorderPlayers(reorderedUsernames) + + assertEquals(reorderedUsernames, lobby.getPlayers().map { it.username }) + } + + @Test + fun reorderPlayers_failsOnUnknownPlayer() { + val player1 = Player("testuser1", "Test User 1") + val player2 = Player("testuser2", "Test User 2") + lobby.addPlayer(player1) + lobby.addPlayer(player2) + + assertFailsWith { + lobby.reorderPlayers(listOf("unknown", "testuser2", "gameowner")) + } + } + + @Test + fun reorderPlayers_failsOnExtraPlayer() { + val player1 = Player("testuser1", "Test User 1") + val player2 = Player("testuser2", "Test User 2") + lobby.addPlayer(player1) + lobby.addPlayer(player2) + + assertFailsWith { + lobby.reorderPlayers(listOf("testuser2", "onemore", "testuser1", "gameowner")) + } + } + + @Test + fun reorderPlayers_failsOnMissingPlayer() { + val player1 = Player("testuser1", "Test User 1") + val player2 = Player("testuser2", "Test User 2") + lobby.addPlayer(player1) + lobby.addPlayer(player2) + + assertFailsWith { + lobby.reorderPlayers(listOf("testuser2", "gameowner")) + } + } + + @Theory + fun startGame_failsBelowMinPlayers(nbPlayers: Int) { + assumeTrue(nbPlayers < gameDefinition.minPlayers) + + // there is already the owner + addPlayers(nbPlayers - 1) + + assertFailsWith { + 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() { + assertEquals(State.LOBBY, lobby.state) + // there is already the owner + addPlayers(gameDefinition.minPlayers - 1) + lobby.startGame() + assertEquals(State.PLAYING, lobby.state) + } + + @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 = intArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + + @JvmStatic + @BeforeClass + fun loadDefinition() { + gameDefinition = GameDefinition.load() + } + } +} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepositoryTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepositoryTest.kt new file mode 100644 index 00000000..c59dc49f --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/LobbyRepositoryTest.kt @@ -0,0 +1,78 @@ +package org.luxons.sevenwonders.server.repositories + +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.server.lobby.Player +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +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 + fun find_failsOnUnknownId() { + assertFailsWith { + 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 + fun remove_failsOnUnknownId() { + assertFailsWith { + 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepositoryTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepositoryTest.kt new file mode 100644 index 00000000..fc016903 --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/repositories/PlayerRepositoryTest.kt @@ -0,0 +1,75 @@ +package org.luxons.sevenwonders.server.repositories + +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue + +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 + fun find_failsOnUnknownUsername() { + assertFailsWith { + 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 + fun remove_failsOnUnknownUsername() { + assertFailsWith { + 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/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/test/TestUtils.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/test/TestUtils.kt new file mode 100644 index 00000000..6eaa7e49 --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/test/TestUtils.kt @@ -0,0 +1,10 @@ +package org.luxons.sevenwonders.server.test + +import org.springframework.messaging.Message +import org.springframework.messaging.MessageChannel +import org.springframework.messaging.simp.SimpMessagingTemplate + +fun mockSimpMessagingTemplate(): SimpMessagingTemplate = SimpMessagingTemplate(object : MessageChannel { + override fun send(message: Message<*>): Boolean = true + override fun send(message: Message<*>, timeout: Long): Boolean = true +}) diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidatorTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidatorTest.kt new file mode 100644 index 00000000..c5ef3f44 --- /dev/null +++ b/sw-server/src/test/kotlin/org/luxons/sevenwonders/server/validation/DestinationAccessValidatorTest.kt @@ -0,0 +1,147 @@ +package org.luxons.sevenwonders.server.validation + +import org.junit.Before +import org.junit.Test +import org.luxons.sevenwonders.server.lobby.Lobby +import org.luxons.sevenwonders.server.lobby.Player +import org.luxons.sevenwonders.server.repositories.LobbyNotFoundException +import org.luxons.sevenwonders.server.repositories.LobbyRepository +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +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 + fun validate_failWhenNoLobbyExist() { + assertFailsWith { + destinationAccessValidator.hasAccess("", "/lobby/0") + } + } + + @Test + fun validate_failWhenNoGameExist() { + assertFailsWith { + destinationAccessValidator.hasAccess("", "/game/0") + } + } + + @Test + fun validate_failWhenReferencedLobbyDoesNotExist() { + createLobby("Test Game", "ownerUser1") + createLobby("Test Game 2", "ownerUser2") + assertFailsWith { + destinationAccessValidator.hasAccess("doesNotMatter", "/lobby/3") + } + } + + @Test + fun validate_failWhenReferencedGameDoesNotExist() { + createGame("Test Game 1", "user1", "user2", "user3") + createGame("Test Game 2", "user4", "user5", "user6") + assertFailsWith { + 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")) + } +} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt deleted file mode 100644 index 9f328c5f..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/TestUtils.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.luxons.sevenwonders.test - -import org.springframework.messaging.Message -import org.springframework.messaging.MessageChannel -import org.springframework.messaging.simp.SimpMessagingTemplate - -fun mockSimpMessagingTemplate(): SimpMessagingTemplate = SimpMessagingTemplate(object : MessageChannel { - override fun send(message: Message<*>): Boolean = true - override fun send(message: Message<*>, timeout: Long): Boolean = true -}) diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt deleted file mode 100644 index 95384e06..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersClient.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.luxons.sevenwonders.test.api - -import com.fasterxml.jackson.module.kotlin.KotlinModule -import org.hildan.jackstomp.JackstompClient -import org.luxons.sevenwonders.config.SEVEN_WONDERS_WS_ENDPOINT -import org.springframework.messaging.converter.MappingJackson2MessageConverter -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeoutException - -class SevenWondersClient { - - private val client: JackstompClient - - init { - val mappingJackson2MessageConverter = MappingJackson2MessageConverter() - mappingJackson2MessageConverter.objectMapper.registerModule(KotlinModule()) - - client = JackstompClient() - client.webSocketClient.messageConverter = mappingJackson2MessageConverter - } - - @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) - fun connect(serverUrl: String): SevenWondersSession { - val session = client.syncConnect(serverUrl + SEVEN_WONDERS_WS_ENDPOINT) - return SevenWondersSession(session) - } - - fun stop() { - client.stop() - } -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt deleted file mode 100644 index f02a5c3b..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/test/api/SevenWondersSession.kt +++ /dev/null @@ -1,70 +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.api.LobbyDTO -import org.luxons.sevenwonders.api.PlayerDTO -import org.luxons.sevenwonders.errors.ErrorDTO -import org.luxons.sevenwonders.model.PlayerTurnInfo -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class SevenWondersSession(val jackstompSession: JackstompSession) { - - fun disconnect() { - jackstompSession.disconnect() - } - - fun watchErrors(): Channel = jackstompSession.subscribe("/user/queue/errors", ErrorDTO::class.java) - - @Throws(InterruptedException::class) - fun chooseName(displayName: String): PlayerDTO { - val action = ChooseNameAction(displayName) - return jackstompSession.request(action, PlayerDTO::class.java, "/app/chooseName", "/user/queue/nameChoice") - } - - fun watchGames(): Channel> { - return jackstompSession.subscribe("/topic/games", Array::class.java) - } - - @Throws(InterruptedException::class) - fun createGame(gameName: String): LobbyDTO { - val action = CreateGameAction(gameName) - return jackstompSession.request(action, LobbyDTO::class.java, "/app/lobby/create", "/user/queue/lobby/joined") - } - - @Throws(InterruptedException::class) - fun joinGame(gameId: Long): LobbyDTO { - val action = JoinGameAction(gameId) - val lobby = - jackstompSession.request(action, LobbyDTO::class.java, "/app/lobby/join", "/user/queue/lobby/joined") - assertNotNull(lobby) - assertEquals(gameId, lobby.id) - return lobby - } - - fun watchLobbyUpdates(gameId: Long): Channel = - jackstompSession.subscribe("/topic/lobby/$gameId/updated", LobbyDTO::class.java) - - fun watchLobbyStart(gameId: Long): Channel = - jackstompSession.subscribe("/topic/lobby/$gameId/started", LobbyDTO::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 = - jackstompSession.subscribe("/user/queue/game/turn", PlayerTurnInfo::class.java) -} diff --git a/sw-server/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt b/sw-server/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt deleted file mode 100644 index 85d03e99..00000000 --- a/sw-server/src/test/kotlin/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.kt +++ /dev/null @@ -1,147 +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.LobbyNotFoundException -import org.luxons.sevenwonders.repositories.LobbyRepository -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -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 - fun validate_failWhenNoLobbyExist() { - assertFailsWith { - destinationAccessValidator.hasAccess("", "/lobby/0") - } - } - - @Test - fun validate_failWhenNoGameExist() { - assertFailsWith { - destinationAccessValidator.hasAccess("", "/game/0") - } - } - - @Test - fun validate_failWhenReferencedLobbyDoesNotExist() { - createLobby("Test Game", "ownerUser1") - createLobby("Test Game 2", "ownerUser2") - assertFailsWith { - destinationAccessValidator.hasAccess("doesNotMatter", "/lobby/3") - } - } - - @Test - fun validate_failWhenReferencedGameDoesNotExist() { - createGame("Test Game 1", "user1", "user2", "user3") - createGame("Test Game 2", "user4", "user5", "user6") - assertFailsWith { - 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")) - } -} diff --git a/sw-ui/build.gradle b/sw-ui/build.gradle index 61f01053..201cd6a1 100644 --- a/sw-ui/build.gradle +++ b/sw-ui/build.gradle @@ -4,6 +4,17 @@ plugins { apply plugin: 'base' +// Fix for "Could not find org.nodejs:node:10.15.3" +// Gradle node plugin + Gradle metadata seems to need this fix +// https://github.com/JetBrains/kotlin-native/issues/1612 +repositories.whenObjectAdded { + if (it instanceof IvyArtifactRepository) { + metadataSources { + artifact() + } + } +} + buildDir = 'build' node { -- cgit