// // ChatViewModelTests.swift // bitchatTests // // Tests for ChatViewModel using MockTransport for isolation. // This is free and unencumbered software released into the public domain. // import Testing import Foundation @testable import bitchat // MARK: - Test Helpers /// Creates a ChatViewModel with mock dependencies for testing @MainActor private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) { let keychain = MockKeychain() let keychainHelper = MockKeychainHelper() let idBridge = NostrIdentityBridge(keychain: keychainHelper) let identityManager = MockIdentityManager(keychain) let transport = MockTransport() let viewModel = ChatViewModel( keychain: keychain, idBridge: idBridge, identityManager: identityManager, transport: transport ) return (viewModel, transport) } // MARK: - Initialization Tests struct ChatViewModelInitializationTests { @Test @MainActor func initialization_setsDelegate() async { let (viewModel, transport) = makeTestableViewModel() // The viewModel should set itself as the transport delegate #expect(transport.delegate === viewModel) } @Test @MainActor func initialization_startsServices() async { let (_, transport) = makeTestableViewModel() // Services should be started during init #expect(transport.startServicesCallCount == 1) } @Test @MainActor func initialization_hasEmptyMessageList() async { let (viewModel, _) = makeTestableViewModel() // Initial messages may include system messages, but should be limited #expect(viewModel.messages.count < 10) } @Test @MainActor func initialization_setsNickname() async { let (_, transport) = makeTestableViewModel() // Nickname should be set during init #expect(!transport.myNickname.isEmpty) } } // MARK: - Message Sending Tests struct ChatViewModelSendingTests { @Test @MainActor func sendMessage_delegatesToTransport() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("Hello World") #expect(transport.sentMessages.count == 1) #expect(transport.sentMessages.first?.content == "Hello World") } @Test @MainActor func sendMessage_emptyContent_ignored() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("") viewModel.sendMessage(" ") viewModel.sendMessage("\n\t") #expect(transport.sentMessages.isEmpty) } @Test @MainActor func sendMessage_withMentions_sendsContent() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("Hello @alice") #expect(transport.sentMessages.count == 1) #expect(transport.sentMessages.first?.content == "Hello @alice") } @Test @MainActor func sendMessage_command_notSentToTransport() async { let (viewModel, transport) = makeTestableViewModel() viewModel.sendMessage("/help") // Commands are processed locally, not sent to transport #expect(transport.sentMessages.isEmpty) } } // MARK: - Message Receiving Tests struct ChatViewModelReceivingTests { @Test @MainActor func didReceiveMessage_callsDelegate() async { let (_, transport) = makeTestableViewModel() let message = BitchatMessage( id: "msg-001", sender: "Alice", content: "Hello from Alice", timestamp: Date(), isRelay: false, originalSender: nil, isPrivate: false, recipientNickname: nil, senderPeerID: PeerID(str: "PEER001"), mentions: nil ) transport.simulateIncomingMessage(message) // Give time for Task and pipeline processing try? await Task.sleep(nanoseconds: 200_000_000) // Message may or may not appear due to rate limiting/pipeline batching // The important thing is no crash and delegate was called #expect(transport.delegate != nil) } @Test @MainActor func didReceivePublicMessage_addsToTimeline() async { let (viewModel, transport) = makeTestableViewModel() transport.simulateIncomingPublicMessage( from: PeerID(str: "PEER002"), nickname: "Bob", content: "Public hello from Bob", timestamp: Date(), messageID: "pub-001" ) // Give time for async Task and pipeline processing try? await Task.sleep(nanoseconds: 200_000_000) #expect(viewModel.messages.contains { $0.content == "Public hello from Bob" }) } } // MARK: - Peer Connection Tests struct ChatViewModelPeerTests { @Test @MainActor func didConnectToPeer_notifiesDelegate() async { let (_, transport) = makeTestableViewModel() let peerID = PeerID(str: "NEWPEER") transport.simulateConnect(peerID, nickname: "NewUser") #expect(transport.connectedPeers.contains(peerID)) } @Test @MainActor func didDisconnectFromPeer_notifiesDelegate() async { let (_, transport) = makeTestableViewModel() let peerID = PeerID(str: "OLDPEER") transport.simulateConnect(peerID, nickname: "OldUser") transport.simulateDisconnect(peerID) #expect(!transport.connectedPeers.contains(peerID)) } @Test @MainActor func isPeerConnected_delegatesToTransport() async { let (_, transport) = makeTestableViewModel() let peerID = PeerID(str: "TESTPEER") // Not connected initially #expect(!transport.isPeerConnected(peerID)) transport.connectedPeers.insert(peerID) #expect(transport.isPeerConnected(peerID)) } } // MARK: - Deduplication Integration Tests // // Note: Detailed deduplication logic is tested in MessageDeduplicationServiceTests. // These tests verify that ChatViewModel has a deduplication service configured. struct ChatViewModelDeduplicationTests { @Test @MainActor func deduplicationService_isConfigured() async { let (viewModel, _) = makeTestableViewModel() // Verify the deduplication service is available and functional // by checking that we can record and query content let testContent = "Test dedup content \(UUID().uuidString)" let testDate = Date() viewModel.deduplicationService.recordContent(testContent, timestamp: testDate) let retrieved = viewModel.deduplicationService.contentTimestamp(for: testContent) #expect(retrieved == testDate) } @Test @MainActor func deduplicationService_normalizedKey_consistent() async { let (viewModel, _) = makeTestableViewModel() let content = "Hello World" let key1 = viewModel.deduplicationService.normalizedContentKey(content) let key2 = viewModel.deduplicationService.normalizedContentKey(content) #expect(key1 == key2) } } // MARK: - Private Chat Tests struct ChatViewModelPrivateChatTests { @Test @MainActor func sendPrivateMessage_delegatesToTransport() async { let (viewModel, transport) = makeTestableViewModel() let recipientID = PeerID(str: "RECIPIENT") // Set up connected peer for routing transport.connectedPeers.insert(recipientID) transport.peerNicknames[recipientID] = "Recipient" 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 } } // MARK: - Bluetooth State Tests struct ChatViewModelBluetoothTests { @Test @MainActor func didUpdateBluetoothState_poweredOn_noAlert() async { let (viewModel, transport) = makeTestableViewModel() transport.simulateBluetoothStateChange(.poweredOn) // Give time for async processing try? await Task.sleep(nanoseconds: 100_000_000) #expect(!viewModel.showBluetoothAlert) } @Test @MainActor func didUpdateBluetoothState_poweredOff_showsAlert() async { let (viewModel, transport) = makeTestableViewModel() transport.simulateBluetoothStateChange(.poweredOff) // Give time for async processing try? await Task.sleep(nanoseconds: 100_000_000) #expect(viewModel.showBluetoothAlert) } @Test @MainActor func didUpdateBluetoothState_unauthorized_showsAlert() async { let (viewModel, transport) = makeTestableViewModel() transport.simulateBluetoothStateChange(.unauthorized) // Give time for async processing try? await Task.sleep(nanoseconds: 100_000_000) #expect(viewModel.showBluetoothAlert) } } // MARK: - Panic Clear Tests struct ChatViewModelPanicTests { @Test @MainActor func panicClearAllData_delegatesToTransport() async { let (viewModel, transport) = makeTestableViewModel() // Set up some state transport.connectedPeers.insert(PeerID(str: "PEER1")) viewModel.panicClearAllData() // After panic, emergency disconnect should be called #expect(transport.emergencyDisconnectCallCount == 1) } } // MARK: - Service Lifecycle Tests struct ChatViewModelLifecycleTests { @Test @MainActor func startServices_calledOnInit() async { let (_, transport) = makeTestableViewModel() #expect(transport.startServicesCallCount == 1) } }