fix(core): application-scoped runtimes, strict multi-host singletons, and replay=0 for transient events
This commit is contained in:
parent
7e2512e390
commit
57677b7770
13 changed files with 244 additions and 31 deletions
|
|
@ -106,6 +106,7 @@ dependencies {
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
testImplementation("io.mockk:mockk:1.13.12")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
|
||||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||||
testImplementation("app.cash.turbine:turbine:1.2.0")
|
testImplementation("app.cash.turbine:turbine:1.2.0")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".HermesApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@android:drawable/sym_def_app_icon"
|
android:icon="@android:drawable/sym_def_app_icon"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|
|
||||||
59
app/src/main/java/app/hermes/mobile/HermesAppContainer.kt
Normal file
59
app/src/main/java/app/hermes/mobile/HermesAppContainer.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package app.hermes.mobile
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.hermes.mobile.core.auth.PkceLoopbackAuthManager
|
||||||
|
import app.hermes.mobile.core.network.HermesRestClient
|
||||||
|
import app.hermes.mobile.core.repository.UnifiedSessionRepository
|
||||||
|
import app.hermes.mobile.core.runtime.HermesConnectionManager
|
||||||
|
import app.hermes.mobile.core.security.EncryptedTokenVault
|
||||||
|
import app.hermes.mobile.core.storage.HermesDatabase
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
|
||||||
|
interface AppContainer {
|
||||||
|
val db: HermesDatabase
|
||||||
|
val tokenVault: EncryptedTokenVault
|
||||||
|
val restClient: HermesRestClient
|
||||||
|
val pkceAuthManager: PkceLoopbackAuthManager
|
||||||
|
val connectionManager: HermesConnectionManager
|
||||||
|
val unifiedSessionRepo: UnifiedSessionRepository
|
||||||
|
val applicationScope: CoroutineScope
|
||||||
|
}
|
||||||
|
|
||||||
|
class HermesAppContainer(private val context: Context) : AppContainer {
|
||||||
|
override val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
override val db: HermesDatabase by lazy {
|
||||||
|
HermesDatabase.getInstance(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val tokenVault: EncryptedTokenVault by lazy {
|
||||||
|
EncryptedTokenVault(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val restClient: HermesRestClient by lazy {
|
||||||
|
HermesRestClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val pkceAuthManager: PkceLoopbackAuthManager by lazy {
|
||||||
|
PkceLoopbackAuthManager(restClient, tokenVault)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val connectionManager: HermesConnectionManager by lazy {
|
||||||
|
HermesConnectionManager(
|
||||||
|
hostDao = db.hostDao(),
|
||||||
|
tokenVault = tokenVault,
|
||||||
|
restClient = restClient,
|
||||||
|
scope = applicationScope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val unifiedSessionRepo: UnifiedSessionRepository by lazy {
|
||||||
|
UnifiedSessionRepository(
|
||||||
|
connectionManager = connectionManager,
|
||||||
|
sessionDao = db.unifiedSessionDao(),
|
||||||
|
scope = applicationScope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/src/main/java/app/hermes/mobile/HermesApplication.kt
Normal file
18
app/src/main/java/app/hermes/mobile/HermesApplication.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package app.hermes.mobile
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
|
||||||
|
class HermesApplication : Application() {
|
||||||
|
lateinit var container: AppContainer
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
container = HermesAppContainer(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTerminate() {
|
||||||
|
super.onTerminate()
|
||||||
|
container.applicationScope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,28 +43,19 @@ class MainActivity : ComponentActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
val db = HermesDatabase.getInstance(applicationContext)
|
val container = (applicationContext as HermesApplication).container
|
||||||
|
|
||||||
|
val db = container.db
|
||||||
val hostDao = db.hostDao()
|
val hostDao = db.hostDao()
|
||||||
val sessionDao = db.unifiedSessionDao()
|
|
||||||
val tokenVault = EncryptedTokenVault(applicationContext)
|
|
||||||
val restClient = HermesRestClient()
|
|
||||||
val pkceAuthManager = PkceLoopbackAuthManager(restClient, tokenVault)
|
|
||||||
|
|
||||||
// Migrate legacy connections from DataStore if present
|
// Migrate legacy connections from DataStore if present
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
MigrationHelper.migrateLegacyConnections(applicationContext, hostDao)
|
MigrationHelper.migrateLegacyConnections(applicationContext, hostDao)
|
||||||
}
|
}
|
||||||
|
val tokenVault = container.tokenVault
|
||||||
val connectionManager = HermesConnectionManager(
|
val pkceAuthManager = container.pkceAuthManager
|
||||||
hostDao = hostDao,
|
val connectionManager = container.connectionManager
|
||||||
tokenVault = tokenVault,
|
val unifiedSessionRepo = container.unifiedSessionRepo
|
||||||
restClient = restClient
|
|
||||||
)
|
|
||||||
|
|
||||||
val unifiedSessionRepo = UnifiedSessionRepository(
|
|
||||||
connectionManager = connectionManager,
|
|
||||||
sessionDao = sessionDao
|
|
||||||
)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
HermesAndroidTheme {
|
HermesAndroidTheme {
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class JsonRpcGatewayClient(
|
||||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
private val _events = MutableSharedFlow<GatewayEvent>(replay = 1, extraBufferCapacity = 64)
|
private val _events = MutableSharedFlow<GatewayEvent>(extraBufferCapacity = 64)
|
||||||
val events: SharedFlow<GatewayEvent> = _events.asSharedFlow()
|
val events: SharedFlow<GatewayEvent> = _events.asSharedFlow()
|
||||||
|
|
||||||
private fun nextId(): String = "a${reqCounter.incrementAndGet()}"
|
private fun nextId(): String = "a${reqCounter.incrementAndGet()}"
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,14 @@ class HermesConnectionManager(
|
||||||
val tokenVault: TokenVault,
|
val tokenVault: TokenVault,
|
||||||
val restClient: HermesRestClient = HermesRestClient(),
|
val restClient: HermesRestClient = HermesRestClient(),
|
||||||
val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
|
val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
|
||||||
val runtimeFactory: (HermesHost) -> HermesHostRuntime = { host ->
|
val runtimeFactory: (CoroutineScope, HermesHost) -> HermesHostRuntime = { parentScope, host ->
|
||||||
|
val childScope = CoroutineScope(SupervisorJob(parentScope.coroutineContext[kotlinx.coroutines.Job]) + Dispatchers.Default)
|
||||||
HermesHostRuntime(
|
HermesHostRuntime(
|
||||||
initialHost = host,
|
initialHost = host,
|
||||||
restClient = restClient,
|
restClient = restClient,
|
||||||
gatewayClient = JsonRpcGatewayClient(scope = scope),
|
gatewayClient = JsonRpcGatewayClient(scope = childScope),
|
||||||
tokenVault = tokenVault,
|
tokenVault = tokenVault,
|
||||||
scope = scope
|
scope = childScope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -44,7 +45,7 @@ class HermesConnectionManager(
|
||||||
private val _activeHostId = MutableStateFlow<HermesHostId?>(null)
|
private val _activeHostId = MutableStateFlow<HermesHostId?>(null)
|
||||||
val activeHostId: StateFlow<HermesHostId?> = _activeHostId.asStateFlow()
|
val activeHostId: StateFlow<HermesHostId?> = _activeHostId.asStateFlow()
|
||||||
|
|
||||||
private val _allEvents = MutableSharedFlow<HostGatewayEvent>(replay = 1, extraBufferCapacity = 128)
|
private val _allEvents = MutableSharedFlow<HostGatewayEvent>(extraBufferCapacity = 128)
|
||||||
val allEvents: SharedFlow<HostGatewayEvent> = _allEvents.asSharedFlow()
|
val allEvents: SharedFlow<HostGatewayEvent> = _allEvents.asSharedFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -89,7 +90,7 @@ class HermesConnectionManager(
|
||||||
|
|
||||||
fun getOrCreateRuntime(host: HermesHost): HermesHostRuntime {
|
fun getOrCreateRuntime(host: HermesHost): HermesHostRuntime {
|
||||||
return runtimes.computeIfAbsent(host.id) {
|
return runtimes.computeIfAbsent(host.id) {
|
||||||
val rt = runtimeFactory(host)
|
val rt = runtimeFactory(scope, host)
|
||||||
// Forward events
|
// Forward events
|
||||||
scope.launch {
|
scope.launch {
|
||||||
rt.events.collect { event ->
|
rt.events.collect { event ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package app.hermes.mobile.core.network
|
||||||
|
|
||||||
|
import app.hermes.mobile.core.model.GatewayEvent
|
||||||
|
import app.hermes.mobile.core.model.GatewayEvent.MessageDeltaEvent
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class EventStreamReplayTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNoReplay() = runTest {
|
||||||
|
val client = JsonRpcGatewayClient(scope = this)
|
||||||
|
|
||||||
|
// Emit first event BEFORE subscribing
|
||||||
|
client.handleIncomingMessage("{\"jsonrpc\":\"2.0\",\"method\":\"event\",\"params\":{\"type\":\"message.delta\",\"session_id\":\"s1\",\"payload\":{\"message_id\":\"m1\",\"delta\":\"Hello\"}}}")
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
val events = mutableListOf<GatewayEvent>()
|
||||||
|
val job = launch {
|
||||||
|
client.events.collect { events.add(it) }
|
||||||
|
}
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// Should not have received the first event
|
||||||
|
assertTrue("Events should be empty since we subscribed after the first emission", events.isEmpty())
|
||||||
|
|
||||||
|
// Emit second event AFTER subscribing
|
||||||
|
client.handleIncomingMessage("{\"jsonrpc\":\"2.0\",\"method\":\"event\",\"params\":{\"type\":\"message.delta\",\"session_id\":\"s1\",\"payload\":{\"message_id\":\"m1\",\"delta\":\" World\"}}}")
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
assertEquals("Should have exactly 1 event", 1, events.size)
|
||||||
|
val event = events[0] as GatewayEvent.MessageDeltaEvent
|
||||||
|
assertEquals(" World", event.delta)
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,9 @@ import app.hermes.mobile.core.model.RuntimeSessionId
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
|
|
@ -123,12 +126,18 @@ class JsonRpcGatewayClientTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
val wsUrl = "ws://${server.hostName}:${server.port}/api/ws"
|
val wsUrl = "ws://${server.hostName}:${server.port}/api/ws"
|
||||||
|
|
||||||
|
// Start subscription BEFORE connecting, so we don't miss the event
|
||||||
|
val eventDeferred = async(start = CoroutineStart.UNDISPATCHED) {
|
||||||
|
withTimeout(5000) {
|
||||||
|
client.events.first { it is GatewayEvent.MessageDeltaEvent }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client.connect(wsUrl, allowCleartext = true)
|
client.connect(wsUrl, allowCleartext = true)
|
||||||
client.awaitGatewayReady(5000)
|
client.awaitGatewayReady(5000)
|
||||||
|
|
||||||
val event = withTimeout(5000) {
|
val event = eventDeferred.await()
|
||||||
client.events.first { it is GatewayEvent.MessageDeltaEvent }
|
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(event is GatewayEvent.MessageDeltaEvent)
|
assertTrue(event is GatewayEvent.MessageDeltaEvent)
|
||||||
val deltaEvent = event as GatewayEvent.MessageDeltaEvent
|
val deltaEvent = event as GatewayEvent.MessageDeltaEvent
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,17 @@ class ApprovalRoutingTest {
|
||||||
connectionManager = HermesConnectionManager(
|
connectionManager = HermesConnectionManager(
|
||||||
hostDao = hostDao,
|
hostDao = hostDao,
|
||||||
tokenVault = tokenVault,
|
tokenVault = tokenVault,
|
||||||
scope = CoroutineScope(testDispatcher)
|
scope = CoroutineScope(testDispatcher),
|
||||||
|
runtimeFactory = { parentScope, host ->
|
||||||
|
val childScope = CoroutineScope(kotlinx.coroutines.SupervisorJob(parentScope.coroutineContext[kotlinx.coroutines.Job]) + testDispatcher)
|
||||||
|
HermesHostRuntime(
|
||||||
|
initialHost = host,
|
||||||
|
restClient = app.hermes.mobile.core.network.HermesRestClient(),
|
||||||
|
gatewayClient = JsonRpcGatewayClient(scope = childScope),
|
||||||
|
tokenVault = tokenVault,
|
||||||
|
scope = childScope
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
sessionRepo = UnifiedSessionRepository(
|
sessionRepo = UnifiedSessionRepository(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package app.hermes.mobile.core.repository
|
||||||
import app.hermes.mobile.core.model.*
|
import app.hermes.mobile.core.model.*
|
||||||
import app.hermes.mobile.core.network.ConnectionState
|
import app.hermes.mobile.core.network.ConnectionState
|
||||||
import app.hermes.mobile.core.network.JsonRpcGatewayClient
|
import app.hermes.mobile.core.network.JsonRpcGatewayClient
|
||||||
|
import app.hermes.mobile.core.network.HermesRestClient
|
||||||
import app.hermes.mobile.core.runtime.HermesConnectionManager
|
import app.hermes.mobile.core.runtime.HermesConnectionManager
|
||||||
import app.hermes.mobile.core.runtime.HermesHostRuntime
|
import app.hermes.mobile.core.runtime.HermesHostRuntime
|
||||||
import app.hermes.mobile.core.security.InMemoryTokenVault
|
import app.hermes.mobile.core.security.InMemoryTokenVault
|
||||||
|
|
@ -53,7 +54,17 @@ class MultiHostConcurrencyExecutionTest {
|
||||||
connectionManager = HermesConnectionManager(
|
connectionManager = HermesConnectionManager(
|
||||||
hostDao = hostDao,
|
hostDao = hostDao,
|
||||||
tokenVault = tokenVault,
|
tokenVault = tokenVault,
|
||||||
scope = CoroutineScope(testDispatcher)
|
scope = CoroutineScope(testDispatcher),
|
||||||
|
runtimeFactory = { parentScope, host ->
|
||||||
|
val childScope = CoroutineScope(kotlinx.coroutines.SupervisorJob(parentScope.coroutineContext[kotlinx.coroutines.Job]) + testDispatcher)
|
||||||
|
app.hermes.mobile.core.runtime.HermesHostRuntime(
|
||||||
|
initialHost = host,
|
||||||
|
restClient = app.hermes.mobile.core.network.HermesRestClient(),
|
||||||
|
gatewayClient = app.hermes.mobile.core.network.JsonRpcGatewayClient(scope = childScope),
|
||||||
|
tokenVault = tokenVault,
|
||||||
|
scope = childScope
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
sessionRepo = UnifiedSessionRepository(
|
sessionRepo = UnifiedSessionRepository(
|
||||||
|
|
@ -664,8 +675,10 @@ class MultiHostConcurrencyExecutionTest {
|
||||||
scope = CoroutineScope(Dispatchers.Default)
|
scope = CoroutineScope(Dispatchers.Default)
|
||||||
)
|
)
|
||||||
|
|
||||||
val session = testRepo.createUnifiedSession(title = "Reconnect Test", initialHostId = host1Id)
|
val host1 = HermesHost(id = HermesHostId("host-windows"), displayName = "Server", baseUrl = wsUrl, allowCleartext = true, lastKnownStatus = HostStatus.ONLINE)
|
||||||
val runtime = testConnectionManager.getRuntime(host1Id)!!
|
testConnectionManager.addHost(host1)
|
||||||
|
val session = testRepo.createUnifiedSession(title = "Reconnect Test", initialHostId = HermesHostId("host-windows"))
|
||||||
|
val runtime = testConnectionManager.getRuntime(HermesHostId("host-windows"))!!
|
||||||
|
|
||||||
// Initial connect
|
// Initial connect
|
||||||
runtime.connect()
|
runtime.connect()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import app.hermes.mobile.core.network.ConnectionState
|
||||||
import app.hermes.mobile.core.network.HermesRestClient
|
import app.hermes.mobile.core.network.HermesRestClient
|
||||||
import app.hermes.mobile.core.network.JsonRpcGatewayClient
|
import app.hermes.mobile.core.network.JsonRpcGatewayClient
|
||||||
import app.hermes.mobile.core.runtime.HermesConnectionManager
|
import app.hermes.mobile.core.runtime.HermesConnectionManager
|
||||||
|
import app.hermes.mobile.core.runtime.HermesHostRuntime
|
||||||
import app.hermes.mobile.core.security.InMemoryTokenVault
|
import app.hermes.mobile.core.security.InMemoryTokenVault
|
||||||
import app.hermes.mobile.core.storage.FakeHostDao
|
import app.hermes.mobile.core.storage.FakeHostDao
|
||||||
import app.hermes.mobile.core.storage.FakeUnifiedSessionDao
|
import app.hermes.mobile.core.storage.FakeUnifiedSessionDao
|
||||||
|
|
@ -49,7 +50,17 @@ class UnifiedSessionRepositoryTest {
|
||||||
connectionManager = HermesConnectionManager(
|
connectionManager = HermesConnectionManager(
|
||||||
hostDao = hostDao,
|
hostDao = hostDao,
|
||||||
tokenVault = tokenVault,
|
tokenVault = tokenVault,
|
||||||
scope = CoroutineScope(testDispatcher)
|
scope = CoroutineScope(testDispatcher),
|
||||||
|
runtimeFactory = { parentScope, host ->
|
||||||
|
val childScope = CoroutineScope(kotlinx.coroutines.SupervisorJob(parentScope.coroutineContext[kotlinx.coroutines.Job]) + testDispatcher)
|
||||||
|
app.hermes.mobile.core.runtime.HermesHostRuntime(
|
||||||
|
initialHost = host,
|
||||||
|
restClient = app.hermes.mobile.core.network.HermesRestClient(),
|
||||||
|
gatewayClient = app.hermes.mobile.core.network.JsonRpcGatewayClient(scope = childScope),
|
||||||
|
tokenVault = tokenVault,
|
||||||
|
scope = childScope
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
repository = UnifiedSessionRepository(
|
repository = UnifiedSessionRepository(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package app.hermes.mobile.core.runtime
|
||||||
|
|
||||||
|
import app.hermes.mobile.core.model.HermesHost
|
||||||
|
import app.hermes.mobile.core.model.HermesHostId
|
||||||
|
import app.hermes.mobile.core.model.HostStatus
|
||||||
|
import app.hermes.mobile.core.network.HermesRestClient
|
||||||
|
import app.hermes.mobile.core.security.InMemoryTokenVault
|
||||||
|
import app.hermes.mobile.core.storage.FakeHostDao
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AppLifecycleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSingleHostIdCorrespondsToAtMostOneLiveRuntime() = runTest {
|
||||||
|
val hostDao = FakeHostDao()
|
||||||
|
val tokenVault = InMemoryTokenVault()
|
||||||
|
val restClient = HermesRestClient()
|
||||||
|
|
||||||
|
val connectionManager = HermesConnectionManager(
|
||||||
|
hostDao = hostDao,
|
||||||
|
tokenVault = tokenVault,
|
||||||
|
restClient = restClient,
|
||||||
|
scope = backgroundScope,
|
||||||
|
runtimeFactory = { parentScope, host ->
|
||||||
|
val childScope = kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.SupervisorJob(parentScope.coroutineContext[kotlinx.coroutines.Job]) + kotlinx.coroutines.test.StandardTestDispatcher(testScheduler))
|
||||||
|
app.hermes.mobile.core.runtime.HermesHostRuntime(
|
||||||
|
initialHost = host,
|
||||||
|
restClient = restClient,
|
||||||
|
gatewayClient = app.hermes.mobile.core.network.JsonRpcGatewayClient(scope = childScope),
|
||||||
|
tokenVault = tokenVault,
|
||||||
|
scope = childScope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val host = HermesHost(
|
||||||
|
id = HermesHostId("h1"),
|
||||||
|
displayName = "Host 1",
|
||||||
|
baseUrl = "http://host1.com",
|
||||||
|
allowCleartext = true,
|
||||||
|
enabled = true,
|
||||||
|
lastSeenAt = 0L,
|
||||||
|
lastKnownStatus = HostStatus.OFFLINE
|
||||||
|
)
|
||||||
|
|
||||||
|
val runtime1 = connectionManager.getOrCreateRuntime(host)
|
||||||
|
val runtime2 = connectionManager.getOrCreateRuntime(host)
|
||||||
|
val runtime3 = connectionManager.getRuntime(host.id)
|
||||||
|
|
||||||
|
assertSame(runtime1, runtime2)
|
||||||
|
assertSame(runtime1, runtime3)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue