This commit is contained in:
4 files changed
+349
-11
No files matched your search
@@ -2,10 +2,10 @@ package com.m57.hermescontrol.data.remote
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import app.cash.turbine.test
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -15,13 +15,16 @@ import org.junit.Test
|
||||
class NetworkMonitorTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
val defaultNetworkField = NetworkMonitor::class.java.getDeclaredField("defaultNetwork")
|
||||
defaultNetworkField.isAccessible = true
|
||||
defaultNetworkField.set(NetworkMonitor, null)
|
||||
val connectedField = NetworkMonitor::class.java.getDeclaredField("_isConnected")
|
||||
connectedField.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(connectedField.get(NetworkMonitor) as MutableStateFlow<Boolean>).value = true
|
||||
NetworkMonitor.resetForTest()
|
||||
}
|
||||
|
||||
private fun createCapabilities(vararg transports: Int): NetworkCapabilities {
|
||||
val capabilities = mockk<NetworkCapabilities>()
|
||||
every { capabilities.hasTransport(any()) } answers {
|
||||
val transport = firstArg<Int>()
|
||||
transports.contains(transport)
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,4 +97,136 @@ class NetworkMonitorTest {
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial capabilities callback after onAvailable sets baseline and does not emit transport change`() =
|
||||
runTest {
|
||||
val connectivityManager = mockk<ConnectivityManager>()
|
||||
val network = mockk<Network>()
|
||||
val wifiCaps = createCapabilities(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val callback = NetworkMonitor.createCallback(connectivityManager)
|
||||
|
||||
NetworkMonitor.transportChanges.test {
|
||||
callback.onAvailable(network)
|
||||
callback.onCapabilitiesChanged(network, wifiCaps)
|
||||
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities change with different transports emits transport change`() =
|
||||
runTest {
|
||||
val connectivityManager = mockk<ConnectivityManager>()
|
||||
val network = mockk<Network>()
|
||||
val wifiCaps = createCapabilities(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val vpnWifiCaps = createCapabilities(NetworkCapabilities.TRANSPORT_WIFI, NetworkCapabilities.TRANSPORT_VPN)
|
||||
val callback = NetworkMonitor.createCallback(connectivityManager)
|
||||
|
||||
NetworkMonitor.transportChanges.test {
|
||||
callback.onAvailable(network)
|
||||
callback.onCapabilitiesChanged(network, wifiCaps)
|
||||
expectNoEvents()
|
||||
|
||||
callback.onCapabilitiesChanged(network, vpnWifiCaps)
|
||||
assertEquals(Unit, awaitItem())
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities change with identical transports does not emit transport change`() =
|
||||
runTest {
|
||||
val connectivityManager = mockk<ConnectivityManager>()
|
||||
val network = mockk<Network>()
|
||||
val wifiCaps1 = createCapabilities(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val wifiCaps2 = createCapabilities(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val callback = NetworkMonitor.createCallback(connectivityManager)
|
||||
|
||||
NetworkMonitor.transportChanges.test {
|
||||
callback.onAvailable(network)
|
||||
callback.onCapabilitiesChanged(network, wifiCaps1)
|
||||
expectNoEvents()
|
||||
|
||||
callback.onCapabilitiesChanged(network, wifiCaps2)
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `capabilities change on non-default network is ignored`() =
|
||||
runTest {
|
||||
val connectivityManager = mockk<ConnectivityManager>()
|
||||
val defaultNet = mockk<Network>()
|
||||
val otherNet = mockk<Network>()
|
||||
val wifiCaps = createCapabilities(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val cellularCaps = createCapabilities(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val callback = NetworkMonitor.createCallback(connectivityManager)
|
||||
|
||||
NetworkMonitor.transportChanges.test {
|
||||
callback.onAvailable(defaultNet)
|
||||
callback.onCapabilitiesChanged(defaultNet, wifiCaps)
|
||||
expectNoEvents()
|
||||
|
||||
callback.onCapabilitiesChanged(otherNet, cellularCaps)
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onLosing on default network emits transport change`() =
|
||||
runTest {
|
||||
val connectivityManager = mockk<ConnectivityManager>()
|
||||
val defaultNet = mockk<Network>()
|
||||
val callback = NetworkMonitor.createCallback(connectivityManager)
|
||||
|
||||
NetworkMonitor.transportChanges.test {
|
||||
callback.onAvailable(defaultNet)
|
||||
callback.onLosing(defaultNet, 3000)
|
||||
|
||||
assertEquals(Unit, awaitItem())
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onLosing on non-default network is ignored`() =
|
||||
runTest {
|
||||
val connectivityManager = mockk<ConnectivityManager>()
|
||||
val defaultNet = mockk<Network>()
|
||||
val otherNet = mockk<Network>()
|
||||
val callback = NetworkMonitor.createCallback(connectivityManager)
|
||||
|
||||
NetworkMonitor.transportChanges.test {
|
||||
callback.onAvailable(defaultNet)
|
||||
callback.onLosing(otherNet, 3000)
|
||||
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractTransports identifies all supported transport types`() {
|
||||
val caps =
|
||||
createCapabilities(
|
||||
NetworkCapabilities.TRANSPORT_CELLULAR,
|
||||
NetworkCapabilities.TRANSPORT_WIFI,
|
||||
NetworkCapabilities.TRANSPORT_ETHERNET,
|
||||
NetworkCapabilities.TRANSPORT_VPN,
|
||||
NetworkCapabilities.TRANSPORT_BLUETOOTH,
|
||||
NetworkCapabilities.TRANSPORT_WIFI_AWARE,
|
||||
)
|
||||
val transports = NetworkMonitor.extractTransports(caps)
|
||||
assertEquals(
|
||||
setOf(
|
||||
NetworkCapabilities.TRANSPORT_CELLULAR,
|
||||
NetworkCapabilities.TRANSPORT_WIFI,
|
||||
NetworkCapabilities.TRANSPORT_ETHERNET,
|
||||
NetworkCapabilities.TRANSPORT_VPN,
|
||||
NetworkCapabilities.TRANSPORT_BLUETOOTH,
|
||||
NetworkCapabilities.TRANSPORT_WIFI_AWARE,
|
||||
),
|
||||
transports,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,9 @@ class HermesWsClientTest {
|
||||
response: okhttp3.Response,
|
||||
) {
|
||||
webSocket.send(
|
||||
"""{"jsonrpc":"2.0","id":"42","result":{"open_requests":[{"id":"srq-secret","method":"secret","params":{"session_id":"session-1","env_var":"API_KEY","prompt":"Enter API key"}}]}}""",
|
||||
"""{"jsonrpc":"2.0","id":"42","result":{"open_requests":[{"id":"srq-secret",""" +
|
||||
""""method":"secret","params":{"session_id":"session-1","env_var":"API_KEY",""" +
|
||||
""""prompt":"Enter API key"}}]}}""",
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -2013,4 +2015,85 @@ class HermesWsClientTest {
|
||||
assertEquals(latency, HermesWsClient.lastLatencyMs.value)
|
||||
assertTrue(HermesWsClient.lastPongTimestamp > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProbeLivenessOnTransportChangeCancelsSocketWhenPingFails() {
|
||||
val connectedField = HermesWsClient::class.java.getDeclaredField("connected")
|
||||
connectedField.isAccessible = true
|
||||
(connectedField.get(HermesWsClient) as java.util.concurrent.atomic.AtomicBoolean).set(true)
|
||||
|
||||
val failureLatch = CountDownLatch(1)
|
||||
HermesWsClient.probeLivenessOnTransportChange(
|
||||
onFailureAction = {
|
||||
failureLatch.countDown()
|
||||
},
|
||||
)
|
||||
|
||||
assertTrue("Expected failure action to be invoked on failed ping", failureLatch.await(6, TimeUnit.SECONDS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProbeLivenessOnTransportChangeSkipsWhenDisconnected() {
|
||||
val connectedField = HermesWsClient::class.java.getDeclaredField("connected")
|
||||
connectedField.isAccessible = true
|
||||
(connectedField.get(HermesWsClient) as java.util.concurrent.atomic.AtomicBoolean).set(false)
|
||||
|
||||
var failureInvoked = false
|
||||
HermesWsClient.probeLivenessOnTransportChange(
|
||||
onFailureAction = {
|
||||
failureInvoked = true
|
||||
},
|
||||
)
|
||||
|
||||
Thread.sleep(100)
|
||||
assertFalse(failureInvoked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProbeLivenessOnTransportChangeSucceedsWhenServerAnswers() {
|
||||
val serverLatch = CountDownLatch(1)
|
||||
|
||||
mockWebServer.enqueue(
|
||||
MockResponse().withWebSocketUpgrade(
|
||||
object : WebSocketListener() {
|
||||
override fun onOpen(
|
||||
webSocket: WebSocket,
|
||||
response: okhttp3.Response,
|
||||
) {
|
||||
serverLatch.countDown()
|
||||
}
|
||||
|
||||
override fun onMessage(
|
||||
webSocket: WebSocket,
|
||||
text: String,
|
||||
) {
|
||||
if (text.contains(""""method":"gateway.ping"""") || text.contains(""""method":"ping"""")) {
|
||||
val id = Regex(""""id":"([^"]+)"""").find(text)?.groupValues?.get(1) ?: "1"
|
||||
webSocket.send(
|
||||
"""{"jsonrpc":"2.0","id":"$id","result":{"pong":true,"timestamp":1700000000.0}}""",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
HermesWsClient.connect()
|
||||
runBlocking {
|
||||
withTimeout(5000) {
|
||||
HermesWsClient.connectionStatus.first { it == ConnectionStatus.CONNECTED }
|
||||
}
|
||||
}
|
||||
assertTrue(serverLatch.await(5, TimeUnit.SECONDS))
|
||||
|
||||
var failureInvoked = false
|
||||
HermesWsClient.probeLivenessOnTransportChange(
|
||||
onFailureAction = {
|
||||
failureInvoked = true
|
||||
},
|
||||
)
|
||||
|
||||
Thread.sleep(500)
|
||||
assertFalse("Expected socket to remain connected when ping succeeds", failureInvoked)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user