diff --git a/.idea/misc.xml b/.idea/misc.xml index a1d4940..6c5519f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5e7c7d7..862a3c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,17 @@ +import java.util.Properties +val localProperties = Properties() +val localPropertiesFile = rootProject.file("local.properties") + +if (localPropertiesFile.exists()) { + localProperties.load(localPropertiesFile.inputStream()) +} + +val serverUrl = + localProperties.getProperty( + "SERVER_URL", + "http://10.0.2.2:8080", + ) + plugins { id("com.android.application") alias(libs.plugins.kotlin.compose) @@ -102,11 +116,17 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField( + "String", + "SERVER_URL", + "\"$serverUrl\"", + ) } buildTypes { release { isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", @@ -119,6 +139,7 @@ android { } buildFeatures { compose = true + buildConfig = true } } diff --git a/app/src/main/java/com/codenames/frontend/data/repository/ChatRepository.kt b/app/src/main/java/com/codenames/frontend/data/repository/ChatRepository.kt index b37fe63..fc99a36 100644 --- a/app/src/main/java/com/codenames/frontend/data/repository/ChatRepository.kt +++ b/app/src/main/java/com/codenames/frontend/data/repository/ChatRepository.kt @@ -2,7 +2,7 @@ package com.codenames.frontend.data.repository import com.codenames.frontend.data.model.ChatDomainModel import com.codenames.frontend.network.dto.ChatMessageDto -import com.codenames.frontend.network.websocket.GameWebSocketHandler +import com.codenames.frontend.network.websocket.GameWebSocketController import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject @@ -10,7 +10,7 @@ import javax.inject.Inject class ChatRepository @Inject constructor( - private val webSocketHandler: GameWebSocketHandler, + private val webSocketHandler: GameWebSocketController, ) { fun observeChat( topic: String, 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 0fc6887..51bca92 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,16 +1,34 @@ package com.codenames.frontend.data.repository +import com.codenames.frontend.data.model.enums.Team +import com.codenames.frontend.network.dto.ClueMessageDto import com.codenames.frontend.network.dto.StartGameMessage -import com.codenames.frontend.network.websocket.GameWebSocketHandler +import com.codenames.frontend.network.websocket.GameWebSocketController import javax.inject.Inject class GameRepository @Inject constructor( - private val webSocketHandler: GameWebSocketHandler, + private val webSocketHandler: GameWebSocketController, ) { suspend fun startGame(lobbyCode: String) { val msg = StartGameMessage(lobbyCode) webSocketHandler.startGame(msg) } + + suspend fun submitClue( + lobbyCode: String, + word: String, + count: Int, + currentTurn: Team, + ) { + val msg = + ClueMessageDto( + lobbyCode = lobbyCode, + word = word, + guessAmount = count, + currentTurn = currentTurn, + ) + webSocketHandler.sendClue(msg) + } } diff --git a/app/src/main/java/com/codenames/frontend/network/provider/IpProvider.kt b/app/src/main/java/com/codenames/frontend/network/provider/IpProvider.kt new file mode 100644 index 0000000..c467fcf --- /dev/null +++ b/app/src/main/java/com/codenames/frontend/network/provider/IpProvider.kt @@ -0,0 +1,7 @@ +package com.codenames.frontend.network.provider + +import com.codenames.frontend.BuildConfig + +fun getHttpUrl() = "http://" + BuildConfig.SERVER_URL + +fun getWsUrl() = "ws://" + BuildConfig.SERVER_URL + "ws-fallback" diff --git a/app/src/main/java/com/codenames/frontend/network/provider/RetrofitProvider.kt b/app/src/main/java/com/codenames/frontend/network/provider/RetrofitProvider.kt index 3d1b1f5..52566c2 100644 --- a/app/src/main/java/com/codenames/frontend/network/provider/RetrofitProvider.kt +++ b/app/src/main/java/com/codenames/frontend/network/provider/RetrofitProvider.kt @@ -10,8 +10,6 @@ import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import retrofit2.Retrofit -const val BASE_URL = "http://localhost:8080/" - @Module @InstallIn(SingletonComponent::class) object RetrofitProvider { @@ -25,7 +23,7 @@ object RetrofitProvider { fun provideRetrofit(json: Json): Retrofit = Retrofit .Builder() - .baseUrl(BASE_URL) + .baseUrl(getHttpUrl()) .addConverterFactory( json.asConverterFactory("application/json".toMediaType()), ).build() 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 new file mode 100644 index 0000000..be0a97f --- /dev/null +++ b/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketController.kt @@ -0,0 +1,50 @@ +package com.codenames.frontend.network.websocket + +import com.codenames.frontend.network.dto.ChatMessageDto +import com.codenames.frontend.network.dto.ClueMessageDto +import com.codenames.frontend.network.dto.GameMessage +import com.codenames.frontend.network.dto.StartGameMessage +import com.codenames.frontend.network.dto.WebSocketJoinMessage +import kotlinx.coroutines.flow.Flow +import org.hildan.krossbow.stomp.conversions.kxserialization.convertAndSend +import org.hildan.krossbow.stomp.conversions.kxserialization.subscribe +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GameWebSocketController + @Inject + constructor( + private val webSocketSessionManager: WebSocketSessionManager, + ) { + suspend fun connectStomp() { + webSocketSessionManager.connectStomp() + } + + suspend fun startGame(msg: StartGameMessage) { + webSocketSessionManager.getSession().convertAndSend("/app/start-game", msg, StartGameMessage.serializer()) + } + + @Suppress("kotlin:S6309") + suspend fun subscribeToLobby(lobbyCode: String): Flow = + webSocketSessionManager.getSession().subscribe("/topic/game/$lobbyCode", GameMessage.serializer()) + + suspend fun sendReconnectMessage(msg: WebSocketJoinMessage) { + webSocketSessionManager.getSession().convertAndSend("/app/join", msg, WebSocketJoinMessage.serializer()) + } + + @Suppress("kotlin:S6309") + suspend fun subscribeToChat(topicPath: String): Flow = + webSocketSessionManager.getSession().subscribe(topicPath, ChatMessageDto.serializer()) + + suspend fun sendChatMessage( + destination: String, + msg: ChatMessageDto, + ) { + webSocketSessionManager.getSession().convertAndSend(destination, msg, ChatMessageDto.serializer()) + } + + suspend fun sendClue(msg: ClueMessageDto) { + webSocketSessionManager.getSession().convertAndSend("/app/submit-clue", msg, ClueMessageDto.serializer()) + } + } diff --git a/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketHandler.kt b/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketHandler.kt deleted file mode 100644 index 41a4800..0000000 --- a/app/src/main/java/com/codenames/frontend/network/websocket/GameWebSocketHandler.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.codenames.frontend.network.websocket - -import android.util.Log -import com.codenames.frontend.data.model.enums.Team -import com.codenames.frontend.network.dto.ChatMessageDto -import com.codenames.frontend.network.dto.ClueMessageDto -import com.codenames.frontend.network.dto.GameMessage -import com.codenames.frontend.network.dto.GuessMessage -import com.codenames.frontend.network.dto.StartGameMessage -import com.codenames.frontend.network.dto.WebSocketJoinMessage -import kotlinx.coroutines.flow.Flow -import org.hildan.krossbow.stomp.StompClient -import org.hildan.krossbow.stomp.conversions.kxserialization.StompSessionWithKxSerialization -import org.hildan.krossbow.stomp.conversions.kxserialization.convertAndSend -import org.hildan.krossbow.stomp.conversions.kxserialization.json.withJsonConversions -import org.hildan.krossbow.stomp.conversions.kxserialization.subscribe -import javax.inject.Inject -import javax.inject.Singleton - -const val BASE_URL = "ws://localhost:8080/ws-fallback" - -@Singleton -class GameWebSocketHandler - @Inject - constructor( - private val client: StompClient, - ) { - lateinit var session: StompSessionWithKxSerialization - - suspend fun connectStomp() { - session = client.connect(BASE_URL).withJsonConversions() - Log.d("WebSocket", "Connected to Websocket, session: $session") - } - - suspend fun startGame(msg: StartGameMessage) { - session.convertAndSend("/app/start-game", msg, StartGameMessage.serializer()) - } - - suspend fun sendGuess(msg: GuessMessage) { - session.convertAndSend("/app/game/guess", msg, GuessMessage.serializer()) - } - - @Suppress("kotlin:S6309") - suspend fun subscribeToLobby(lobbyCode: String): Flow = - session.subscribe("/topic/game/$lobbyCode", GameMessage.serializer()) - - suspend fun sendReconnectMessage(msg: WebSocketJoinMessage) { - session.convertAndSend("/app/join", msg, WebSocketJoinMessage.serializer()) - } - - @Suppress("kotlin:S6309") - suspend fun subscribeToChat(topicPath: String): Flow = session.subscribe(topicPath, ChatMessageDto.serializer()) - - suspend fun sendChatMessage( - destination: String, - msg: ChatMessageDto, - ) { - session.convertAndSend(destination, msg, ChatMessageDto.serializer()) - } - - suspend fun sendClue( - lobbyCode: String, - word: String, - guessAmount: Int, - currentTurn: Team, - ) { - val msg = - ClueMessageDto( - lobbyCode = lobbyCode, - word = word, - guessAmount = guessAmount, - currentTurn = currentTurn, - ) - session.convertAndSend("/app/submit-clue", msg, ClueMessageDto.serializer()) - } - } diff --git a/app/src/main/java/com/codenames/frontend/network/websocket/WebSocketSessionManager.kt b/app/src/main/java/com/codenames/frontend/network/websocket/WebSocketSessionManager.kt new file mode 100644 index 0000000..63d7451 --- /dev/null +++ b/app/src/main/java/com/codenames/frontend/network/websocket/WebSocketSessionManager.kt @@ -0,0 +1,41 @@ +package com.codenames.frontend.network.websocket + +import android.util.Log +import com.codenames.frontend.network.provider.getWsUrl +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.hildan.krossbow.stomp.StompClient +import org.hildan.krossbow.stomp.conversions.kxserialization.StompSessionWithKxSerialization +import org.hildan.krossbow.stomp.conversions.kxserialization.json.withJsonConversions + +@Singleton +class WebSocketSessionManager + @Inject + constructor( + val client: StompClient, + ) { + private var session: StompSessionWithKxSerialization? = null + + suspend fun connectStomp() { + if (isConnected()) return + try { + session = client.connect(getWsUrl()).withJsonConversions() + } catch (e: Exception) { + Log.e("WebSocket", "Failed to connect to Websocket", e) + return + } + Log.d("WebSocket", "Connected to Websocket, session: $session") + } + + fun isConnected(): Boolean = session != null + + fun getSession(): StompSessionWithKxSerialization = + requireNotNull(session) { + "WebSocket is not connected" + } + + suspend fun disconnect() { + session?.disconnect() + session = null + } + } 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 68841a1..f7c1b1c 100644 --- a/app/src/main/java/com/codenames/frontend/viewmodel/GameViewModel.kt +++ b/app/src/main/java/com/codenames/frontend/viewmodel/GameViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.codenames.frontend.data.model.ChatLists import com.codenames.frontend.data.model.GameState -import com.codenames.frontend.data.model.enums.CardType import com.codenames.frontend.data.model.enums.ChatTab import com.codenames.frontend.data.model.enums.ConnectionState import com.codenames.frontend.data.model.enums.Role @@ -14,7 +13,7 @@ import com.codenames.frontend.data.model.toGameState import com.codenames.frontend.data.repository.ChatRepository import com.codenames.frontend.data.repository.GameRepository import com.codenames.frontend.network.dto.GameMessage -import com.codenames.frontend.network.websocket.GameWebSocketHandler +import com.codenames.frontend.network.websocket.GameWebSocketController import com.codenames.frontend.ui.roles.PlayerRoles import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -29,7 +28,7 @@ import javax.inject.Inject class GameViewModel @Inject constructor( - private val client: GameWebSocketHandler, + private val handler: GameWebSocketController, private val chatRepository: ChatRepository, private val gameRepository: GameRepository, ) : ViewModel() { @@ -58,14 +57,14 @@ class GameViewModel _connectionState.value = ConnectionState.CONNECTING try { - client.connectStomp() + handler.connectStomp() Log.d("GameViewModel", "Connection successful") _connectionState.value = ConnectionState.CONNECTED launch { - client + handler .subscribeToLobby(lobbyCode) .collect { handleMessage(it) } } @@ -117,38 +116,6 @@ class GameViewModel } } - fun sendLobbyMessage( - lobbyCode: String, - username: String, - content: String, - ) { - viewModelScope.launch { - chatRepository.sendMessage("/app/chat/$lobbyCode", username, content) - } - } - - fun sendTeamMessage( - lobbyCode: String, - team: String, - username: String, - content: String, - ) { - viewModelScope.launch { - chatRepository.sendMessage("/app/chat/$lobbyCode/$team", username, content) - } - } - - fun sendOperativeMessage( - lobbyCode: String, - team: String, - username: String, - content: String, - ) { - viewModelScope.launch { - chatRepository.sendMessage("/app/chat/$lobbyCode/$team/operative", username, content) - } - } - fun submitClue( lobbyCode: String, word: String, @@ -164,13 +131,20 @@ class GameViewModel val team = if (turn == PlayerRoles.BLUE_SPYMASTER) Team.BLUE else Team.RED viewModelScope.launch { try { - client.sendClue(lobbyCode, word, count, team) + gameRepository.submitClue(lobbyCode, word, count, team) } catch (e: Exception) { _connectionState.value = ConnectionState.Error(e.message ?: "Connection error") } } } + fun handleMessage(message: GameMessage) { + val state = message.toGameState() + _uiState.update { + state + } + } + fun sendChatMessage( tab: ChatTab, lobbyCode: String, @@ -213,22 +187,35 @@ class GameViewModel } } - fun handleMessage(message: GameMessage) { - val state = message.toGameState() - _uiState.update { - state + fun sendLobbyMessage( + lobbyCode: String, + username: String, + content: String, + ) { + viewModelScope.launch { + chatRepository.sendMessage("/app/chat/$lobbyCode", username, content) } - Log.d("GameViewModel", "Updated game state: $state") } - fun getCurrentFound(team: CardType): Int { - val cards = _uiState.value.cards - var count = 0 - for (card in cards) { - if (card.type == team && card.revealed) { - count++ - } + fun sendTeamMessage( + lobbyCode: String, + team: String, + username: String, + content: String, + ) { + viewModelScope.launch { + chatRepository.sendMessage("/app/chat/$lobbyCode/$team", username, content) + } + } + + fun sendOperativeMessage( + lobbyCode: String, + team: String, + username: String, + content: String, + ) { + viewModelScope.launch { + chatRepository.sendMessage("/app/chat/$lobbyCode/$team/operative", username, content) } - return count } } diff --git a/app/src/test/java/com/codenames/frontend/data/repository/ChatRepositoryTest.kt b/app/src/test/java/com/codenames/frontend/data/repository/ChatRepositoryTest.kt index 3e6e4fb..28adbf8 100644 --- a/app/src/test/java/com/codenames/frontend/data/repository/ChatRepositoryTest.kt +++ b/app/src/test/java/com/codenames/frontend/data/repository/ChatRepositoryTest.kt @@ -1,7 +1,7 @@ package com.codenames.frontend.data.repository import com.codenames.frontend.network.dto.ChatMessageDto -import com.codenames.frontend.network.websocket.GameWebSocketHandler +import com.codenames.frontend.network.websocket.GameWebSocketController import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify @@ -21,7 +21,7 @@ import org.junit.Test import kotlin.test.assertFailsWith class ChatRepositoryTest { - private lateinit var webSocketHandler: GameWebSocketHandler + private lateinit var webSocketHandler: GameWebSocketController private lateinit var repository: ChatRepository private val testTopic = "/topic/chat" 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 be20743..a2c86e8 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 @@ -1,6 +1,7 @@ package com.codenames.frontend.data.repository -import com.codenames.frontend.network.websocket.GameWebSocketHandler +import com.codenames.frontend.data.model.enums.Team +import com.codenames.frontend.network.websocket.GameWebSocketController import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify @@ -13,7 +14,7 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class GameRepositoryTest { - private val webSocketHandler: GameWebSocketHandler = mockk() + private val webSocketHandler: GameWebSocketController = mockk() private lateinit var gameRepository: GameRepository @Before @@ -31,4 +32,19 @@ class GameRepositoryTest { coVerify { webSocketHandler.startGame(any()) } } + + @Test + fun testSubmitClue() = + runTest { + val lobbyCode = "ABCDE" + val word = "test" + val guessAmount = 2 + val currentTurn = Team.RED + + coEvery { webSocketHandler.sendClue(any()) } just Runs + + gameRepository.submitClue(lobbyCode, word, guessAmount, currentTurn) + + coVerify { webSocketHandler.sendClue(any()) } + } } diff --git a/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketHandlerTest.kt b/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt similarity index 60% rename from app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketHandlerTest.kt rename to app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt index 8e7d3ea..8b0e46c 100644 --- a/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketHandlerTest.kt +++ b/app/src/test/java/com/codenames/frontend/network/websocket/GameWebSocketControllerTest.kt @@ -1,99 +1,53 @@ package com.codenames.frontend.network.websocket -import android.util.Log import com.codenames.frontend.data.model.enums.Team import com.codenames.frontend.network.dto.ChatMessageDto import com.codenames.frontend.network.dto.ClueMessageDto import com.codenames.frontend.network.dto.GameMessage -import com.codenames.frontend.network.dto.GuessMessage import com.codenames.frontend.network.dto.StartGameMessage import com.codenames.frontend.network.dto.WebSocketJoinMessage import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest -import org.hildan.krossbow.stomp.StompClient -import org.hildan.krossbow.stomp.StompSession import org.hildan.krossbow.stomp.conversions.kxserialization.StompSessionWithKxSerialization import org.hildan.krossbow.stomp.conversions.kxserialization.convertAndSend -import org.hildan.krossbow.stomp.conversions.kxserialization.json.withJsonConversions import org.hildan.krossbow.stomp.conversions.kxserialization.subscribe import org.junit.Before import org.junit.Test -class GameWebSocketHandlerTest { - private lateinit var client: StompClient +class GameWebSocketControllerTest { + private lateinit var wsClient: GameWebSocketController + private lateinit var sessionManager: WebSocketSessionManager private lateinit var session: StompSessionWithKxSerialization - private lateinit var wsClient: GameWebSocketHandler @Before fun setup() { - client = mockk() + sessionManager = mockk() session = mockk(relaxed = true) - wsClient = GameWebSocketHandler(client) - wsClient.session = session + wsClient = GameWebSocketController(sessionManager) + + coEvery { sessionManager.getSession() } returns session } @Test - fun testConnectStomp() = + fun testConnect() = runTest { - val client = mockk() - val session = mockk() - val sessionWithJson = mockk() - - mockkStatic(Log::class) - - coEvery { client.connect(BASE_URL) } returns session - every { Log.d(any(), any()) } returns 0 - - coEvery { - sessionWithJson.subscribe(any(), any()) - } returns emptyFlow() - - val wsClient = GameWebSocketHandler(client) + coEvery { sessionManager.connectStomp() } returns Unit wsClient.connectStomp() - coVerify { - client.connect(BASE_URL) - session.withJsonConversions() - } - } - - @Test - fun testSendGuess_sendsCorrectMessage(): Unit = - runTest { - val session = mockk(relaxed = true) - val client = mockk() - - val wsClient = GameWebSocketHandler(client) - wsClient.session = session - - val msg = GuessMessage("name", "word", 1) - - wsClient.sendGuess(msg) - - coVerify { - session.convertAndSend("/app/game/guess", msg, GuessMessage.serializer()) - } + coVerify { sessionManager.connectStomp() } } @Test fun testSubscribeToLobby_subscribesToCorrectTopic(): Unit = runTest { - val session = mockk(relaxed = true) - val client = mockk() - coEvery { session.subscribe("/topic/game/ABCDE") } returns emptyFlow() - val wsClient = GameWebSocketHandler(client) - wsClient.session = session - wsClient.subscribeToLobby("ABCDE") coVerify { @@ -107,12 +61,6 @@ class GameWebSocketHandlerTest { @Test fun testSendReconnectMessageSendsMessage() = runTest { - val session = mockk(relaxed = true) - val client = mockk() - - val wsClient = GameWebSocketHandler(client) - wsClient.session = session - val msg = WebSocketJoinMessage("name", "1234") wsClient.sendReconnectMessage(msg) @@ -167,7 +115,9 @@ class GameWebSocketHandlerTest { val guessAmount = 2 val currentTurn = Team.RED - wsClient.sendClue(lobbyCode, word, guessAmount, currentTurn) + val clueMessage = ClueMessageDto(lobbyCode, word, guessAmount, currentTurn) + + wsClient.sendClue(clueMessage) val expectedMsg = ClueMessageDto( diff --git a/app/src/test/java/com/codenames/frontend/network/websocket/WebSocketSessionManagerTest.kt b/app/src/test/java/com/codenames/frontend/network/websocket/WebSocketSessionManagerTest.kt new file mode 100644 index 0000000..2d97f97 --- /dev/null +++ b/app/src/test/java/com/codenames/frontend/network/websocket/WebSocketSessionManagerTest.kt @@ -0,0 +1,150 @@ +package com.codenames.frontend.network.websocket + +import android.util.Log +import com.codenames.frontend.network.dto.GameMessage +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.hildan.krossbow.stomp.StompClient +import org.hildan.krossbow.stomp.StompSession +import org.hildan.krossbow.stomp.conversions.kxserialization.StompSessionWithKxSerialization +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import kotlin.test.assertNotNull + +class WebSocketSessionManagerTest { + private lateinit var client: StompClient + private lateinit var session: StompSessionWithKxSerialization + private lateinit var rawSession: StompSession + + private val url = "ws://localhost:8080/ws-fallback" + + @Before + fun setup() { + client = mockk() + session = mockk(relaxed = true) + rawSession = mockk(relaxed = true) + + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + every { Log.e(any(), any()) } returns 0 + every { Log.e(any(), any(), any()) } returns 0 + } + + @Test + fun testConnectStomp() = + runTest { + val sessionWithJson = mockk() + + coEvery { client.connect(any()) } returns session + + coEvery { + sessionWithJson.subscribe(any(), any()) + } returns emptyFlow() + + val wsClient = WebSocketSessionManager(client) + + wsClient.connectStomp() + + coVerify { + client.connect(any()) + } + } + + @Test + fun testConnectStomp_throwsExceptionWhenConnectionFails() = + runTest { + coEvery { client.connect(any()) } throws Exception("Connection failed") + + val wsClient = WebSocketSessionManager(client) + + wsClient.connectStomp() + + coVerify(exactly = 1) { client.connect(any()) } + + assertFalse(wsClient.isConnected()) + } + + @Test + fun testConnectStomp_DoesNotConnectWhenSessionExists() = + runTest { + val wsClient = WebSocketSessionManager(client) + coEvery { client.connect(any()) } returns session + wsClient.connectStomp() + wsClient.connectStomp() + coVerify(exactly = 1) { client.connect(any()) } + } + + @Test + fun testDisconnectStomp() = + runTest { + val wsClient = WebSocketSessionManager(client) + + coEvery { client.connect(any()) } returns session + + wsClient.connectStomp() + wsClient.disconnect() + + coVerify { + session.disconnect() + } + } + + @Test + fun testDisconnectStomp_doesNothingWhenSessionDoesNotExist() = + runTest { + val wsClient = WebSocketSessionManager(client) + + wsClient.disconnect() + + coVerify(exactly = 0) { + session.disconnect() + } + } + + @Test + fun testIsConnected_returnsTrueWhenSessionExists() = + runTest { + val wsClient = WebSocketSessionManager(client) + + coEvery { client.connect(any()) } returns session + + wsClient.connectStomp() + + assert(wsClient.isConnected()) + } + + @Test + fun testIsConnected_returnsFalseWhenSessionDoesNotExist() = + runTest { + val wsClient = WebSocketSessionManager(client) + + assert(!wsClient.isConnected()) + } + + @Test + fun testGetSession_returnsSession() = + runTest { + val wsClient = WebSocketSessionManager(client) + + coEvery { client.connect(any()) } returns session + + wsClient.connectStomp() + + assertNotNull(wsClient.getSession()) + } + + @Test + fun testGetSession_throwsWhenSessionIsNull() = + runTest { + val wsClient = WebSocketSessionManager(client) + + assertThrows(IllegalArgumentException::class.java) { wsClient.getSession() } + } +} 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 1b311aa..ae24210 100644 --- a/app/src/test/java/com/codenames/frontend/viewmodel/GameViewModelTest.kt +++ b/app/src/test/java/com/codenames/frontend/viewmodel/GameViewModelTest.kt @@ -13,7 +13,7 @@ import com.codenames.frontend.data.repository.GameRepository import com.codenames.frontend.network.dto.CardDto import com.codenames.frontend.network.dto.ClueDto import com.codenames.frontend.network.dto.GameMessage -import com.codenames.frontend.network.websocket.GameWebSocketHandler +import com.codenames.frontend.network.websocket.GameWebSocketController import com.codenames.frontend.ui.roles.PlayerRoles import io.mockk.Runs import io.mockk.coEvery @@ -65,14 +65,14 @@ class GameViewModelTest { ) private lateinit var viewModel: GameViewModel - private lateinit var client: GameWebSocketHandler + private lateinit var client: GameWebSocketController private lateinit var chatRepository: ChatRepository private lateinit var gameRepository: GameRepository @Before fun setup() { Dispatchers.setMain(testDispatcher) - client = mockk() + client = mockk() chatRepository = mockk(relaxed = true) gameRepository = mockk(relaxed = true) @@ -266,52 +266,6 @@ class GameViewModelTest { ) } - @Test - fun getCurrentFound_returnsCorrectCount() { - val message = - GameMessage( - winner = null, - currentTurn = Team.RED, - currentPhase = Role.OPERATIVE, - currentClue = ClueDto(word = "Animal", guessAmount = 3), - cardList = - listOf( - CardDto("A", CardType.RED, true), - CardDto("B", CardType.RED, false), - CardDto("C", CardType.RED, true), - CardDto("D", CardType.BLUE, true), - ), - ) - - viewModel.handleMessage(message) - - val result = viewModel.getCurrentFound(CardType.RED) - - assertEquals(2, result) - } - - @Test - fun getCurrentFound_returnsZeroWhenNoCardsFound() { - val message = - GameMessage( - winner = null, - currentTurn = Team.RED, - currentPhase = Role.OPERATIVE, - currentClue = ClueDto(word = "Animal", guessAmount = 3), - cardList = - listOf( - CardDto("A", CardType.RED, false), - CardDto("B", CardType.BLUE, false), - ), - ) - - viewModel.handleMessage(message) - - val result = viewModel.getCurrentFound(CardType.RED) - - assertEquals(0, result) - } - @Test fun connect_asHost_shouldStartGame() = runTest { @@ -390,7 +344,7 @@ class GameViewModelTest { fun testSubmitClue_RedSpymaster_Success() = runTest { coEvery { - client.sendClue(any(), any(), any(), any()) + gameRepository.submitClue(any(), any(), any(), any()) } just Runs viewModel.handleMessage(testMessage.copy(currentTurn = Team.RED, currentPhase = Role.SPYMASTER)) @@ -398,22 +352,17 @@ class GameViewModelTest { viewModel.submitClue(lobbyCode, "EAGLE", 2) advanceUntilIdle() - coVerify { client.sendClue(lobbyCode, "EAGLE", 2, Team.RED) } + coVerify { gameRepository.submitClue(lobbyCode, "EAGLE", 2, Team.RED) } } @Test fun testSubmitClue_NetworkError_UpdatesConnectionState() = runTest { coEvery { - client.sendClue(any(), any(), any(), any()) + gameRepository.submitClue(any(), any(), any(), any()) } throws Exception("Network connection failed") - viewModel.handleMessage( - testMessage.copy( - currentTurn = Team.RED, - currentPhase = Role.SPYMASTER, - ), - ) + viewModel.handleMessage(testMessage.copy(currentTurn = Team.RED, currentPhase = Role.SPYMASTER)) viewModel.submitClue(lobbyCode, "EAGLE", 2) advanceUntilIdle() @@ -430,7 +379,7 @@ class GameViewModelTest { viewModel.submitClue(lobbyCode, "EAGLE", 2) advanceUntilIdle() - coVerify(exactly = 0) { client.sendClue(any(), any(), any(), any()) } + coVerify(exactly = 0) { client.sendClue(any()) } } @Test @@ -521,7 +470,7 @@ class GameViewModelTest { advanceUntilIdle() coVerify(exactly = 0) { - client.sendClue(any(), any(), any(), any()) + client.sendClue(any()) } } }