test: Add HermesWsClient test (#187)

This commit is contained in:
M57 authored and GitHub committed 2026-06-20 23:56:04 +03:00
1 parent 49fd12ac34
commit 4547861c71
1 file changed
+332
@@ -0,0 +1,332 @@
package com.m57.hermescontrol.data.ws
import android.util.Log
import com.m57.hermescontrol.data.local.AuthManager
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class HermesWsClientTest {
private lateinit var mockWebServer: MockWebServer
@Before
fun setUp() {
mockkStatic(Log::class)
every { Log.d(any<String>(), any<String>()) } returns 0
every { Log.i(any<String>(), any<String>()) } returns 0
every { Log.w(any<String>(), any<String>()) } returns 0
every { Log.e(any<String>(), any<String>(), any()) } returns 0
mockWebServer = MockWebServer()
mockWebServer.start()
mockkObject(AuthManager)
every { AuthManager.wsUrl() } returns mockWebServer.url("/").toString().replace("http://", "ws://")
every { AuthManager.isAutoReconnect() } returns false
// Reset state
val connectedField = HermesWsClient::class.java.getDeclaredField("connected")
connectedField.isAccessible = true
(connectedField.get(HermesWsClient) as java.util.concurrent.atomic.AtomicBoolean).set(false)
val statusField = HermesWsClient::class.java.getDeclaredField("_connectionStatus")
statusField.isAccessible = true
@Suppress("UNCHECKED_CAST")
(statusField.get(HermesWsClient) as MutableStateFlow<ConnectionStatus>).value = ConnectionStatus.DISCONNECTED
HermesWsClient.disconnect() // Ensure it starts clean
}
@After
fun tearDown() {
HermesWsClient.disconnect()
// Wait a bit to allow internal OkHttp coroutines to clean up before shutting down MockWebServer
Thread.sleep(100)
try {
mockWebServer.shutdown()
} catch (e: Exception) {
e.printStackTrace()
}
unmockkAll()
}
@Test
fun testConnectAndSend() {
var serverWebSocket: WebSocket? = null
val serverLatch = CountDownLatch(1)
val messageLatch = CountDownLatch(1)
var receivedMessage: String? = null
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: okhttp3.Response,
) {
serverWebSocket = webSocket
serverLatch.countDown()
}
override fun onMessage(
webSocket: WebSocket,
text: String,
) {
receivedMessage = text
messageLatch.countDown()
}
},
),
)
val clientLatch = CountDownLatch(1)
HermesWsClient.onConnected = {
clientLatch.countDown()
}
HermesWsClient.connect()
assertTrue("Client failed to connect", clientLatch.await(5, TimeUnit.SECONDS))
assertTrue("Server failed to accept connection", serverLatch.await(5, TimeUnit.SECONDS))
assertTrue(HermesWsClient.isConnected)
assertEquals(ConnectionStatus.CONNECTED, HermesWsClient.connectionStatus.value)
// Send a message
val id = HermesWsClient.send("test_method", mapOf("param" to "value"))
// Verify message received by server
assertTrue("Message not received", messageLatch.await(5, TimeUnit.SECONDS))
assertNotNull(receivedMessage)
val msg = receivedMessage ?: ""
assertTrue(msg.contains("test_method"))
assertTrue(msg.contains("value"))
assertTrue(msg.contains(id))
}
@Test
fun testReceiveMessage() {
var serverWebSocket: WebSocket? = null
val serverLatch = CountDownLatch(1)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: okhttp3.Response,
) {
serverWebSocket = webSocket
serverLatch.countDown()
}
},
),
)
val clientLatch = CountDownLatch(1)
HermesWsClient.onConnected = {
clientLatch.countDown()
}
HermesWsClient.connect()
assertTrue(clientLatch.await(5, TimeUnit.SECONDS))
assertTrue(serverLatch.await(5, TimeUnit.SECONDS))
val eventLatch = CountDownLatch(1)
var receivedEvent: WsEvent? = null
HermesWsClient.onMessage = { event ->
receivedEvent = event
eventLatch.countDown()
}
// Server sends a message to client
val jsonResponse =
"""
{
"jsonrpc": "2.0",
"id": "1",
"result": "success"
}
""".trimIndent()
serverWebSocket?.send(jsonResponse)
assertTrue("Event not received", eventLatch.await(5, TimeUnit.SECONDS))
assertTrue(receivedEvent is WsEvent.RpcResult)
assertEquals("1", (receivedEvent as WsEvent.RpcResult).id)
}
@Test
fun testDisconnect() {
val serverLatch = CountDownLatch(1)
val closedLatch = CountDownLatch(1)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: okhttp3.Response,
) {
serverLatch.countDown()
}
override fun onClosing(
webSocket: WebSocket,
code: Int,
reason: String,
) {
closedLatch.countDown()
}
},
),
)
val clientLatch = CountDownLatch(1)
val clientDisconnectedLatch = CountDownLatch(1)
HermesWsClient.onConnected = {
clientLatch.countDown()
}
HermesWsClient.onDisconnected = { _ ->
clientDisconnectedLatch.countDown()
}
HermesWsClient.connect()
assertTrue(clientLatch.await(5, TimeUnit.SECONDS))
assertTrue(serverLatch.await(5, TimeUnit.SECONDS))
HermesWsClient.disconnect()
assertFalse(HermesWsClient.isConnected)
assertEquals(ConnectionStatus.DISCONNECTED, HermesWsClient.connectionStatus.value)
// Verify server received close frame
assertTrue(closedLatch.await(5, TimeUnit.SECONDS))
}
@Test
fun testSendMessage() {
var serverWebSocket: WebSocket? = null
val serverLatch = CountDownLatch(1)
val messageLatch = CountDownLatch(1)
var receivedMessage: String? = null
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: okhttp3.Response,
) {
serverWebSocket = webSocket
serverLatch.countDown()
}
override fun onMessage(
webSocket: WebSocket,
text: String,
) {
receivedMessage = text
messageLatch.countDown()
}
},
),
)
val clientLatch = CountDownLatch(1)
HermesWsClient.onConnected = {
clientLatch.countDown()
}
HermesWsClient.connect()
assertTrue("Client failed to connect", clientLatch.await(5, TimeUnit.SECONDS))
assertTrue("Server failed to accept connection", serverLatch.await(5, TimeUnit.SECONDS))
// Use the convenience method
HermesWsClient.sendMessage("test_session_id", "Hello Hermes!")
// Verify message received by server
assertTrue("Message not received", messageLatch.await(5, TimeUnit.SECONDS))
assertNotNull(receivedMessage)
val msg = receivedMessage ?: ""
assertTrue(msg.contains(WsMethods.PROMPT_SUBMIT))
assertTrue(msg.contains("test_session_id"))
assertTrue(msg.contains("Hello Hermes!"))
}
@Test
fun testAutoReconnect() {
every { AuthManager.isAutoReconnect() } returns true
var serverSocket1: WebSocket? = null
var serverSocket2: WebSocket? = null
val connect1Latch = CountDownLatch(1)
val connect2Latch = CountDownLatch(1)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: okhttp3.Response,
) {
serverSocket1 = webSocket
connect1Latch.countDown()
}
},
),
)
mockWebServer.enqueue(
MockResponse().withWebSocketUpgrade(
object : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: okhttp3.Response,
) {
serverSocket2 = webSocket
connect2Latch.countDown()
}
},
),
)
val clientConnected1Latch = CountDownLatch(1)
val clientDisconnectedLatch = CountDownLatch(1)
HermesWsClient.onConnected = {
clientConnected1Latch.countDown()
}
HermesWsClient.onDisconnected = { _ ->
clientDisconnectedLatch.countDown()
}
HermesWsClient.connect()
assertTrue("Failed initial connection", connect1Latch.await(5, TimeUnit.SECONDS))
assertTrue("Client failed initial connection", clientConnected1Latch.await(5, TimeUnit.SECONDS))
assertEquals(ConnectionStatus.CONNECTED, HermesWsClient.connectionStatus.value)
// Force server to close socket 1 to trigger reconnect
serverSocket1?.close(1001, "Server shutting down")
assertTrue("Client didn't detect disconnect", clientDisconnectedLatch.await(5, TimeUnit.SECONDS))
assertEquals(ConnectionStatus.RECONNECTING, HermesWsClient.connectionStatus.value)
// The client should now attempt to reconnect after initial backoff (1000ms)
// Wait for the second connection to hit the server
assertTrue("Failed to reconnect", connect2Latch.await(5, TimeUnit.SECONDS))
}
}