From 14fbbc6c5f98f7f65696f4c92f0225ca49d8b391 Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 4 Jun 2026 13:23:22 +0300 Subject: [PATCH] Stabilize ChatViewModel test Tor lifecycle --- bitchat/ViewModels/ChatViewModel.swift | 25 +++++++++++++++---- .../Extensions/ChatViewModel+Tor.swift | 4 +-- .../ChatViewModelExtensionsTests.swift | 7 +++++- bitchatTests/ChatViewModelTests.swift | 8 +++--- bitchatTests/ChatViewModelTorTests.swift | 7 +++++- bitchatTests/GeohashPresenceTests.swift | 7 +++++- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index b0c923a8..488324b3 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -90,6 +90,16 @@ import UIKit #endif import UniformTypeIdentifiers +struct ChatViewModelTorLifecycle { + var torEnforced: @MainActor () -> Bool + var isAutoStartAllowed: @MainActor () -> Bool + + static let live = ChatViewModelTorLifecycle( + torEnforced: { TorManager.shared.torEnforced }, + isAutoStartAllowed: { TorManager.shared.isAutoStartAllowed() } + ) +} + /// Manages the application state and business logic for BitChat. /// Acts as the primary coordinator between UI components and backend services, /// implementing the BitchatDelegate protocol to handle network events. @@ -268,6 +278,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv let idBridge: NostrIdentityBridge let identityManager: SecureIdentityStateManagerProtocol let ndrService: NdrNostrService + let torLifecycle: ChatViewModelTorLifecycle var nostrRelayManager: NostrRelayManager? private let userDefaults = UserDefaults.standard @@ -397,14 +408,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv keychain: KeychainManagerProtocol, idBridge: NostrIdentityBridge, identityManager: SecureIdentityStateManagerProtocol, - ndrService: NdrNostrService? = nil + ndrService: NdrNostrService? = nil, + torLifecycle: ChatViewModelTorLifecycle = .live ) { self.init( keychain: keychain, idBridge: idBridge, identityManager: identityManager, transport: BLEService(keychain: keychain, idBridge: idBridge, identityManager: identityManager), - ndrService: ndrService + ndrService: ndrService, + torLifecycle: torLifecycle ) } @@ -416,13 +429,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv idBridge: NostrIdentityBridge, identityManager: SecureIdentityStateManagerProtocol, transport: Transport, - ndrService: NdrNostrService? = nil + ndrService: NdrNostrService? = nil, + torLifecycle: ChatViewModelTorLifecycle = .live ) { let resolvedNdrService = ndrService ?? NdrNostrService.shared self.keychain = keychain self.idBridge = idBridge self.identityManager = identityManager self.ndrService = resolvedNdrService + self.torLifecycle = torLifecycle self.meshService = transport self.publicMessagePipeline = PublicMessagePipeline() @@ -502,12 +517,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv } // Announce Tor status (geohash-only; do not show in mesh chat). Only when auto-start is allowed. - if TorManager.shared.torEnforced && !torStatusAnnounced && TorManager.shared.isAutoStartAllowed() { + if torLifecycle.torEnforced() && !torStatusAnnounced && torLifecycle.isAutoStartAllowed() { torStatusAnnounced = true addGeohashOnlySystemMessage( String(localized: "system.tor.starting", comment: "System message when Tor is starting") ) - } else if !TorManager.shared.torEnforced && !torStatusAnnounced { + } else if !torLifecycle.torEnforced() && !torStatusAnnounced { torStatusAnnounced = true addGeohashOnlySystemMessage( String(localized: "system.tor.dev_bypass", comment: "System message when Tor bypass is enabled in development") diff --git a/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift b/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift index 443849db..0d908f86 100644 --- a/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift +++ b/bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift @@ -15,7 +15,7 @@ extension ChatViewModel { @objc func handleTorWillStart() { Task { @MainActor in - if !self.torStatusAnnounced && TorManager.shared.torEnforced { + if !self.torStatusAnnounced && self.torLifecycle.torEnforced() { self.torStatusAnnounced = true // Post only in geohash channels (queue if not active) self.addGeohashOnlySystemMessage( @@ -44,7 +44,7 @@ extension ChatViewModel { String(localized: "system.tor.restarted", comment: "System message when Tor has restarted") ) self.torRestartPending = false - } else if TorManager.shared.torEnforced && !self.torInitialReadyAnnounced { + } else if self.torLifecycle.torEnforced() && !self.torInitialReadyAnnounced { // Initial start completed self.addGeohashOnlySystemMessage( String(localized: "system.tor.started", comment: "System message when Tor has started") diff --git a/bitchatTests/ChatViewModelExtensionsTests.swift b/bitchatTests/ChatViewModelExtensionsTests.swift index 13934872..3d2bc4fb 100644 --- a/bitchatTests/ChatViewModelExtensionsTests.swift +++ b/bitchatTests/ChatViewModelExtensionsTests.swift @@ -25,12 +25,17 @@ private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: Mo let idBridge = NostrIdentityBridge(keychain: keychainHelper) let identityManager = MockIdentityManager(keychain) let transport = MockTransport() + let torLifecycle = ChatViewModelTorLifecycle( + torEnforced: { true }, + isAutoStartAllowed: { false } + ) let viewModel = ChatViewModel( keychain: keychain, idBridge: idBridge, identityManager: identityManager, - transport: transport + transport: transport, + torLifecycle: torLifecycle ) return (viewModel, transport) diff --git a/bitchatTests/ChatViewModelTests.swift b/bitchatTests/ChatViewModelTests.swift index 1be3b578..7eb00312 100644 --- a/bitchatTests/ChatViewModelTests.swift +++ b/bitchatTests/ChatViewModelTests.swift @@ -318,7 +318,7 @@ struct ChatViewModelPrivateChatTests { @Test @MainActor func sendPrivateMessage_delegatesToTransport() async { let (viewModel, transport) = makeTestableViewModel() - let recipientID = PeerID(str: "RECIPIENT") + let recipientID = PeerID(str: "0000000000000001") // Set up connected peer for routing transport.connectedPeers.insert(recipientID) @@ -326,9 +326,9 @@ struct ChatViewModelPrivateChatTests { viewModel.sendPrivateMessage("Secret message", to: recipientID) - // The message routing depends on connection state and other factors - // At minimum, it should not crash - #expect(true) // If we get here without crash, the test passes + #expect(transport.sentPrivateMessages.count == 1) + #expect(transport.sentPrivateMessages.first?.content == "Secret message") + #expect(transport.sentPrivateMessages.first?.peerID == recipientID) } } diff --git a/bitchatTests/ChatViewModelTorTests.swift b/bitchatTests/ChatViewModelTorTests.swift index d4697dd8..25a667d2 100644 --- a/bitchatTests/ChatViewModelTorTests.swift +++ b/bitchatTests/ChatViewModelTorTests.swift @@ -18,12 +18,17 @@ private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: Mo let idBridge = NostrIdentityBridge(keychain: keychainHelper) let identityManager = MockIdentityManager(keychain) let transport = MockTransport() + let torLifecycle = ChatViewModelTorLifecycle( + torEnforced: { true }, + isAutoStartAllowed: { false } + ) let viewModel = ChatViewModel( keychain: keychain, idBridge: idBridge, identityManager: identityManager, - transport: transport + transport: transport, + torLifecycle: torLifecycle ) return (viewModel, transport) diff --git a/bitchatTests/GeohashPresenceTests.swift b/bitchatTests/GeohashPresenceTests.swift index dcb5e3bc..675cfc0d 100644 --- a/bitchatTests/GeohashPresenceTests.swift +++ b/bitchatTests/GeohashPresenceTests.swift @@ -286,12 +286,17 @@ struct ChatViewModelPresenceHandlingTests { let idBridge = NostrIdentityBridge(keychain: keychainHelper) let identityManager = MockIdentityManager(keychain) let transport = MockTransport() + let torLifecycle = ChatViewModelTorLifecycle( + torEnforced: { true }, + isAutoStartAllowed: { false } + ) let viewModel = ChatViewModel( keychain: keychain, idBridge: idBridge, identityManager: identityManager, - transport: transport + transport: transport, + torLifecycle: torLifecycle ) return (viewModel, transport)