test: Add HermesWsClient test (#187)
This commit is contained in:
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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user