test: add pre-extraction test coverage for Tor logic

69 tests covering TorSettings/presets, TorRelayEvaluation routing,
and OkHttpClientFactory proxy/timeout behavior. Documents current
behavior before extracting to commons/ for desktop Tor support.

Coverage:
- TorSettings: parsing, preset matching, whichPreset, isPreset
- TorRelayEvaluation: .onion/localhost/DM/trusted/new routing,
  priority order, empty relay lists, Tor OFF/INTERNAL/EXTERNAL
- OkHttpClientFactory: SOCKS proxy creation, timeout multipliers,
  null port fallback (9050 security issue documented)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-13 07:24:31 +03:00
co-authored by Claude Opus 4.6
parent 6a75fd9240
commit ff9093f5cc
3 changed files with 680 additions and 0 deletions
@@ -0,0 +1,235 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.model.torState
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class TorRelayEvaluationTest {
// Helper relay URLs
private val clearnetRelay = NormalizedRelayUrl("wss://relay.damus.io/")
private val onionRelay = NormalizedRelayUrl("wss://abc123.onion/")
private val localhostRelay = NormalizedRelayUrl("ws://127.0.0.1:8080/")
private val localhostNameRelay = NormalizedRelayUrl("ws://localhost:8080/")
private val localNetworkRelay = NormalizedRelayUrl("ws://192.168.1.100:8080/")
private val dmRelay = NormalizedRelayUrl("wss://dm.relay.com/")
private val trustedRelay = NormalizedRelayUrl("wss://trusted.relay.com/")
private fun buildEvaluation(
torType: TorType = TorType.INTERNAL,
onionViaTor: Boolean = true,
dmViaTor: Boolean = true,
newViaTor: Boolean = true,
trustedViaTor: Boolean = false,
dmRelays: Set<NormalizedRelayUrl> = setOf(dmRelay),
trustedRelays: Set<NormalizedRelayUrl> = setOf(trustedRelay),
) = TorRelayEvaluation(
torSettings =
TorRelaySettings(
torType = torType,
onionRelaysViaTor = onionViaTor,
dmRelaysViaTor = dmViaTor,
newRelaysViaTor = newViaTor,
trustedRelaysViaTor = trustedViaTor,
),
trustedRelayList = trustedRelays,
dmRelayList = dmRelays,
)
// --- Tor OFF: always false ---
@Test
fun torOff_clearnetRelay_returnsFalse() {
val eval = buildEvaluation(torType = TorType.OFF)
assertFalse(eval.useTor(clearnetRelay))
}
@Test
fun torOff_onionRelay_returnsFalse() {
val eval = buildEvaluation(torType = TorType.OFF)
assertFalse(eval.useTor(onionRelay))
}
@Test
fun torOff_dmRelay_returnsFalse() {
val eval = buildEvaluation(torType = TorType.OFF)
assertFalse(eval.useTor(dmRelay))
}
// --- Localhost: always false regardless of Tor ---
@Test
fun localhost127_alwaysFalse() {
val eval = buildEvaluation(torType = TorType.INTERNAL)
assertFalse(eval.useTor(localhostRelay))
}
@Test
fun localhostName_alwaysFalse() {
val eval = buildEvaluation(torType = TorType.INTERNAL)
assertFalse(eval.useTor(localhostNameRelay))
}
@Test
fun localNetwork192_alwaysFalse() {
val eval = buildEvaluation(torType = TorType.INTERNAL)
assertFalse(eval.useTor(localNetworkRelay))
}
// --- .onion relays ---
@Test
fun onionRelay_torInternal_onionEnabled_returnsTrue() {
val eval = buildEvaluation(torType = TorType.INTERNAL, onionViaTor = true)
assertTrue(eval.useTor(onionRelay))
}
@Test
fun onionRelay_torInternal_onionDisabled_returnsFalse() {
val eval = buildEvaluation(torType = TorType.INTERNAL, onionViaTor = false)
assertFalse(eval.useTor(onionRelay))
}
@Test
fun onionRelay_torExternal_onionEnabled_returnsTrue() {
val eval = buildEvaluation(torType = TorType.EXTERNAL, onionViaTor = true)
assertTrue(eval.useTor(onionRelay))
}
// --- DM relays ---
@Test
fun dmRelay_dmViaTorEnabled_returnsTrue() {
val eval = buildEvaluation(dmViaTor = true)
assertTrue(eval.useTor(dmRelay))
}
@Test
fun dmRelay_dmViaTorDisabled_returnsFalse() {
val eval = buildEvaluation(dmViaTor = false)
assertFalse(eval.useTor(dmRelay))
}
// --- Trusted relays ---
@Test
fun trustedRelay_trustedViaTorEnabled_returnsTrue() {
val eval = buildEvaluation(trustedViaTor = true)
assertTrue(eval.useTor(trustedRelay))
}
@Test
fun trustedRelay_trustedViaTorDisabled_returnsFalse() {
val eval = buildEvaluation(trustedViaTor = false)
assertFalse(eval.useTor(trustedRelay))
}
// --- New/unknown relays ---
@Test
fun unknownRelay_newViaTorEnabled_returnsTrue() {
val eval = buildEvaluation(newViaTor = true)
assertTrue(eval.useTor(clearnetRelay))
}
@Test
fun unknownRelay_newViaTorDisabled_returnsFalse() {
val eval = buildEvaluation(newViaTor = false)
assertFalse(eval.useTor(clearnetRelay))
}
// --- Priority: .onion > DM > trusted > new ---
@Test
fun onionRelay_inDmList_treatedAsOnionNotDm() {
// If a relay is both .onion AND in DM list, .onion takes precedence
val onionDmRelay = NormalizedRelayUrl("wss://dmrelay.onion/")
val eval =
buildEvaluation(
onionViaTor = false,
dmViaTor = true,
dmRelays = setOf(onionDmRelay),
)
// onion check happens first, and it's disabled → false
assertFalse(eval.useTor(onionDmRelay))
}
@Test
fun relay_inBothDmAndTrusted_dmTakesPrecedence() {
val bothRelay = NormalizedRelayUrl("wss://both.relay.com/")
val eval =
buildEvaluation(
dmViaTor = true,
trustedViaTor = false,
dmRelays = setOf(bothRelay),
trustedRelays = setOf(bothRelay),
)
// DM check happens before trusted check
assertTrue(eval.useTor(bothRelay))
}
@Test
fun relay_inBothDmAndTrusted_dmDisabled_doesNotFallToTrusted() {
val bothRelay = NormalizedRelayUrl("wss://both.relay.com/")
val eval =
buildEvaluation(
dmViaTor = false,
trustedViaTor = true,
dmRelays = setOf(bothRelay),
trustedRelays = setOf(bothRelay),
)
// DM check matches first, dm is disabled → false
// Does NOT fall through to trusted check
assertFalse(eval.useTor(bothRelay))
}
// --- Empty relay lists ---
@Test
fun emptyRelayLists_allRelaysAreNew() {
val eval =
buildEvaluation(
newViaTor = true,
dmRelays = emptySet(),
trustedRelays = emptySet(),
)
assertTrue(eval.useTor(clearnetRelay))
assertTrue(eval.useTor(dmRelay)) // Not in DM list, treated as new
assertTrue(eval.useTor(trustedRelay)) // Not in trusted list, treated as new
}
// --- External Tor mode ---
@Test
fun torExternal_newViaTor_returnsTrue() {
val eval = buildEvaluation(torType = TorType.EXTERNAL, newViaTor = true)
assertTrue(eval.useTor(clearnetRelay))
}
@Test
fun torExternal_localhostStillFalse() {
val eval = buildEvaluation(torType = TorType.EXTERNAL)
assertFalse(eval.useTor(localhostRelay))
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.okhttp
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.InetSocketAddress
import java.net.Proxy
/**
* Tests for SOCKS proxy creation and timeout logic.
*
* Note: OkHttpClientFactoryForRelays cannot be instantiated in unit tests because
* its constructor calls android.os.Build (isEmulator check). These tests verify
* the proxy and timeout logic directly using the same patterns the factory uses.
* This documents the behavior we must preserve during extraction to commons.
*/
class OkHttpClientFactoryForRelaysTest {
// --- buildLocalSocksProxy logic (tested directly) ---
@Test
fun socksProxy_withPort_returnsSocksType() {
val proxy = buildLocalSocksProxy(9050)
assertEquals(Proxy.Type.SOCKS, proxy.type())
}
@Test
fun socksProxy_withPort_usesLocalhost() {
val proxy = buildLocalSocksProxy(9050)
val addr = proxy.address() as InetSocketAddress
assertEquals("127.0.0.1", addr.hostString)
}
@Test
fun socksProxy_withPort_usesGivenPort() {
val proxy = buildLocalSocksProxy(9050)
val addr = proxy.address() as InetSocketAddress
assertEquals(9050, addr.port)
}
@Test
fun socksProxy_customPort_usesGivenPort() {
val proxy = buildLocalSocksProxy(9150) // Tor Browser port
val addr = proxy.address() as InetSocketAddress
assertEquals(9150, addr.port)
}
@Test
fun socksProxy_nullPort_fallsBackToDefault9050() {
// DOCUMENTS CURRENT BEHAVIOR: null falls back to 9050
// This is a security concern flagged in the plan — will be fixed during extraction
val proxy = buildLocalSocksProxy(null)
val addr = proxy.address() as InetSocketAddress
assertEquals(OkHttpClientFactoryForRelays.DEFAULT_SOCKS_PORT, addr.port)
}
// --- Timeout logic ---
@Test
fun timeout_mobile_returns30Seconds() {
assertEquals(OkHttpClientFactoryForRelays.DEFAULT_TIMEOUT_ON_MOBILE_SECS, buildTimeout(true))
}
@Test
fun timeout_wifi_returns10Seconds() {
assertEquals(OkHttpClientFactoryForRelays.DEFAULT_TIMEOUT_ON_WIFI_SECS, buildTimeout(false))
}
// --- Timeout multiplier with proxy ---
@Test
fun timeoutWithProxy_tripled() {
val base = 10
val withProxy = computeTimeout(base, hasProxy = true)
assertEquals(30, withProxy)
}
@Test
fun timeoutWithoutProxy_unchanged() {
val base = 10
val withoutProxy = computeTimeout(base, hasProxy = false)
assertEquals(10, withoutProxy)
}
@Test
fun readWriteTimeout_tripleOfConnectTimeout() {
// Both factories do: readTimeout = connectTimeout * 3
val connectSeconds = 30
assertEquals(90, connectSeconds * 3)
}
// Note: OkHttpClient.Builder tests removed — OkHttp internals depend on
// Android platform classes in this module's test classpath.
// These will be tested in desktopApp/jvmTest after extraction.
// --- Constants ---
@Test
fun defaultSocksPort_is9050() {
assertEquals(9050, OkHttpClientFactoryForRelays.DEFAULT_SOCKS_PORT)
}
@Test
fun defaultIsMobile_isFalse() {
assertEquals(false, OkHttpClientFactoryForRelays.DEFAULT_IS_MOBILE)
}
@Test
fun defaultTimeoutWifi_is10() {
assertEquals(10, OkHttpClientFactoryForRelays.DEFAULT_TIMEOUT_ON_WIFI_SECS)
}
@Test
fun defaultTimeoutMobile_is30() {
assertEquals(30, OkHttpClientFactoryForRelays.DEFAULT_TIMEOUT_ON_MOBILE_SECS)
}
// --- Helper functions matching factory logic ---
private fun buildLocalSocksProxy(port: Int?): Proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port ?: OkHttpClientFactoryForRelays.DEFAULT_SOCKS_PORT))
private fun buildTimeout(isMobile: Boolean): Int =
if (isMobile) {
OkHttpClientFactoryForRelays.DEFAULT_TIMEOUT_ON_MOBILE_SECS
} else {
OkHttpClientFactoryForRelays.DEFAULT_TIMEOUT_ON_WIFI_SECS
}
private fun computeTimeout(
baseSeconds: Int,
hasProxy: Boolean,
): Int = if (hasProxy) baseSeconds * 3 else baseSeconds
}
@@ -0,0 +1,294 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.tor
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class TorSettingsTest {
// --- parseTorType ---
@Test
fun parseTorType_code0_returnsOff() {
assertEquals(TorType.OFF, parseTorType(0))
}
@Test
fun parseTorType_code1_returnsInternal() {
assertEquals(TorType.INTERNAL, parseTorType(1))
}
@Test
fun parseTorType_code2_returnsExternal() {
assertEquals(TorType.EXTERNAL, parseTorType(2))
}
@Test
fun parseTorType_null_defaultsToInternal() {
assertEquals(TorType.INTERNAL, parseTorType(null))
}
@Test
fun parseTorType_unknownCode_defaultsToInternal() {
assertEquals(TorType.INTERNAL, parseTorType(99))
}
@Test
fun parseTorType_negativeCode_defaultsToInternal() {
assertEquals(TorType.INTERNAL, parseTorType(-1))
}
// --- TorType screenCode consistency ---
@Test
fun torType_screenCodes_areUnique() {
val codes = TorType.entries.map { it.screenCode }
assertEquals(codes.size, codes.toSet().size)
}
@Test
fun torType_allValues_roundTripViaParse() {
TorType.entries.forEach { type ->
assertEquals(type, parseTorType(type.screenCode))
}
}
// --- parseTorPresetType ---
@Test
fun parseTorPresetType_code0_returnsOnlyWhenNeeded() {
assertEquals(TorPresetType.ONLY_WHEN_NEEDED, parseTorPresetType(0))
}
@Test
fun parseTorPresetType_code1_returnsDefault() {
assertEquals(TorPresetType.DEFAULT, parseTorPresetType(1))
}
@Test
fun parseTorPresetType_code2_returnsSmallPayloads() {
assertEquals(TorPresetType.SMALL_PAYLOADS, parseTorPresetType(2))
}
@Test
fun parseTorPresetType_code3_returnsFullPrivacy() {
assertEquals(TorPresetType.FULL_PRIVACY, parseTorPresetType(3))
}
@Test
fun parseTorPresetType_unknownCode_defaultsToCustom() {
assertEquals(TorPresetType.CUSTOM, parseTorPresetType(99))
}
@Test
fun parseTorPresetType_null_defaultsToCustom() {
assertEquals(TorPresetType.CUSTOM, parseTorPresetType(null))
}
@Test
fun torPresetType_screenCodes_areUnique() {
val codes = TorPresetType.entries.map { it.screenCode }
assertEquals(codes.size, codes.toSet().size)
}
// --- Preset definitions ---
@Test
fun onlyWhenNeededPreset_onlyOnionEnabled() {
assertTrue(torOnlyWhenNeededPreset.onionRelaysViaTor)
assertFalse(torOnlyWhenNeededPreset.dmRelaysViaTor)
assertFalse(torOnlyWhenNeededPreset.newRelaysViaTor)
assertFalse(torOnlyWhenNeededPreset.trustedRelaysViaTor)
assertFalse(torOnlyWhenNeededPreset.urlPreviewsViaTor)
assertFalse(torOnlyWhenNeededPreset.profilePicsViaTor)
assertFalse(torOnlyWhenNeededPreset.imagesViaTor)
assertFalse(torOnlyWhenNeededPreset.videosViaTor)
assertFalse(torOnlyWhenNeededPreset.moneyOperationsViaTor)
assertFalse(torOnlyWhenNeededPreset.nip05VerificationsViaTor)
assertFalse(torOnlyWhenNeededPreset.mediaUploadsViaTor)
}
@Test
fun defaultPreset_onionDmNewEnabled() {
assertTrue(torDefaultPreset.onionRelaysViaTor)
assertTrue(torDefaultPreset.dmRelaysViaTor)
assertTrue(torDefaultPreset.newRelaysViaTor)
assertFalse(torDefaultPreset.trustedRelaysViaTor)
assertFalse(torDefaultPreset.urlPreviewsViaTor)
assertFalse(torDefaultPreset.imagesViaTor)
assertFalse(torDefaultPreset.videosViaTor)
assertFalse(torDefaultPreset.moneyOperationsViaTor)
assertFalse(torDefaultPreset.nip05VerificationsViaTor)
assertFalse(torDefaultPreset.mediaUploadsViaTor)
}
@Test
fun smallPayloadsPreset_addsPreviewsNip05Money() {
assertTrue(torSmallPayloadsPreset.onionRelaysViaTor)
assertTrue(torSmallPayloadsPreset.dmRelaysViaTor)
assertTrue(torSmallPayloadsPreset.newRelaysViaTor)
assertTrue(torSmallPayloadsPreset.trustedRelaysViaTor)
assertTrue(torSmallPayloadsPreset.urlPreviewsViaTor)
assertTrue(torSmallPayloadsPreset.profilePicsViaTor)
assertFalse(torSmallPayloadsPreset.imagesViaTor)
assertFalse(torSmallPayloadsPreset.videosViaTor)
assertTrue(torSmallPayloadsPreset.moneyOperationsViaTor)
assertTrue(torSmallPayloadsPreset.nip05VerificationsViaTor)
assertFalse(torSmallPayloadsPreset.mediaUploadsViaTor)
}
@Test
fun fullPrivacyPreset_allEnabled() {
assertTrue(torFullyPrivate.onionRelaysViaTor)
assertTrue(torFullyPrivate.dmRelaysViaTor)
assertTrue(torFullyPrivate.newRelaysViaTor)
assertTrue(torFullyPrivate.trustedRelaysViaTor)
assertTrue(torFullyPrivate.urlPreviewsViaTor)
assertTrue(torFullyPrivate.profilePicsViaTor)
assertTrue(torFullyPrivate.imagesViaTor)
assertTrue(torFullyPrivate.videosViaTor)
assertTrue(torFullyPrivate.moneyOperationsViaTor)
assertTrue(torFullyPrivate.nip05VerificationsViaTor)
assertTrue(torFullyPrivate.mediaUploadsViaTor)
}
// --- Preset hierarchy: each level is a superset of the previous ---
@Test
fun presets_areIncreasing_defaultSupersetOfOnlyWhenNeeded() {
// Default enables DM + new relays on top of onlyWhenNeeded
assertTrue(torDefaultPreset.dmRelaysViaTor)
assertTrue(torDefaultPreset.newRelaysViaTor)
assertFalse(torOnlyWhenNeededPreset.dmRelaysViaTor)
assertFalse(torOnlyWhenNeededPreset.newRelaysViaTor)
}
@Test
fun presets_areIncreasing_fullPrivacySupersetOfSmallPayloads() {
// Full privacy adds images, videos, media uploads
assertTrue(torFullyPrivate.imagesViaTor)
assertTrue(torFullyPrivate.videosViaTor)
assertTrue(torFullyPrivate.mediaUploadsViaTor)
assertFalse(torSmallPayloadsPreset.imagesViaTor)
assertFalse(torSmallPayloadsPreset.videosViaTor)
assertFalse(torSmallPayloadsPreset.mediaUploadsViaTor)
}
// --- whichPreset ---
@Test
fun whichPreset_matchesOnlyWhenNeeded() {
assertEquals(TorPresetType.ONLY_WHEN_NEEDED, whichPreset(torOnlyWhenNeededPreset))
}
@Test
fun whichPreset_matchesDefault() {
assertEquals(TorPresetType.DEFAULT, whichPreset(torDefaultPreset))
}
@Test
fun whichPreset_matchesSmallPayloads() {
assertEquals(TorPresetType.SMALL_PAYLOADS, whichPreset(torSmallPayloadsPreset))
}
@Test
fun whichPreset_matchesFullPrivacy() {
assertEquals(TorPresetType.FULL_PRIVACY, whichPreset(torFullyPrivate))
}
@Test
fun whichPreset_returnsCustomForMixedSettings() {
val mixed =
TorSettings(
onionRelaysViaTor = true,
dmRelaysViaTor = true,
newRelaysViaTor = false, // differs from DEFAULT
trustedRelaysViaTor = true, // differs from DEFAULT
)
assertEquals(TorPresetType.CUSTOM, whichPreset(mixed))
}
@Test
fun whichPreset_ignoresProfilePicsInComparison() {
// profilePicsViaTor is commented out in isPreset()
val withProfilePics = torDefaultPreset.copy(profilePicsViaTor = true)
assertEquals(TorPresetType.DEFAULT, whichPreset(withProfilePics))
}
@Test
fun whichPreset_ignoresTorTypeAndPort() {
// whichPreset only compares boolean flags, not torType/port
val withExternal = torDefaultPreset.copy(torType = TorType.EXTERNAL, externalSocksPort = 1234)
assertEquals(TorPresetType.DEFAULT, whichPreset(withExternal))
}
// --- isPreset ---
@Test
fun isPreset_exactMatch_returnsTrue() {
assertTrue(isPreset(torFullyPrivate, torFullyPrivate))
}
@Test
fun isPreset_differentFlag_returnsFalse() {
val modified = torFullyPrivate.copy(imagesViaTor = false)
assertFalse(isPreset(modified, torFullyPrivate))
}
@Test
fun isPreset_torTypeDifference_ignored() {
val withOff = torDefaultPreset.copy(torType = TorType.OFF)
assertTrue(isPreset(withOff, torDefaultPreset))
}
// --- TorSettings data class ---
@Test
fun torSettings_defaultValues() {
val defaults = TorSettings()
assertEquals(TorType.INTERNAL, defaults.torType)
assertEquals(9050, defaults.externalSocksPort)
assertTrue(defaults.onionRelaysViaTor)
assertTrue(defaults.dmRelaysViaTor)
assertTrue(defaults.newRelaysViaTor)
assertFalse(defaults.trustedRelaysViaTor)
}
@Test
fun torSettings_equality_worksForDistinctUntilChanged() {
val a = TorSettings(torType = TorType.INTERNAL, externalSocksPort = 9050)
val b = TorSettings(torType = TorType.INTERNAL, externalSocksPort = 9050)
assertEquals(a, b)
assertEquals(a.hashCode(), b.hashCode())
}
@Test
fun torSettings_copy_changesOneField() {
val original = TorSettings()
val modified = original.copy(torType = TorType.OFF)
assertEquals(TorType.OFF, modified.torType)
assertEquals(original.externalSocksPort, modified.externalSocksPort)
assertNotEquals(original, modified)
}
}