feat(tor): proxy-aware DesktopHttpClient + relay connection wiring
Phase 3: Network plumbing for Tor-routed relay connections. DesktopHttpClient refactored from static singleton to Tor-aware class: - Dual client: direct (30s timeouts) + SOCKS proxy (60s, 2x multiplier) - Per-relay routing: localhost=direct, .onion=proxy, others via evaluator - Shared ConnectionPool across client rebuilds - Companion getSimpleHttpClient() for backward compat (NIP-46 bunker) DesktopRelayConnectionManager now takes DesktopHttpClient instance. Main.kt wires TorManager → DesktopHttpClient → RelayManager. Tests: 10 new DesktopHttpClientTest cases covering Tor on/off, .onion routing, localhost bypass, timeout multipliers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -442,11 +442,33 @@ fun App(
|
|||||||
onShowAddColumnDialog: () -> Unit,
|
onShowAddColumnDialog: () -> Unit,
|
||||||
replyToNote: com.vitorpamplona.quartz.nip01Core.core.Event?,
|
replyToNote: com.vitorpamplona.quartz.nip01Core.core.Event?,
|
||||||
) {
|
) {
|
||||||
val relayManager = remember { DesktopRelayConnectionManager() }
|
|
||||||
val localCache = remember { DesktopLocalCache() }
|
val localCache = remember { DesktopLocalCache() }
|
||||||
val accountState by accountManager.accountState.collectAsState()
|
val accountState by accountManager.accountState.collectAsState()
|
||||||
val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
|
val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main) }
|
||||||
|
|
||||||
|
// Tor support: load settings, create manager, create proxy-aware HTTP client
|
||||||
|
val torSettings =
|
||||||
|
remember {
|
||||||
|
com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||||
|
.load()
|
||||||
|
}
|
||||||
|
val torTypeFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.torType) }
|
||||||
|
val externalPortFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.externalSocksPort) }
|
||||||
|
val torManager =
|
||||||
|
remember {
|
||||||
|
com.vitorpamplona.amethyst.desktop.tor
|
||||||
|
.DesktopTorManager(torTypeFlow, externalPortFlow, scope)
|
||||||
|
}
|
||||||
|
val httpClient =
|
||||||
|
remember {
|
||||||
|
com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient(
|
||||||
|
torManager = torManager,
|
||||||
|
shouldUseTorForRelay = { false }, // TODO: wire TorRelayEvaluation when settings UI is built
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val relayManager = remember { DesktopRelayConnectionManager(httpClient) }
|
||||||
|
|
||||||
// Subscriptions coordinator — uses default relay URLs for metadata indexing.
|
// Subscriptions coordinator — uses default relay URLs for metadata indexing.
|
||||||
// Feed subscriptions (inside MainContent) drive actual relay pool connections.
|
// Feed subscriptions (inside MainContent) drive actual relay pool connections.
|
||||||
val subscriptionsCoordinator =
|
val subscriptionsCoordinator =
|
||||||
|
|||||||
+1
-1
@@ -138,7 +138,7 @@ class AccountManager internal constructor(
|
|||||||
private suspend fun getOrCreateNip46Client(): INostrClient =
|
private suspend fun getOrCreateNip46Client(): INostrClient =
|
||||||
nip46ClientMutex.withLock {
|
nip46ClientMutex.withLock {
|
||||||
nip46Client ?: NostrClient(
|
nip46Client ?: NostrClient(
|
||||||
BasicOkHttpWebSocket.Builder(DesktopHttpClient::getHttpClient),
|
BasicOkHttpWebSocket.Builder(DesktopHttpClient::getSimpleHttpClient),
|
||||||
).also {
|
).also {
|
||||||
nip46Client = it
|
nip46Client = it
|
||||||
it.connect()
|
it.connect()
|
||||||
|
|||||||
+104
-7
@@ -20,21 +20,118 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.desktop.network
|
package com.vitorpamplona.amethyst.desktop.network
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.ITorManager
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import okhttp3.ConnectionPool
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
object DesktopHttpClient {
|
/**
|
||||||
private val client: OkHttpClient by lazy {
|
* Desktop HTTP client with optional SOCKS proxy support for Tor.
|
||||||
|
*
|
||||||
|
* Maintains two clients: one with SOCKS proxy (for Tor-routed traffic)
|
||||||
|
* and one without (for direct connections). Selects the appropriate
|
||||||
|
* client per relay URL based on Tor settings.
|
||||||
|
*
|
||||||
|
* When Tor is OFF, all relays use the direct client.
|
||||||
|
* When Tor is ON, .onion relays always use the proxy client.
|
||||||
|
* Localhost relays always use the direct client.
|
||||||
|
*/
|
||||||
|
class DesktopHttpClient(
|
||||||
|
torManager: ITorManager,
|
||||||
|
private val shouldUseTorForRelay: (NormalizedRelayUrl) -> Boolean,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
private val sharedConnectionPool = ConnectionPool()
|
||||||
|
|
||||||
|
private val directClient: OkHttpClient by lazy {
|
||||||
OkHttpClient
|
OkHttpClient
|
||||||
.Builder()
|
.Builder()
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectionPool(sharedConnectionPool)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
.writeTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
.pingInterval(30, TimeUnit.SECONDS)
|
.writeTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.pingInterval(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
.retryOnConnectionFailure(true)
|
.retryOnConnectionFailure(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHttpClient(url: NormalizedRelayUrl): OkHttpClient = client
|
/** Proxy client, rebuilt when SOCKS port changes. */
|
||||||
|
val proxyClient: StateFlow<OkHttpClient?> =
|
||||||
|
torManager.activePortOrNull
|
||||||
|
.map { port ->
|
||||||
|
if (port == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port))
|
||||||
|
val torTimeout = BASE_TIMEOUT_SECONDS * TOR_TIMEOUT_MULTIPLIER
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.connectionPool(sharedConnectionPool)
|
||||||
|
.proxy(proxy)
|
||||||
|
.connectTimeout(torTimeout, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(torTimeout, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(torTimeout, TimeUnit.SECONDS)
|
||||||
|
.pingInterval(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}.stateIn(scope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the appropriate OkHttpClient for the given relay URL.
|
||||||
|
*
|
||||||
|
* - Localhost relays → always direct (no proxy)
|
||||||
|
* - .onion relays when Tor active → proxy client
|
||||||
|
* - Other relays → based on TorRelayEvaluation routing decision
|
||||||
|
* - If proxy needed but not available (Tor bootstrapping) → direct client as fallback
|
||||||
|
*/
|
||||||
|
fun getHttpClient(url: NormalizedRelayUrl): OkHttpClient {
|
||||||
|
if (url.isLocalHost()) return directClient
|
||||||
|
|
||||||
|
val torActive = proxyClient.value != null
|
||||||
|
if (!torActive) return directClient
|
||||||
|
|
||||||
|
// .onion always needs Tor
|
||||||
|
if (url.isOnion()) return proxyClient.value ?: directClient
|
||||||
|
|
||||||
|
// Delegate to TorRelayEvaluation for routing decision
|
||||||
|
return if (shouldUseTorForRelay(url)) {
|
||||||
|
proxyClient.value ?: directClient
|
||||||
|
} else {
|
||||||
|
directClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the direct (non-proxy) client for non-relay HTTP traffic. */
|
||||||
|
fun getNonProxyClient(): OkHttpClient = directClient
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_TIMEOUT_SECONDS = 30L
|
||||||
|
private const val TOR_TIMEOUT_MULTIPLIER = 2 // Desktop: 2x, not Android's 3x
|
||||||
|
|
||||||
|
/** Simple direct client for code that doesn't need Tor routing (e.g. NIP-46 bunker). */
|
||||||
|
private val simpleClient: OkHttpClient by lazy {
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.connectTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.pingInterval(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Backward-compatible static accessor for code that doesn't need Tor. */
|
||||||
|
fun getSimpleHttpClient(url: NormalizedRelayUrl): OkHttpClient = simpleClient
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -24,9 +24,11 @@ import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSoc
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Desktop-specific relay connection manager that configures OkHttp for websockets.
|
* Desktop-specific relay connection manager that configures OkHttp for websockets.
|
||||||
* Delegates to the shared RelayConnectionManager from commons.
|
* Now Tor-aware: passes the DesktopHttpClient's getHttpClient which selects
|
||||||
|
* proxy or direct client per relay URL based on Tor settings.
|
||||||
*/
|
*/
|
||||||
class DesktopRelayConnectionManager :
|
class DesktopRelayConnectionManager(
|
||||||
RelayConnectionManager(
|
httpClient: DesktopHttpClient,
|
||||||
websocketBuilder = BasicOkHttpWebSocket.Builder(DesktopHttpClient::getHttpClient),
|
) : RelayConnectionManager(
|
||||||
|
websocketBuilder = BasicOkHttpWebSocket.Builder(httpClient::getHttpClient),
|
||||||
)
|
)
|
||||||
|
|||||||
+102
-41
@@ -20,17 +20,25 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.desktop.network
|
package com.vitorpamplona.amethyst.desktop.network
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class DesktopHttpClientTest {
|
class DesktopHttpClientTest {
|
||||||
|
// --- Simple (companion) client tests ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetHttpClientReturnsConfiguredClient() {
|
fun simpleClient_returnsConfiguredClient() {
|
||||||
val url = NormalizedRelayUrl("wss://relay.damus.io")
|
val url = NormalizedRelayUrl("wss://relay.damus.io/")
|
||||||
val client = DesktopHttpClient.getHttpClient(url)
|
val client = DesktopHttpClient.getSimpleHttpClient(url)
|
||||||
|
|
||||||
assertNotNull(client)
|
assertNotNull(client)
|
||||||
assertEquals(30_000, client.connectTimeoutMillis)
|
assertEquals(30_000, client.connectTimeoutMillis)
|
||||||
@@ -41,52 +49,105 @@ class DesktopHttpClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetHttpClientReturnsSameInstance() {
|
fun simpleClient_returnsSameInstance() {
|
||||||
val url1 = NormalizedRelayUrl("wss://relay.damus.io")
|
val url1 = NormalizedRelayUrl("wss://relay.damus.io/")
|
||||||
val url2 = NormalizedRelayUrl("wss://nos.lol")
|
val url2 = NormalizedRelayUrl("wss://nos.lol/")
|
||||||
|
assertEquals(DesktopHttpClient.getSimpleHttpClient(url1), DesktopHttpClient.getSimpleHttpClient(url2))
|
||||||
|
}
|
||||||
|
|
||||||
val client1 = DesktopHttpClient.getHttpClient(url1)
|
// --- Tor-aware client tests ---
|
||||||
val client2 = DesktopHttpClient.getHttpClient(url2)
|
|
||||||
|
|
||||||
// Should return the same singleton instance
|
private fun buildTorAwareClient(
|
||||||
assertEquals(client1, client2)
|
torPort: Int? = null,
|
||||||
|
shouldUseTor: (NormalizedRelayUrl) -> Boolean = { false },
|
||||||
|
): DesktopHttpClient {
|
||||||
|
val statusFlow =
|
||||||
|
MutableStateFlow<TorServiceStatus>(
|
||||||
|
if (torPort != null) TorServiceStatus.Active(torPort) else TorServiceStatus.Off,
|
||||||
|
)
|
||||||
|
val portFlow = MutableStateFlow<Int?>(torPort)
|
||||||
|
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
|
||||||
|
|
||||||
|
val fakeTorManager =
|
||||||
|
object : com.vitorpamplona.amethyst.commons.tor.ITorManager {
|
||||||
|
override val status = statusFlow
|
||||||
|
override val activePortOrNull = portFlow
|
||||||
|
|
||||||
|
override suspend fun dormant() {}
|
||||||
|
|
||||||
|
override suspend fun active() {}
|
||||||
|
|
||||||
|
override suspend fun newIdentity() {}
|
||||||
|
}
|
||||||
|
return DesktopHttpClient(fakeTorManager, shouldUseTor, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testHttpClientHasExpectedTimeouts() {
|
fun torOff_returnsDirectClient() {
|
||||||
val url = NormalizedRelayUrl("wss://relay.nostr.band")
|
val httpClient = buildTorAwareClient(torPort = null)
|
||||||
val client = DesktopHttpClient.getHttpClient(url)
|
val url = NormalizedRelayUrl("wss://relay.damus.io/")
|
||||||
|
val client = httpClient.getHttpClient(url)
|
||||||
|
|
||||||
|
assertNotNull(client)
|
||||||
|
assertNull(client.proxy, "Should not have proxy when Tor is off")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun torOn_localhostRelay_returnsDirectClient() {
|
||||||
|
val httpClient = buildTorAwareClient(torPort = 9050, shouldUseTor = { true })
|
||||||
|
val url = NormalizedRelayUrl("ws://127.0.0.1:8080/")
|
||||||
|
val client = httpClient.getHttpClient(url)
|
||||||
|
|
||||||
|
assertNull(client.proxy, "Localhost should never use proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun torOn_clearnetRelay_shouldUseTorTrue_returnsProxiedClient() {
|
||||||
|
val httpClient = buildTorAwareClient(torPort = 9050, shouldUseTor = { true })
|
||||||
|
val url = NormalizedRelayUrl("wss://relay.damus.io/")
|
||||||
|
val client = httpClient.getHttpClient(url)
|
||||||
|
|
||||||
|
assertNotNull(client.proxy, "Should have SOCKS proxy when Tor routing is enabled")
|
||||||
|
assertEquals(java.net.Proxy.Type.SOCKS, client.proxy!!.type())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun torOn_clearnetRelay_shouldUseTorFalse_returnsDirectClient() {
|
||||||
|
val httpClient = buildTorAwareClient(torPort = 9050, shouldUseTor = { false })
|
||||||
|
val url = NormalizedRelayUrl("wss://relay.damus.io/")
|
||||||
|
val client = httpClient.getHttpClient(url)
|
||||||
|
|
||||||
|
assertNull(client.proxy, "Should not proxy when shouldUseTor returns false")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun torOn_onionRelay_alwaysProxied() {
|
||||||
|
val httpClient = buildTorAwareClient(torPort = 9050, shouldUseTor = { false })
|
||||||
|
val url = NormalizedRelayUrl("wss://abc123.onion/")
|
||||||
|
val client = httpClient.getHttpClient(url)
|
||||||
|
|
||||||
|
assertNotNull(client.proxy, ".onion relays should always use proxy when Tor is active")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun proxyClient_hasDoubledTimeouts() {
|
||||||
|
val httpClient = buildTorAwareClient(torPort = 9050, shouldUseTor = { true })
|
||||||
|
val url = NormalizedRelayUrl("wss://relay.damus.io/")
|
||||||
|
val client = httpClient.getHttpClient(url)
|
||||||
|
|
||||||
|
// Desktop uses 2x multiplier: 30 * 2 = 60 seconds
|
||||||
|
assertEquals(60_000, client.connectTimeoutMillis)
|
||||||
|
assertEquals(60_000, client.readTimeoutMillis)
|
||||||
|
assertEquals(60_000, client.writeTimeoutMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun directClient_hasBaseTimeouts() {
|
||||||
|
val httpClient = buildTorAwareClient(torPort = null)
|
||||||
|
val client = httpClient.getNonProxyClient()
|
||||||
|
|
||||||
// Verify all timeouts are 30 seconds
|
|
||||||
assertEquals(30_000, client.connectTimeoutMillis)
|
assertEquals(30_000, client.connectTimeoutMillis)
|
||||||
assertEquals(30_000, client.readTimeoutMillis)
|
assertEquals(30_000, client.readTimeoutMillis)
|
||||||
assertEquals(30_000, client.writeTimeoutMillis)
|
assertEquals(30_000, client.writeTimeoutMillis)
|
||||||
assertEquals(30_000, client.pingIntervalMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testHttpClientHasRetryEnabled() {
|
|
||||||
val url = NormalizedRelayUrl("wss://relay.snort.social")
|
|
||||||
val client = DesktopHttpClient.getHttpClient(url)
|
|
||||||
|
|
||||||
assertTrue(client.retryOnConnectionFailure, "Retry on connection failure should be enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testHttpClientIsLazyInitialized() {
|
|
||||||
// This test verifies the lazy initialization pattern
|
|
||||||
// The client should be created only once even with multiple calls
|
|
||||||
val url1 = NormalizedRelayUrl("wss://relay1.example.com")
|
|
||||||
val url2 = NormalizedRelayUrl("wss://relay2.example.com")
|
|
||||||
val url3 = NormalizedRelayUrl("wss://relay3.example.com")
|
|
||||||
|
|
||||||
val client1 = DesktopHttpClient.getHttpClient(url1)
|
|
||||||
val client2 = DesktopHttpClient.getHttpClient(url2)
|
|
||||||
val client3 = DesktopHttpClient.getHttpClient(url3)
|
|
||||||
|
|
||||||
// All should be the same instance due to lazy singleton
|
|
||||||
assertEquals(client1, client2)
|
|
||||||
assertEquals(client2, client3)
|
|
||||||
assertEquals(client1, client3)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -20,20 +20,41 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.desktop.network
|
package com.vitorpamplona.amethyst.desktop.network
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class DesktopRelayConnectionManagerTest {
|
class DesktopRelayConnectionManagerTest {
|
||||||
|
private fun createManager(): DesktopRelayConnectionManager {
|
||||||
|
val fakeTorManager =
|
||||||
|
object : com.vitorpamplona.amethyst.commons.tor.ITorManager {
|
||||||
|
override val status = MutableStateFlow<TorServiceStatus>(TorServiceStatus.Off)
|
||||||
|
override val activePortOrNull = MutableStateFlow<Int?>(null)
|
||||||
|
|
||||||
|
override suspend fun dormant() {}
|
||||||
|
|
||||||
|
override suspend fun active() {}
|
||||||
|
|
||||||
|
override suspend fun newIdentity() {}
|
||||||
|
}
|
||||||
|
val scope = CoroutineScope(SupervisorJob())
|
||||||
|
val httpClient = DesktopHttpClient(fakeTorManager, { false }, scope)
|
||||||
|
return DesktopRelayConnectionManager(httpClient)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRelayConnectionManagerCanBeInstantiated() {
|
fun testRelayConnectionManagerCanBeInstantiated() {
|
||||||
val manager = DesktopRelayConnectionManager()
|
val manager = createManager()
|
||||||
assertNotNull(manager)
|
assertNotNull(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testRelayConnectionManagerHasNoActiveConnectionsInitially() {
|
fun testRelayConnectionManagerHasNoActiveConnectionsInitially() {
|
||||||
val manager = DesktopRelayConnectionManager()
|
val manager = createManager()
|
||||||
val connectedRelays = manager.connectedRelays.value
|
val connectedRelays = manager.connectedRelays.value
|
||||||
val availableRelays = manager.availableRelays.value
|
val availableRelays = manager.availableRelays.value
|
||||||
assertTrue(connectedRelays.isEmpty(), "Should have no connected relays on initialization")
|
assertTrue(connectedRelays.isEmpty(), "Should have no connected relays on initialization")
|
||||||
|
|||||||
Reference in New Issue
Block a user