diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index 36c3f52..2c92f6d 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -38,6 +38,23 @@ + + + + + + + @@ -58,7 +75,12 @@ + + + + + diff --git a/app/src/androidTest/java/com/codenames/frontend/GameboardScreenTest.kt b/app/src/androidTest/java/com/codenames/frontend/GameboardScreenTest.kt index f4b5bf9..e8d3ed9 100644 --- a/app/src/androidTest/java/com/codenames/frontend/GameboardScreenTest.kt +++ b/app/src/androidTest/java/com/codenames/frontend/GameboardScreenTest.kt @@ -48,6 +48,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -71,6 +72,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -91,6 +93,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -122,6 +125,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -156,6 +160,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -179,6 +184,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -205,6 +211,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -236,6 +243,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -258,6 +266,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -279,6 +288,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -300,6 +310,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = {}, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -324,6 +335,7 @@ class GameboardScreenTest { onReveal = {}, onPassTurn = { passTurnClicks++ }, onCheatRequest = {}, + onReturnToHome = {}, ) } @@ -355,6 +367,7 @@ class GameboardScreenTest { onHintChange = { _, _ -> }, onReveal = { positions -> revealedPositions = positions }, onCheatRequest = {}, + onReturnToHome = {}, ) } diff --git a/app/src/androidTest/java/com/codenames/frontend/UserPreferencesDataStoreTest.kt b/app/src/androidTest/java/com/codenames/frontend/UserPreferencesDataStoreTest.kt index a6308b5..76736b8 100644 --- a/app/src/androidTest/java/com/codenames/frontend/UserPreferencesDataStoreTest.kt +++ b/app/src/androidTest/java/com/codenames/frontend/UserPreferencesDataStoreTest.kt @@ -87,4 +87,19 @@ class UserPreferencesDataStoreTest { assertNull(result.lobbyRole) assertNull(result.lobbyTeam) } + + @Test + fun removeUserId_removesUserId() = + runBlocking { + dataStore.saveUserData( + username = "User", + userId = UUID.randomUUID(), + ) + + dataStore.removeUserId() + + val result = dataStore.userData.first() + + assertNull(result.userId) + } } diff --git a/app/src/main/java/com/codenames/frontend/data/datastore/UserPreferencesDataStore.kt b/app/src/main/java/com/codenames/frontend/data/datastore/UserPreferencesDataStore.kt index c2c8586..d079fab 100644 --- a/app/src/main/java/com/codenames/frontend/data/datastore/UserPreferencesDataStore.kt +++ b/app/src/main/java/com/codenames/frontend/data/datastore/UserPreferencesDataStore.kt @@ -80,4 +80,10 @@ class UserPreferencesDataStore prefs.remove(PreferencesKeys.LOBBY_TEAM) } } + + suspend fun removeUserId() { + context.dataStore.edit { prefs -> + prefs.remove(PreferencesKeys.USER_ID) + } + } } diff --git a/app/src/main/java/com/codenames/frontend/data/repository/GameRepository.kt b/app/src/main/java/com/codenames/frontend/data/repository/GameRepository.kt index 35e1206..a5db56b 100644 --- a/app/src/main/java/com/codenames/frontend/data/repository/GameRepository.kt +++ b/app/src/main/java/com/codenames/frontend/data/repository/GameRepository.kt @@ -1,6 +1,5 @@ package com.codenames.frontend.data.repository -import com.codenames.frontend.data.model.enums.Role import com.codenames.frontend.data.model.enums.Team import com.codenames.frontend.network.dto.CheatCardMessage import com.codenames.frontend.network.dto.ClueMessageDto @@ -68,10 +67,8 @@ class GameRepository username: String, userId: UUID, lobbyCode: String, - lobbyRole: Role, - lobbyTeam: Team, ) { - val msg = WebSocketJoinMessage(username, lobbyCode) + val msg = WebSocketJoinMessage(username, lobbyCode, userId.toString()) webSocketHandler.sendReconnectMessage(msg) } diff --git a/app/src/main/java/com/codenames/frontend/data/repository/LobbyRepository.kt b/app/src/main/java/com/codenames/frontend/data/repository/LobbyRepository.kt index fd0f9f6..7a6f504 100644 --- a/app/src/main/java/com/codenames/frontend/data/repository/LobbyRepository.kt +++ b/app/src/main/java/com/codenames/frontend/data/repository/LobbyRepository.kt @@ -5,6 +5,7 @@ import com.codenames.frontend.data.model.enums.Team import com.codenames.frontend.network.api.LobbyApi import com.codenames.frontend.network.dto.LobbyResponse import com.codenames.frontend.network.dto.PlayerDto +import java.util.UUID import javax.inject.Inject class LobbyRepository @@ -16,28 +17,32 @@ class LobbyRepository suspend fun leaveLobby( lobbyCode: String, - username: String, - ): LobbyResponse = api.leaveLobby(lobbyCode, username) + userId: UUID, + ): LobbyResponse = api.leaveLobby(lobbyCode, userId.toString()) suspend fun joinLobby( username: String, lobbyCode: String, - ): LobbyResponse = api.joinLobby(lobbyCode, username) + ): LobbyResponse { + val msg = api.joinLobby(lobbyCode, username) + return msg + } suspend fun getLobbyInfo(lobbyCode: String): LobbyResponse = api.getLobbyInfo(lobbyCode) suspend fun changeRole( username: String, + userId: UUID, lobbyCode: String, role: Role, team: Team, ): LobbyResponse { - val player = PlayerDto(username, role, team, false) + val player = PlayerDto(username, role, team, false, userId.toString()) return api.changeRole(lobbyCode, player) } suspend fun sendStartGame( lobbyCode: String, - username: String, - ): LobbyResponse = api.startGame(lobbyCode, username) + userId: UUID, + ): LobbyResponse = api.startGame(lobbyCode, userId.toString()) } diff --git a/app/src/main/java/com/codenames/frontend/data/repository/SessionRepository.kt b/app/src/main/java/com/codenames/frontend/data/repository/SessionRepository.kt index a4d00ed..1d3d6e3 100644 --- a/app/src/main/java/com/codenames/frontend/data/repository/SessionRepository.kt +++ b/app/src/main/java/com/codenames/frontend/data/repository/SessionRepository.kt @@ -37,4 +37,8 @@ class SessionRepository suspend fun clearLobbyData() { dataStore.clearSessionData() } + + suspend fun clearUserId() { + dataStore.removeUserId() + } } diff --git a/app/src/main/java/com/codenames/frontend/network/api/LobbyApi.kt b/app/src/main/java/com/codenames/frontend/network/api/LobbyApi.kt index ccfb313..c2f6c4d 100644 --- a/app/src/main/java/com/codenames/frontend/network/api/LobbyApi.kt +++ b/app/src/main/java/com/codenames/frontend/network/api/LobbyApi.kt @@ -29,7 +29,7 @@ interface LobbyApi { @GET("lobby/{lobbyCode}/leave") suspend fun leaveLobby( @Path("lobbyCode") lobbyCode: String, - @Query("username") username: String, + @Query("uuid") uuid: String, ): LobbyResponse @POST("lobby/{lobbyCode}/select-position") @@ -41,6 +41,6 @@ interface LobbyApi { @GET("lobby/{lobbyCode}/start-game") suspend fun startGame( @Path("lobbyCode") lobbyCode: String, - @Query("username") username: String, + @Query("uuid") uuid: String, ): LobbyResponse } diff --git a/app/src/main/java/com/codenames/frontend/network/dto/LobbyResponse.kt b/app/src/main/java/com/codenames/frontend/network/dto/LobbyResponse.kt index 09238e5..98f1439 100644 --- a/app/src/main/java/com/codenames/frontend/network/dto/LobbyResponse.kt +++ b/app/src/main/java/com/codenames/frontend/network/dto/LobbyResponse.kt @@ -8,4 +8,5 @@ data class LobbyResponse( val playerList: List? = emptyList(), val isStarted: Boolean = false, val message: String = "", + val uuid: String? = null, ) diff --git a/app/src/main/java/com/codenames/frontend/network/dto/PlayerDto.kt b/app/src/main/java/com/codenames/frontend/network/dto/PlayerDto.kt index c663fa7..602a23d 100644 --- a/app/src/main/java/com/codenames/frontend/network/dto/PlayerDto.kt +++ b/app/src/main/java/com/codenames/frontend/network/dto/PlayerDto.kt @@ -10,4 +10,5 @@ data class PlayerDto( val role: Role? = null, val team: Team? = null, val isHost: Boolean, + val uuid: String? = null, ) diff --git a/app/src/main/java/com/codenames/frontend/network/dto/WebSocketJoinMessage.kt b/app/src/main/java/com/codenames/frontend/network/dto/WebSocketJoinMessage.kt index 2423f49..abef119 100644 --- a/app/src/main/java/com/codenames/frontend/network/dto/WebSocketJoinMessage.kt +++ b/app/src/main/java/com/codenames/frontend/network/dto/WebSocketJoinMessage.kt @@ -9,4 +9,5 @@ data class WebSocketJoinMessage( val username: String, @SerialName("code") val lobbyCode: String, + val uuid: String, ) diff --git a/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketController.kt b/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketController.kt index f131a2d..5682616 100644 --- a/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketController.kt +++ b/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketController.kt @@ -23,6 +23,10 @@ class GameWebSocketController webSocketSessionManager.connectStomp() } + suspend fun disconnect() { + webSocketSessionManager.disconnect() + } + suspend fun startGame(msg: StartGameMessage) { webSocketSessionManager.getSession().convertAndSend("/app/start-game", msg, StartGameMessage.serializer()) } diff --git a/app/src/main/java/com/codenames/frontend/ui/screens/GameScreenWrapper.kt b/app/src/main/java/com/codenames/frontend/ui/screens/GameScreenWrapper.kt index a9c6d17..6464f29 100644 --- a/app/src/main/java/com/codenames/frontend/ui/screens/GameScreenWrapper.kt +++ b/app/src/main/java/com/codenames/frontend/ui/screens/GameScreenWrapper.kt @@ -75,5 +75,10 @@ fun GameScreenWrapper( onSettingsClick = { navController.navigate(Screen.Settings.route) }, + onReturnToHome = { + navController.navigate(Screen.Start.route) + lobbyViewModel.cleanup() + gameViewModel.resetGameState() + }, ) } diff --git a/app/src/main/java/com/codenames/frontend/ui/screens/GameboardScreen.kt b/app/src/main/java/com/codenames/frontend/ui/screens/GameboardScreen.kt index 1ddabdb..4767696 100644 --- a/app/src/main/java/com/codenames/frontend/ui/screens/GameboardScreen.kt +++ b/app/src/main/java/com/codenames/frontend/ui/screens/GameboardScreen.kt @@ -132,6 +132,7 @@ fun GameboardScreen( onPassTurn: () -> Unit = {}, onSendChatMessage: (ChatTab, String) -> Unit = { _, _ -> }, onSettingsClick: (() -> Unit)? = null, + onReturnToHome: (() -> Unit), ) { val dimensions = LocalResponsiveDimensions.current @@ -157,7 +158,6 @@ fun GameboardScreen( userRole == PlayerRoles.BLUE_SPYMASTER || userRole == PlayerRoles.RED_SPYMASTER val isActiveSpymaster = userRole == currentTurn && isSpymaster val canEndTurn = userRole == currentTurn && !isSpymaster && remainingGuesses > 0 - val canSelectCards = canEndTurn val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -172,9 +172,13 @@ fun GameboardScreen( val onInputChange: (String) -> Unit = { hintInput = it } - val currentCanSelectCards by rememberUpdatedState(canSelectCards) + val currentCanSelectCards by rememberUpdatedState(canEndTurn) val currentSelectedCardPositions by rememberUpdatedState(selectedCardPositions) + val gameOver = gameState.winner != null + + val backgroundTeam = if (winner != null) getPlayerRoleFromTeam(winner) else currentTurn + val shakeDetector = remember { ShakeDetector { @@ -206,7 +210,7 @@ fun GameboardScreen( modifier = modifier .fillMaxSize() - .background(getTeamBackgroundColor(currentTurn)), + .background(getTeamBackgroundColor(backgroundTeam)), ) { Column( modifier = @@ -257,7 +261,7 @@ fun GameboardScreen( ), selectionState = BoardSelectionState( - canSelectCards = canSelectCards, + canSelectCards = canEndTurn, selectedCardPositions = selectedCardPositions, remainingGuesses = remainingGuesses, ), @@ -277,18 +281,19 @@ fun GameboardScreen( .weight(1f) .fillMaxWidth(), ) - - HintSection( - isActiveSpymaster, - currentHint, - hintInput, - countInput, - onHintChange = onHintChange, - onInputChange, - onCountChange = { countInput = it }, - keyboardController, - focusManager, - ) + if (!gameOver) { + HintSection( + isActiveSpymaster, + currentHint, + hintInput, + countInput, + onHintChange = onHintChange, + onInputChange, + onCountChange = { countInput = it }, + keyboardController, + focusManager, + ) + } } GameChatOverlay( @@ -315,7 +320,7 @@ fun GameboardScreen( ) DeselectAllButton( - isVisible = canSelectCards && selectedCardPositions.isNotEmpty(), + isVisible = canEndTurn && selectedCardPositions.isNotEmpty(), onClick = { selectedCardPositions = emptyList() }, modifier = Modifier @@ -327,6 +332,22 @@ fun GameboardScreen( ), ) + if (gameOver) { + AppButton( + onClick = onReturnToHome, + text = "Return to home screen", + modifier = + Modifier + .align(Alignment.BottomCenter) + .padding( + start = dimensions.screenPadding, + end = dimensions.screenPadding, + bottom = dimensions.screenPadding, + ), + style = AppButtonStyle(backgroundBrush = greenGradient), + ) + } + onSettingsClick?.let { openSettings -> SettingsCornerButton( onClick = openSettings, @@ -1095,3 +1116,9 @@ fun getTeamBackgroundColor(currentTurn: PlayerRoles): Color = PlayerRoles.RED_OPERATIVE, PlayerRoles.RED_SPYMASTER -> AppRedTeamBackground PlayerRoles.NONE -> AppBackground } + +fun getPlayerRoleFromTeam(color: Team): PlayerRoles = + when (color) { + Team.RED -> PlayerRoles.RED_SPYMASTER + Team.BLUE -> PlayerRoles.BLUE_SPYMASTER + } diff --git a/app/src/main/java/com/codenames/frontend/ui/screens/JoinLobbyScreen.kt b/app/src/main/java/com/codenames/frontend/ui/screens/JoinLobbyScreen.kt index 290112a..863b2c6 100644 --- a/app/src/main/java/com/codenames/frontend/ui/screens/JoinLobbyScreen.kt +++ b/app/src/main/java/com/codenames/frontend/ui/screens/JoinLobbyScreen.kt @@ -76,7 +76,7 @@ fun JoinlobbyScreen( val focusManager = LocalFocusManager.current val state by viewModel.state.collectAsState() - val username by sessionViewModel.userState.collectAsState() + val userState by sessionViewModel.userState.collectAsState() val joinEnabled = isLobbyIdValid(lobbyId) @@ -92,7 +92,7 @@ fun JoinlobbyScreen( keyboardController?.hide() focusManager.clearFocus() - viewModel.joinLobby(username.username, lobbyId) + viewModel.joinLobby(userState.username, lobbyId) } Box( diff --git a/app/src/main/java/com/codenames/frontend/ui/screens/LobbyScreen.kt b/app/src/main/java/com/codenames/frontend/ui/screens/LobbyScreen.kt index a4db508..ca537a9 100644 --- a/app/src/main/java/com/codenames/frontend/ui/screens/LobbyScreen.kt +++ b/app/src/main/java/com/codenames/frontend/ui/screens/LobbyScreen.kt @@ -40,7 +40,6 @@ import com.codenames.frontend.ui.roles.PlayerRoles import com.codenames.frontend.ui.theme.AppBackground import com.codenames.frontend.ui.theme.AppBlack import com.codenames.frontend.ui.theme.AppBlueLight -import com.codenames.frontend.ui.theme.AppMutedDark import com.codenames.frontend.ui.theme.AppRedLight import com.codenames.frontend.ui.theme.AppWhite import com.codenames.frontend.ui.theme.LocalResponsiveDimensions @@ -74,7 +73,7 @@ fun LobbyScreen( val connectionState by gameViewModel.connectionState.collectAsState() val onStartGame = { - viewModel.sendStartGame(userState.username) + viewModel.sendStartGame(userState.username, userState.userId) } LaunchedEffect(userState.userId) { @@ -150,7 +149,7 @@ fun LobbyScreen( gradient = blueGradient, textColor = AppBlueLight, title = "BLUE TEAM", - onRoleSelect = { viewModel.changeRole(it, userState.username) }, + onRoleSelect = { viewModel.changeRole(it, userState.username, userState.userId) }, lobbyUiState = lobbyUiState, ) @@ -173,7 +172,7 @@ fun LobbyScreen( gradient = redGradient, textColor = AppRedLight, title = "RED TEAM", - onRoleSelect = { viewModel.changeRole(it, userState.username) }, + onRoleSelect = { viewModel.changeRole(it, userState.username, userState.userId) }, lobbyUiState = lobbyUiState, ) } @@ -351,25 +350,13 @@ fun GameSettingsColumn( text = "GAME SETTINGS", color = AppWhite, fontWeight = FontWeight.Bold, - fontSize = dimensions.smallFontSize, + fontSize = dimensions.buttonFontSize, modifier = Modifier.padding(bottom = dimensions.itemSpacing), ) - AppButton( - text = "TIMER: OFF", - onClick = { /* TODO: Timer Logik */ }, - modifier = - Modifier - .fillMaxWidth() - .height(dimensions.secondaryButtonHeight) - .padding(bottom = dimensions.smallSpacing), - style = - AppButtonStyle( - containerColor = AppMutedDark, - contentColor = AppWhite, - fontSize = dimensions.smallFontSize, - lineHeight = dimensions.bodyFontSize, - ), + Text( + text = "Upgrade to pro to see all features!", + color = AppWhite, ) } @@ -409,7 +396,7 @@ fun GameSettingsColumn( } } } - viewModel.leaveLobby(username = userState.username, onResult = onResult) + viewModel.leaveLobby(userId = userState.userId, onResult = onResult) }, modifier = Modifier diff --git a/app/src/main/java/com/codenames/frontend/ui/screens/UserNameScreen.kt b/app/src/main/java/com/codenames/frontend/ui/screens/UserNameScreen.kt index fa6e5cd..a2bd72d 100644 --- a/app/src/main/java/com/codenames/frontend/ui/screens/UserNameScreen.kt +++ b/app/src/main/java/com/codenames/frontend/ui/screens/UserNameScreen.kt @@ -38,7 +38,6 @@ import com.codenames.frontend.ui.theme.blueGradient import com.codenames.frontend.viewmodel.GameViewModel import com.codenames.frontend.viewmodel.LobbyViewModel import com.codenames.frontend.viewmodel.SessionViewModel -import java.util.UUID @Suppress("ktlint:standard:function-naming") @Composable @@ -117,7 +116,6 @@ fun UserNameScreen( onClick = { if (username.isBlank()) return@AppButton viewModel.setUsername(username) - viewModel.setUserId(UUID.randomUUID()) // ONLY FOR TESTING!!!! navController.navigate(Screen.Start.route) }, modifier = diff --git a/app/src/main/java/com/codenames/frontend/viewmodel/GameViewModel.kt b/app/src/main/java/com/codenames/frontend/viewmodel/GameViewModel.kt index 4fe495e..e390fc2 100644 --- a/app/src/main/java/com/codenames/frontend/viewmodel/GameViewModel.kt +++ b/app/src/main/java/com/codenames/frontend/viewmodel/GameViewModel.kt @@ -205,7 +205,7 @@ class GameViewModel if (lobbyCode != null && team != null && role != null) { viewModelScope.launch { try { - gameRepository.sendRejoin(username, userId, lobbyCode, role, team) + gameRepository.sendRejoin(username, userId, lobbyCode) } catch (e: Exception) { setConnectionError(e) } @@ -232,6 +232,15 @@ class GameViewModel } } + fun resetGameState() { + viewModelScope.launch { + _uiState.update { + GameState() + } + handler.disconnect() + } + } + private fun setConnectionError(error: Throwable) { _connectionState.value = ConnectionState.Error(ErrorMessageMapper.toUserMessage(error)) } diff --git a/app/src/main/java/com/codenames/frontend/viewmodel/LobbyViewModel.kt b/app/src/main/java/com/codenames/frontend/viewmodel/LobbyViewModel.kt index 9e73915..643b186 100644 --- a/app/src/main/java/com/codenames/frontend/viewmodel/LobbyViewModel.kt +++ b/app/src/main/java/com/codenames/frontend/viewmodel/LobbyViewModel.kt @@ -11,6 +11,7 @@ import com.codenames.frontend.data.model.enums.Role import com.codenames.frontend.data.model.enums.Team import com.codenames.frontend.data.model.toLobbyState import com.codenames.frontend.data.repository.LobbyRepository +import com.codenames.frontend.data.repository.SessionRepository import com.codenames.frontend.ui.roles.PlayerRoles import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -20,13 +21,18 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import java.util.UUID import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +private const val ID_NOT_FOUND = "No valid ID found." @HiltViewModel class LobbyViewModel @Inject constructor( private val repository: LobbyRepository, + private val sessionRepository: SessionRepository, ) : ViewModel() { private val _state = MutableStateFlow(LobbyUiState()) val state: StateFlow = _state @@ -42,13 +48,13 @@ class LobbyViewModel } viewModelScope.launch { setLoading(true) - + sessionRepository.clearUserId() try { val response = repository.createLobby(username) - _state.update { response.toLobbyState() } + if (response.uuid != null) sessionRepository.saveUser(username, UUID.fromString(response.uuid)) startPolling(response.lobbyCode) } catch (e: Exception) { setError(e) @@ -71,12 +77,15 @@ class LobbyViewModel viewModelScope.launch { setLoading(true) + sessionRepository.clearUserId() + try { val response = repository.joinLobby(username, lobbyCode) _state.update { response.toLobbyState() } + if (response.uuid != null) sessionRepository.saveUser(username, UUID.fromString(response.uuid)) updateUiState(_state.value.players) startPolling(response.lobbyCode) } catch (e: Exception) { @@ -88,7 +97,7 @@ class LobbyViewModel } fun leaveLobby( - username: String, + userId: UUID?, onResult: (Boolean) -> Unit, ) { val lobbyCode = _state.value.lobbyCode @@ -99,12 +108,16 @@ class LobbyViewModel onResult(successful) return } + if (userId == null) { + setError(ID_NOT_FOUND) + return + } viewModelScope.launch { setLoading(true) try { - val response = repository.leaveLobby(lobbyCode, username) + val response = repository.leaveLobby(lobbyCode, userId) _state.update { response.toLobbyState() } @@ -126,18 +139,23 @@ class LobbyViewModel role: Role, team: Team, username: String, + userId: UUID?, ) { val lobbyCode = _state.value.lobbyCode if (lobbyCode.isNullOrBlank()) { setError("Not in a Lobby") return } + if (userId == null) { + setError(ID_NOT_FOUND) + return + } viewModelScope.launch { setLoading(true) try { - val response = repository.changeRole(username, lobbyCode, role, team) + val response = repository.changeRole(username, userId, lobbyCode, role, team) _state.update { response.toLobbyState() @@ -154,12 +172,13 @@ class LobbyViewModel fun changeRole( role: PlayerRoles, username: String, + userId: UUID?, ) { when (role) { - PlayerRoles.BLUE_SPYMASTER -> changeRole(role = Role.SPYMASTER, team = Team.BLUE, username = username) - PlayerRoles.RED_SPYMASTER -> changeRole(role = Role.SPYMASTER, team = Team.RED, username = username) - PlayerRoles.BLUE_OPERATIVE -> changeRole(role = Role.OPERATIVE, team = Team.BLUE, username = username) - PlayerRoles.RED_OPERATIVE -> changeRole(role = Role.OPERATIVE, team = Team.RED, username = username) + PlayerRoles.BLUE_SPYMASTER -> changeRole(role = Role.SPYMASTER, team = Team.BLUE, username = username, userId) + PlayerRoles.RED_SPYMASTER -> changeRole(role = Role.SPYMASTER, team = Team.RED, username = username, userId) + PlayerRoles.BLUE_OPERATIVE -> changeRole(role = Role.OPERATIVE, team = Team.BLUE, username = username, userId) + PlayerRoles.RED_OPERATIVE -> changeRole(role = Role.OPERATIVE, team = Team.RED, username = username, userId) else -> setError("Invalid role") } } @@ -201,13 +220,20 @@ class LobbyViewModel return player.isHost } - fun sendStartGame(username: String) { + fun sendStartGame( + username: String, + userId: UUID?, + ) { val lobbyCode = _state.value.lobbyCode.orEmpty() - if (!username.isBlank() && !lobbyCode.isEmpty() && getIsHost(username)) { + if (userId == null) { + setError(ID_NOT_FOUND) + return + } + if (!username.isBlank() && lobbyCode.isNotEmpty() && getIsHost(username)) { viewModelScope.launch { try { setLoading(true) - val response = repository.sendStartGame(lobbyCode, username) + val response = repository.sendStartGame(lobbyCode, userId) _state.update { response.toLobbyState() } @@ -232,7 +258,7 @@ class LobbyViewModel } } - private fun cleanup() { + fun cleanup() { _state.update { it.copy( lobbyCode = null, @@ -243,6 +269,8 @@ class LobbyViewModel redSpymasters = emptyList(), ) } + stopPolling() + clearError() } private fun updateUiState(players: List) { @@ -286,7 +314,7 @@ class LobbyViewModel return@launch } - delay(pollingTime) + delay(pollingTime.milliseconds) } } } diff --git a/app/src/main/java/com/codenames/frontend/viewmodel/SessionViewModel.kt b/app/src/main/java/com/codenames/frontend/viewmodel/SessionViewModel.kt index 9b72566..4e3a923 100644 --- a/app/src/main/java/com/codenames/frontend/viewmodel/SessionViewModel.kt +++ b/app/src/main/java/com/codenames/frontend/viewmodel/SessionViewModel.kt @@ -40,14 +40,6 @@ class SessionViewModel } } - fun setUserId(userId: UUID) { - _userState.update { - it.copy( - userId = userId, - ) - } - } - fun persistLobbyState( lobbyCode: String, role: Role, @@ -71,6 +63,15 @@ class SessionViewModel } } + fun persistUserId(userId: UUID) { + viewModelScope.launch { + sessionRepository.saveUser( + _userState.value.username, + userId, + ) + } + } + fun clearLobby() { viewModelScope.launch { sessionRepository.clearLobbyData() diff --git a/app/src/test/java/com/codenames/frontend/data/repository/GameRepositoryTest.kt b/app/src/test/java/com/codenames/frontend/data/repository/GameRepositoryTest.kt index 018eace..06afd6d 100644 --- a/app/src/test/java/com/codenames/frontend/data/repository/GameRepositoryTest.kt +++ b/app/src/test/java/com/codenames/frontend/data/repository/GameRepositoryTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test +import java.util.UUID @OptIn(ExperimentalCoroutinesApi::class) class GameRepositoryTest { @@ -96,4 +97,19 @@ class GameRepositoryTest { coVerify { webSocketHandler.passTurn(any()) } } + + @Test + fun testSendRejoin() = + runTest { + val username = "User" + val lobbyCode = "ABCDE" + val userId = UUID.randomUUID() + coEvery { webSocketHandler.sendReconnectMessage(any()) } just Runs + + gameRepository.sendRejoin(username, userId, lobbyCode) + + coVerify { + webSocketHandler.sendReconnectMessage(any()) + } + } } diff --git a/app/src/test/java/com/codenames/frontend/data/repository/LobbyRepositoryTest.kt b/app/src/test/java/com/codenames/frontend/data/repository/LobbyRepositoryTest.kt index 0b215ae..a259e43 100644 --- a/app/src/test/java/com/codenames/frontend/data/repository/LobbyRepositoryTest.kt +++ b/app/src/test/java/com/codenames/frontend/data/repository/LobbyRepositoryTest.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import java.util.UUID import kotlin.test.assertFailsWith @OptIn(ExperimentalCoroutinesApi::class) @@ -66,14 +67,15 @@ class LobbyRepositoryTest { runTest { val username = "Max" val lobbyCode = "1234" + val userId = UUID.randomUUID() val response = LobbyResponse(lobbyCode, emptyList(), false) - coEvery { api.leaveLobby(username, lobbyCode) } returns response + coEvery { api.leaveLobby(lobbyCode = lobbyCode, uuid = userId.toString()) } returns response - val result = repository.leaveLobby(username, lobbyCode) + val result = repository.leaveLobby(lobbyCode, userId) - coVerify { api.leaveLobby(username, lobbyCode) } + coVerify { api.leaveLobby(lobbyCode, userId.toString()) } assertEquals(response, result) } @@ -97,12 +99,13 @@ class LobbyRepositoryTest { val lobbyCode = "1234" val role = Role.OPERATIVE val team = Team.RED + val userId = UUID.randomUUID() val response = LobbyResponse(lobbyCode, emptyList(), false) coEvery { api.changeRole(eq(lobbyCode), any()) } returns response - repository.changeRole(username, lobbyCode, role, team) + repository.changeRole(username, userId, lobbyCode, role, team) coVerify { api.changeRole( @@ -133,6 +136,7 @@ class LobbyRepositoryTest { runTest { val lobbyCode = "ABCDE" val username = "Test" + val userId = UUID.randomUUID() val list = listOf( PlayerDto( @@ -145,7 +149,7 @@ class LobbyRepositoryTest { coEvery { api.startGame(any(), any()) } returns LobbyResponse(lobbyCode, list, false) - val response = repository.sendStartGame(lobbyCode, username) + val response = repository.sendStartGame(lobbyCode, userId) coVerify { api.startGame(any(), any()) } assertEquals(response.lobbyCode, lobbyCode) diff --git a/app/src/test/java/com/codenames/frontend/data/repository/SessionRepositoryTest.kt b/app/src/test/java/com/codenames/frontend/data/repository/SessionRepositoryTest.kt index 60d11d4..400d218 100644 --- a/app/src/test/java/com/codenames/frontend/data/repository/SessionRepositoryTest.kt +++ b/app/src/test/java/com/codenames/frontend/data/repository/SessionRepositoryTest.kt @@ -64,4 +64,14 @@ class SessionRepositoryTest { dataStore.clearSessionData() } } + + @Test + fun `clearUserId delegates to datastore`() = + runTest { + repository.clearUserId() + + coVerify { + dataStore.removeUserId() + } + } } diff --git a/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt b/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt index 2038626..56173a3 100644 --- a/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt +++ b/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt @@ -18,6 +18,7 @@ import org.hildan.krossbow.stomp.conversions.kxserialization.convertAndSend import org.hildan.krossbow.stomp.conversions.kxserialization.subscribe import org.junit.Before import org.junit.Test +import java.util.UUID class GameWebSocketControllerTest { private lateinit var wsClient: GameWebSocketController @@ -63,7 +64,7 @@ class GameWebSocketControllerTest { @Test fun testSendReconnectMessageSendsMessage() = runTest { - val msg = WebSocketJoinMessage("name", "1234") + val msg = WebSocketJoinMessage("name", "1234", UUID.randomUUID().toString()) wsClient.sendReconnectMessage(msg) diff --git a/app/src/test/java/com/codenames/frontend/viewmodel/GameViewModelTest.kt b/app/src/test/java/com/codenames/frontend/viewmodel/GameViewModelTest.kt index 927887a..b94cb4f 100644 --- a/app/src/test/java/com/codenames/frontend/viewmodel/GameViewModelTest.kt +++ b/app/src/test/java/com/codenames/frontend/viewmodel/GameViewModelTest.kt @@ -568,7 +568,7 @@ class GameViewModelTest { fun testSendRejoinMessage_sendsMessage() = runTest { coEvery { - gameRepository.sendRejoin(any(), any(), any(), any(), any()) + gameRepository.sendRejoin(any(), any(), any()) } just Runs val rejoinState = RejoinState.Available(SessionState(lobbyCode, Role.OPERATIVE, Team.RED)) @@ -578,7 +578,7 @@ class GameViewModelTest { advanceUntilIdle() coVerify { - gameRepository.sendRejoin(any(), any(), any(), any(), any()) + gameRepository.sendRejoin(any(), any(), any()) } } @@ -586,7 +586,7 @@ class GameViewModelTest { fun testSendRejoinMessage_ignoresWithoutRejoinState() = runTest { coEvery { - gameRepository.sendRejoin(any(), any(), any(), any(), any()) + gameRepository.sendRejoin(any(), any(), any()) } just Runs val rejoinState = null @@ -596,7 +596,7 @@ class GameViewModelTest { advanceUntilIdle() coVerify(exactly = 0) { - gameRepository.sendRejoin(any(), any(), any(), any(), any()) + gameRepository.sendRejoin(any(), any(), any()) } } @@ -604,7 +604,7 @@ class GameViewModelTest { fun testSendRejoinMessage_ignoresNullValuesInRejoinState() = runTest { coEvery { - gameRepository.sendRejoin(any(), any(), any(), any(), any()) + gameRepository.sendRejoin(any(), any(), any()) } just Runs val rejoinState = RejoinState.Available(SessionState(null, null, null)) @@ -614,7 +614,7 @@ class GameViewModelTest { advanceUntilIdle() coVerify(exactly = 0) { - gameRepository.sendRejoin(any(), any(), any(), any(), any()) + gameRepository.sendRejoin(any(), any(), any()) } } @@ -655,6 +655,29 @@ class GameViewModelTest { assertEquals(ConnectionState.IDLE, viewModel.connectionState.value) } + @Test + fun resetGameState_resetsGameState() = + runTest { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + coEvery { client.disconnect() } just Runs + + viewModel.handleMessage(testMessage) + + advanceUntilIdle() + + viewModel.resetGameState() + + advanceUntilIdle() + + val compareMessage = GameState() + + assertEquals(compareMessage.cards, viewModel.uiState.value.cards) + assertEquals(compareMessage.winner, viewModel.uiState.value.winner) + assertEquals(compareMessage.currentTurn, viewModel.uiState.value.currentTurn) + } + @Test fun requestCheat_callsRepository() = runTest { diff --git a/app/src/test/java/com/codenames/frontend/viewmodel/LobbyViewModelTest.kt b/app/src/test/java/com/codenames/frontend/viewmodel/LobbyViewModelTest.kt index 8e428f7..52daa2f 100644 --- a/app/src/test/java/com/codenames/frontend/viewmodel/LobbyViewModelTest.kt +++ b/app/src/test/java/com/codenames/frontend/viewmodel/LobbyViewModelTest.kt @@ -5,12 +5,15 @@ import com.codenames.frontend.data.model.enums.ChatTab import com.codenames.frontend.data.model.enums.Role import com.codenames.frontend.data.model.enums.Team import com.codenames.frontend.data.repository.LobbyRepository +import com.codenames.frontend.data.repository.SessionRepository import com.codenames.frontend.network.dto.LobbyResponse import com.codenames.frontend.network.dto.PlayerDto import com.codenames.frontend.ui.roles.PlayerRoles +import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk @@ -30,10 +33,15 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import java.util.UUID +import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalCoroutinesApi::class) class LobbyViewModelTest { private val dispatcher = StandardTestDispatcher() + val repository = mockk() + val sessionRepository = mockk() @Before fun setup() { @@ -48,8 +56,6 @@ class LobbyViewModelTest { @Test fun testCreateLobby_success() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", @@ -59,12 +65,13 @@ class LobbyViewModelTest { coEvery { repository.createLobby("User") } returns response coEvery { repository.getLobbyInfo(any()) } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("User") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -78,15 +85,14 @@ class LobbyViewModelTest { @Test fun testCreateLobby_error() = runTest { - val repository = mockk() - coEvery { repository.createLobby(any()) } throws RuntimeException("Network error") + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("Max") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -99,8 +105,6 @@ class LobbyViewModelTest { @Test fun testJoinLobby_success() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", @@ -116,11 +120,13 @@ class LobbyViewModelTest { false, ) - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("User", "1234") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -134,15 +140,14 @@ class LobbyViewModelTest { @Test fun testJoinLobby_error() = runTest { - val repository = mockk() - coEvery { repository.joinLobby(any(), any()) } throws RuntimeException("Network error") + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Max", "1234") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -155,8 +160,6 @@ class LobbyViewModelTest { @Test fun testLeaveLobby_success() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", @@ -171,17 +174,20 @@ class LobbyViewModelTest { false, ) + val userId = UUID.randomUUID() + coEvery { repository.joinLobby("User", "1234") } returns response coEvery { repository.getLobbyInfo(any()) } returns response - coEvery { repository.leaveLobby("1234", "User") } returns response2 + coEvery { repository.leaveLobby("1234", userId) } returns response2 + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("User", "1234") - advanceTimeBy(2000) - viewModel.leaveLobby("User", onResult = {}) + advanceTimeBy(2000.milliseconds) + viewModel.leaveLobby(userId = userId, onResult = {}) - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -195,27 +201,27 @@ class LobbyViewModelTest { @Test fun testLeaveLobby_error() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", playerList = listOf(PlayerDto("User", null, Team.RED, true)), false, ) + val userID = UUID.randomUUID() coEvery { repository.joinLobby("User", "1234") } returns response coEvery { repository.leaveLobby(any(), any()) } throws RuntimeException("Network error") + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("User", "1234") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) - viewModel.leaveLobby("User", onResult = {}) + viewModel.leaveLobby(userId = userID, onResult = {}) - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -228,10 +234,9 @@ class LobbyViewModelTest { @Test fun testChangeRole_success() = runTest { - val repository = mockk() - val newRole = Role.OPERATIVE val newTeam = Team.RED + val username = "User" val response = LobbyResponse( @@ -246,21 +251,23 @@ class LobbyViewModelTest { playerList = listOf(PlayerDto("User", newRole, newTeam, true)), false, ) + val userId = UUID.randomUUID() coEvery { repository.joinLobby("User", "1234") } returns response coEvery { repository.getLobbyInfo(any()) } returns response - coEvery { repository.changeRole("User", "1234", newRole, newTeam) } returns response2 + coEvery { repository.changeRole("User", userId, "1234", newRole, newTeam) } returns response2 + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("User", "1234") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) - viewModel.changeRole(newRole, newTeam, "User") + viewModel.changeRole(newRole, newTeam, username, userId) viewModel.stopPollingForTest() - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) val state = viewModel.state.value @@ -274,8 +281,6 @@ class LobbyViewModelTest { @Test fun testChangeRole_error() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", @@ -290,18 +295,21 @@ class LobbyViewModelTest { any(), any(), any(), + any(), ) } throws RuntimeException("Network error") - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("User", "1234") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) - viewModel.changeRole(Role.OPERATIVE, Team.RED, "User") + viewModel.changeRole(Role.OPERATIVE, Team.RED, "User", UUID.randomUUID()) - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -314,15 +322,14 @@ class LobbyViewModelTest { @Test fun testSetError_WithEmptyValue() = runTest { - val repository = mockk() - coEvery { repository.createLobby(any()) } throws RuntimeException("") + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("Max") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -335,15 +342,14 @@ class LobbyViewModelTest { @Test fun testSetError_WithNullValue() = runTest { - val repository = mockk() - coEvery { repository.createLobby(any()) } throws RuntimeException() + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("Max") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -357,8 +363,6 @@ class LobbyViewModelTest { @Test fun testPolling_callsRepositoryRepeatedly() = runTest { - val repository = mockk() - coEvery { repository.getLobbyInfo(any()) } returns LobbyResponse( lobbyCode = "1234", @@ -366,12 +370,11 @@ class LobbyViewModelTest { false, ) - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.startPollingForTest("1234") - advanceTimeBy(2000) - advanceTimeBy(2000) + advanceTimeBy(4000.milliseconds) viewModel.stopPollingForTest() @@ -383,20 +386,17 @@ class LobbyViewModelTest { @Test fun testPolling_stateIsUpdated() = runTest { - val repository = mockk() - coEvery { repository.getLobbyInfo(any()) } returnsMany listOf( LobbyResponse("1", emptyList(), false), LobbyResponse("2", emptyList(), false), ) - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.startPollingForTest("1234") - advanceTimeBy(2000) - advanceTimeBy(2000) + advanceTimeBy(4000.milliseconds) viewModel.stopPollingForTest() @@ -406,8 +406,6 @@ class LobbyViewModelTest { @Test fun testPolling_doesNotStartTwice() = runTest { - val repository = mockk() - coEvery { repository.getLobbyInfo("1234") } returns LobbyResponse( lobbyCode = "1234", @@ -415,16 +413,15 @@ class LobbyViewModelTest { false, ) - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.startPollingForTest("1234") - advanceTimeBy(2000) - advanceTimeBy(2000) + advanceTimeBy(4000.milliseconds) viewModel.startPollingForTest("2345") - advanceTimeBy(2000) + advanceTimeBy(2000.milliseconds) viewModel.stopPollingForTest() @@ -437,8 +434,6 @@ class LobbyViewModelTest { @Test fun testCreateLobby_alreadyInLobby() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", @@ -447,8 +442,9 @@ class LobbyViewModelTest { ) coEvery { repository.createLobby("User") } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("User") @@ -468,8 +464,6 @@ class LobbyViewModelTest { @Test fun testJoinLobby_alreadyInLobby() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "1234", @@ -478,8 +472,9 @@ class LobbyViewModelTest { ) coEvery { repository.createLobby("User") } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("User") @@ -499,11 +494,9 @@ class LobbyViewModelTest { @Test fun testLeaveLobby_notInLobby() = runTest { - val repository = mockk() + val viewModel = LobbyViewModel(repository, sessionRepository) - val viewModel = LobbyViewModel(repository) - - viewModel.leaveLobby("User", onResult = {}) + viewModel.leaveLobby(UUID.randomUUID(), onResult = {}) advanceUntilIdle() @@ -515,13 +508,40 @@ class LobbyViewModelTest { } @Test - fun testChangeRoles_notInLobby() = + fun testLeaveLobby_nullIdDoesNothing() = runTest { - val repository = mockk() + val viewModel = LobbyViewModel(repository, sessionRepository) + val username = "User" + val lobbyCode = "ABCDE" + val onResult = { bool: Boolean -> } + + val response = + LobbyResponse( + lobbyCode = "ABCDE", + playerList = listOf(PlayerDto("User", null, Team.RED, true)), + false, + ) - val viewModel = LobbyViewModel(repository) + coEvery { + repository.joinLobby(username, lobbyCode) + } returns response + coEvery { sessionRepository.clearUserId() } just Runs - viewModel.changeRole(Role.OPERATIVE, Team.RED, "User") + viewModel.joinLobby(username, lobbyCode) + + advanceUntilIdle() + + viewModel.leaveLobby(null, onResult) + + assertNotNull(viewModel.state.value.error) + } + + @Test + fun testChangeRoles_notInLobby() = + runTest { + val viewModel = LobbyViewModel(repository, sessionRepository) + + viewModel.changeRole(Role.OPERATIVE, Team.RED, "User", UUID.randomUUID()) advanceUntilIdle() @@ -534,32 +554,39 @@ class LobbyViewModelTest { @Test fun changeRole_DelegatesCorrectly1() { - val repository = mockk() - val viewModel = spyk(LobbyViewModel(repository), recordPrivateCalls = true) - viewModel.changeRole(PlayerRoles.BLUE_SPYMASTER, "Alice") + val viewModel = spyk(LobbyViewModel(repository, sessionRepository), recordPrivateCalls = true) + viewModel.changeRole(PlayerRoles.BLUE_SPYMASTER, "Alice", UUID.randomUUID()) verify { - viewModel.changeRole(Role.SPYMASTER, Team.BLUE, "Alice") + viewModel.changeRole(Role.SPYMASTER, Team.BLUE, "Alice", any()) } } @Test fun changeRole_DelegatesCorrectly2() { - val repository = mockk() - val viewModel = spyk(LobbyViewModel(repository), recordPrivateCalls = true) + val viewModel = spyk(LobbyViewModel(repository, sessionRepository), recordPrivateCalls = true) - viewModel.changeRole(PlayerRoles.RED_OPERATIVE, "Bob") + viewModel.changeRole(PlayerRoles.RED_OPERATIVE, "Bob", UUID.randomUUID()) verify { - viewModel.changeRole(Role.OPERATIVE, Team.RED, "Bob") + viewModel.changeRole(Role.OPERATIVE, Team.RED, "Bob", any()) } } + @Test + fun changeRole_DoesNothingWithoutId() = + runTest { + val viewModel = LobbyViewModel(repository, sessionRepository) + + viewModel.changeRole(PlayerRoles.RED_SPYMASTER, "User", null) + + assertNotNull(viewModel.state.value.error) + } + @Test fun `getRoleForUser returns BLUE_OPERATIVE`() = runTest { - val repository = mockk() - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) val players = listOf( @@ -582,6 +609,8 @@ class LobbyViewModelTest { repository.joinLobby("Max", "ABCD") } returns response + coEvery { sessionRepository.clearUserId() } just Runs + viewModel.joinLobby("Max", "ABCD") advanceUntilIdle() @@ -594,8 +623,7 @@ class LobbyViewModelTest { @Test fun `getRoleForUser returns NONE when player does not exist`() = runTest { - val repository = mockk() - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) val result = viewModel.getRoleForUser("Unknown") @@ -605,8 +633,7 @@ class LobbyViewModelTest { @Test fun `changeRole updates player role correctly`() = runTest { - val repository = mockk() - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) val initialPlayers = listOf( @@ -642,13 +669,17 @@ class LobbyViewModelTest { false, ) + val userId = UUID.randomUUID() + coEvery { repository.joinLobby("Max", "ABCD") } returns joinResponse + coEvery { sessionRepository.clearUserId() } just Runs coEvery { repository.changeRole( "Max", + userId, "ABCD", Role.SPYMASTER, Team.RED, @@ -662,6 +693,7 @@ class LobbyViewModelTest { role = Role.SPYMASTER, team = Team.RED, username = "Max", + userId = userId, ) advanceUntilIdle() @@ -673,6 +705,7 @@ class LobbyViewModelTest { coVerify(exactly = 1) { repository.changeRole( "Max", + any(), "ABCD", Role.SPYMASTER, Team.RED, @@ -683,13 +716,13 @@ class LobbyViewModelTest { @Test fun `changeRole sets error when not in lobby`() = runTest { - val repository = mockk() - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.changeRole( role = Role.SPYMASTER, team = Team.RED, username = "Max", + userId = UUID.randomUUID(), ) advanceUntilIdle() @@ -702,36 +735,32 @@ class LobbyViewModelTest { @Test fun changeRole_DelegatesCorrectly_redSpymaster() { - val repository = mockk() - val viewModel = spyk(LobbyViewModel(repository)) + val viewModel = spyk(LobbyViewModel(repository, sessionRepository)) - viewModel.changeRole(PlayerRoles.RED_SPYMASTER, "Alice") + viewModel.changeRole(PlayerRoles.RED_SPYMASTER, "Alice", UUID.randomUUID()) verify { - viewModel.changeRole(Role.SPYMASTER, Team.RED, "Alice") + viewModel.changeRole(Role.SPYMASTER, Team.RED, "Alice", any()) } } @Test fun changeRole_DelegatesCorrectly_blueOperative() { - val repository = mockk() - val viewModel = spyk(LobbyViewModel(repository)) + val viewModel = spyk(LobbyViewModel(repository, sessionRepository)) - viewModel.changeRole(PlayerRoles.BLUE_OPERATIVE, "Bob") + viewModel.changeRole(PlayerRoles.BLUE_OPERATIVE, "Bob", UUID.randomUUID()) verify { - viewModel.changeRole(Role.OPERATIVE, Team.BLUE, "Bob") + viewModel.changeRole(Role.OPERATIVE, Team.BLUE, "Bob", any()) } } @Test fun changeRole_invalidRole_setsError() = runTest { - val repository = mockk() + val viewModel = LobbyViewModel(repository, sessionRepository) - val viewModel = LobbyViewModel(repository) - - viewModel.changeRole(PlayerRoles.NONE, "User") + viewModel.changeRole(PlayerRoles.NONE, "User", UUID.randomUUID()) advanceUntilIdle() @@ -743,8 +772,7 @@ class LobbyViewModelTest { @Test fun getRoleForUser_userNotFound_returnsNone() { - val repository = mockk() - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) val result = viewModel.getRoleForUser("Unknown") @@ -754,11 +782,9 @@ class LobbyViewModelTest { @Test fun sendStartGame_blankUsername_doesNothing() = runTest { - val repository = mockk(relaxed = true) - - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) - viewModel.sendStartGame("") + viewModel.sendStartGame("", UUID.randomUUID()) advanceUntilIdle() @@ -768,10 +794,20 @@ class LobbyViewModelTest { } @Test - fun getRoleForUser_blueOperative_returnsCorrectRole() = + fun sendStartGame_NullId_doesNothing() = runTest { - val repository = mockk() + val viewModel = LobbyViewModel(repository, sessionRepository) + viewModel.sendStartGame("User", null) + advanceUntilIdle() + coVerify(exactly = 0) { + repository.sendStartGame("User", any()) + } + } + + @Test + fun getRoleForUser_blueOperative_returnsCorrectRole() = + runTest { val response = LobbyResponse( lobbyCode = "12345", @@ -791,7 +827,9 @@ class LobbyViewModelTest { repository.joinLobby("Alice", "12345") } returns response - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Alice", "12345") @@ -805,8 +843,6 @@ class LobbyViewModelTest { @Test fun getRoleForUser_redSpymaster_returnsCorrectRole() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "12345", @@ -826,7 +862,9 @@ class LobbyViewModelTest { repository.joinLobby("Bob", "12345") } returns response - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Bob", "12345") @@ -840,8 +878,6 @@ class LobbyViewModelTest { @Test fun getRoleForUser_nullRole_returnsNone() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "12345", @@ -861,7 +897,11 @@ class LobbyViewModelTest { repository.joinLobby("Bob", "12345") } returns response - val viewModel = LobbyViewModel(repository) + coEvery { + sessionRepository.clearUserId() + } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Bob", "12345") @@ -875,8 +915,6 @@ class LobbyViewModelTest { @Test fun getIsHost_returnsTrueForHost() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "12345", @@ -896,7 +934,9 @@ class LobbyViewModelTest { repository.joinLobby("Host", "12345") } returns response - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Host", "12345") @@ -908,8 +948,6 @@ class LobbyViewModelTest { @Test fun getIsHost_returnsFalseForNonHost() = runTest { - val repository = mockk() - val response = LobbyResponse( lobbyCode = "12345", @@ -927,7 +965,11 @@ class LobbyViewModelTest { repository.joinLobby("User", "12345") } returns response - val viewModel = LobbyViewModel(repository) + coEvery { + sessionRepository.clearUserId() + } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("User", "12345") @@ -942,8 +984,6 @@ class LobbyViewModelTest { mockkStatic(Log::class) every { Log.d(any(), any()) } returns 0 - val repository = mockk() - val joinResponse = LobbyResponse( lobbyCode = "12345", @@ -969,21 +1009,23 @@ class LobbyViewModelTest { } returns joinResponse coEvery { - repository.sendStartGame("12345", "Host") + repository.sendStartGame("12345", any()) } returns startResponse - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Host", "12345") advanceUntilIdle() - viewModel.sendStartGame("Host") + viewModel.sendStartGame("Host", UUID.randomUUID()) advanceUntilIdle() coVerify { - repository.sendStartGame("12345", "Host") + repository.sendStartGame("12345", any()) } } @@ -993,8 +1035,6 @@ class LobbyViewModelTest { mockkStatic(Log::class) every { Log.d(any(), any()) } returns 0 - val repository = mockk() - val joinResponse = LobbyResponse( lobbyCode = "12345", @@ -1016,13 +1056,15 @@ class LobbyViewModelTest { repository.sendStartGame(any(), any()) } throws RuntimeException("Start failed") - val viewModel = LobbyViewModel(repository) + coEvery { sessionRepository.clearUserId() } just Runs + + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Host", "12345") advanceUntilIdle() - viewModel.sendStartGame("Host") + viewModel.sendStartGame("Host", UUID.randomUUID()) advanceUntilIdle() @@ -1034,8 +1076,7 @@ class LobbyViewModelTest { @Test fun getAvailableChatTabsForUser_unknownUser_returnsGlobalOnly() { - val repository = mockk() - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) val result = viewModel.getAvailableChatTabsForUser("Unknown") @@ -1045,7 +1086,6 @@ class LobbyViewModelTest { @Test fun getAvailableChatTabsForUser_playerWithoutTeam_returnsGlobalOnly() = runTest { - val repository = mockk() val response = LobbyResponse( lobbyCode = "12345", @@ -1055,11 +1095,12 @@ class LobbyViewModelTest { coEvery { repository.joinLobby("Alice", "12345") } returns response coEvery { repository.getLobbyInfo("12345") } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Alice", "12345") - advanceTimeBy(1) + advanceTimeBy(1.milliseconds) viewModel.stopPollingForTest() assertEquals(listOf(ChatTab.GLOBAL), viewModel.getAvailableChatTabsForUser("Alice")) @@ -1068,7 +1109,6 @@ class LobbyViewModelTest { @Test fun getAvailableChatTabsForUser_spymasterWithTeam_returnsGlobalAndTeam() = runTest { - val repository = mockk() val response = LobbyResponse( lobbyCode = "12345", @@ -1078,11 +1118,12 @@ class LobbyViewModelTest { coEvery { repository.joinLobby("Alice", "12345") } returns response coEvery { repository.getLobbyInfo("12345") } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Alice", "12345") - advanceTimeBy(1) + advanceTimeBy(1.milliseconds) viewModel.stopPollingForTest() assertEquals(listOf(ChatTab.GLOBAL, ChatTab.TEAM), viewModel.getAvailableChatTabsForUser("Alice")) @@ -1091,7 +1132,6 @@ class LobbyViewModelTest { @Test fun getAvailableChatTabsForUser_singleOperative_returnsGlobalAndTeam() = runTest { - val repository = mockk() val response = LobbyResponse( lobbyCode = "12345", @@ -1105,11 +1145,12 @@ class LobbyViewModelTest { coEvery { repository.joinLobby("Alice", "12345") } returns response coEvery { repository.getLobbyInfo("12345") } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Alice", "12345") - advanceTimeBy(1) + advanceTimeBy(1.milliseconds) viewModel.stopPollingForTest() assertEquals( @@ -1121,7 +1162,6 @@ class LobbyViewModelTest { @Test fun getAvailableChatTabsForUser_multipleSameTeamOperatives_returnsAllTabs() = runTest { - val repository = mockk() val response = LobbyResponse( lobbyCode = "12345", @@ -1135,11 +1175,12 @@ class LobbyViewModelTest { coEvery { repository.joinLobby("Alice", "12345") } returns response coEvery { repository.getLobbyInfo("12345") } returns response + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.joinLobby("Alice", "12345") - advanceTimeBy(1) + advanceTimeBy(1.milliseconds) viewModel.stopPollingForTest() assertEquals( @@ -1151,11 +1192,10 @@ class LobbyViewModelTest { @Test fun clearError_resetsError() = runTest { - val repository = mockk() - coEvery { repository.createLobby(any()) } throws RuntimeException("Network error") + coEvery { sessionRepository.clearUserId() } just Runs - val viewModel = LobbyViewModel(repository) + val viewModel = LobbyViewModel(repository, sessionRepository) viewModel.createLobby("Max") diff --git a/app/src/test/java/com/codenames/frontend/viewmodel/SessionViewModelTest.kt b/app/src/test/java/com/codenames/frontend/viewmodel/SessionViewModelTest.kt index 43b90c7..f6e6c06 100644 --- a/app/src/test/java/com/codenames/frontend/viewmodel/SessionViewModelTest.kt +++ b/app/src/test/java/com/codenames/frontend/viewmodel/SessionViewModelTest.kt @@ -75,28 +75,20 @@ class SessionViewModelTest { } @Test - fun `setUserId updates user id`() { - val uuid = UUID.randomUUID() - - viewModel.setUserId(uuid) - - assertEquals(uuid, viewModel.userState.value.userId) - } - - @Test - fun `persistUserState saves user`() = + fun `persistUserState saves username`() = runTest { val uuid = UUID.randomUUID() + viewModel.persistUserId(uuid) + viewModel.setUsername("Max") - viewModel.setUserId(uuid) viewModel.persistUserState() advanceUntilIdle() coVerify { - sessionRepository.saveUser("Max", uuid) + sessionRepository.saveUser("Max", any()) } }