// // BLEServiceCoreTests.swift // bitchatTests // // Focused BLEService tests for packet handling behavior. // import Testing import Foundation import CoreBluetooth import BitFoundation @testable import bitchat struct BLEServiceCoreTests { @Test func duplicatePacket_isDeduped() async { let ble = makeService() let delegate = PublicCaptureDelegate() ble.delegate = delegate let sender = PeerID(str: "1122334455667788") let timestamp = UInt64(Date().timeIntervalSince1970 * 1000) let packet = makePublicPacket(content: "Hello", sender: sender, timestamp: timestamp) ble._test_handlePacket(packet, fromPeerID: sender) let receivedFirst = await TestHelpers.waitUntil( { delegate.publicMessagesSnapshot().count == 1 }, timeout: TestConstants.defaultTimeout ) #expect(receivedFirst) ble._test_handlePacket(packet, fromPeerID: sender) let receivedDuplicate = await TestHelpers.waitUntil( { delegate.publicMessagesSnapshot().count > 1 }, timeout: TestConstants.shortTimeout ) #expect(!receivedDuplicate) let messages = delegate.publicMessagesSnapshot() #expect(messages.count == 1) #expect(messages.first?.content == "Hello") } @Test func staleBroadcast_isIgnored() async { let ble = makeService() let delegate = PublicCaptureDelegate() ble.delegate = delegate let sender = PeerID(str: "A1B2C3D4E5F60708") let oldTimestamp = UInt64(Date().addingTimeInterval(-901).timeIntervalSince1970 * 1000) let packet = makePublicPacket(content: "Old", sender: sender, timestamp: oldTimestamp) ble._test_handlePacket(packet, fromPeerID: sender) let didReceive = await TestHelpers.waitUntil({ !delegate.publicMessagesSnapshot().isEmpty }, timeout: 0.3) #expect(!didReceive) #expect(delegate.publicMessagesSnapshot().isEmpty) } @Test func announceSenderMismatch_isRejected() async throws { let ble = makeService() let signer = NoiseEncryptionService(keychain: MockKeychain()) let announcement = AnnouncementPacket( nickname: "Spoof", noisePublicKey: signer.getStaticPublicKeyData(), signingPublicKey: signer.getSigningPublicKeyData(), directNeighbors: nil ) let payload = try #require(announcement.encode(), "Failed to encode announcement") let derivedPeerID = PeerID(publicKey: announcement.noisePublicKey) let wrongFirst = derivedPeerID.bare.first == "0" ? "1" : "0" let wrongBare = String(wrongFirst) + String(derivedPeerID.bare.dropFirst()) let wrongPeerID = PeerID(str: wrongBare) let packet = BitchatPacket( type: MessageType.announce.rawValue, senderID: Data(hexString: wrongPeerID.id) ?? Data(), recipientID: nil, timestamp: UInt64(Date().timeIntervalSince1970 * 1000), payload: payload, signature: nil, ttl: 7 ) let signed = try #require(signer.signPacket(packet), "Failed to sign announce packet") ble._test_handlePacket(signed, fromPeerID: wrongPeerID, preseedPeer: false) _ = await TestHelpers.waitUntil({ !ble.currentPeerSnapshots().isEmpty }, timeout: 0.3) #expect(ble.currentPeerSnapshots().isEmpty) } @Test func ingressAllowsRelayedSenderOnBoundLink() async throws { let ble = makeService() let boundPeer = PeerID(str: "1122334455667788") let relayedSender = PeerID(str: "8899aabbccddeeff") let packet = makePublicPacket( content: "Relayed", sender: relayedSender, timestamp: UInt64(Date().timeIntervalSince1970 * 1000) ) #expect(ble._test_acceptsIngress(packet: packet, boundPeerID: boundPeer)) } @Test func ingressRejectsDirectAnnounceThatConflictsWithBoundLink() async throws { let ble = makeService() let boundPeer = PeerID(str: "1122334455667788") let claimedPeer = PeerID(str: "8899aabbccddeeff") let packet = BitchatPacket( type: MessageType.announce.rawValue, senderID: Data(hexString: claimedPeer.id) ?? Data(), recipientID: nil, timestamp: UInt64(Date().timeIntervalSince1970 * 1000), payload: Data(), signature: nil, ttl: 7 ) #expect(!ble._test_acceptsIngress(packet: packet, boundPeerID: boundPeer)) } @Test func ingressRejectsSelfLoopbackBeforeSpoofChecks() async throws { let ble = makeService() let packet = makePublicPacket( content: "Loopback", sender: ble.myPeerID, timestamp: UInt64(Date().timeIntervalSince1970 * 1000) ) #expect(!ble._test_acceptsIngress(packet: packet, boundPeerID: PeerID(str: "1122334455667788"))) } @Test func ingressAllowsSelfAuthoredRSRWithTTLZeroFromBoundPeer() async throws { let ble = makeService() var packet = makePublicPacket( content: "Recovered by sync", sender: ble.myPeerID, timestamp: UInt64(Date().timeIntervalSince1970 * 1000) ) packet.isRSR = true packet.ttl = 0 #expect(ble._test_acceptsIngress(packet: packet, boundPeerID: PeerID(str: "1122334455667788"))) } @Test func ingressRecordSuppressesSecondLinkDuplicate() async throws { let ble = makeService() let packet = makePublicPacket( content: "Duplicate link copy", sender: PeerID(str: "1122334455667788"), timestamp: UInt64(Date().timeIntervalSince1970 * 1000) ) #expect(ble._test_recordIngressIfNew(packet: packet, linkID: "central-a")) #expect(!ble._test_recordIngressIfNew(packet: packet, linkID: "central-b")) } @Test func modifiedServices_rediscoverWhenBitChatServiceIsInvalidated() async throws { let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB") #expect(BLEService._test_shouldRediscoverBitChatService( invalidatedServiceUUIDs: [BLEService.serviceUUID], cachedServiceUUIDs: [otherService] )) } @Test func modifiedServices_rediscoverWhenCachedServicesNoLongerIncludeBitChat() async throws { let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB") #expect(BLEService._test_shouldRediscoverBitChatService( invalidatedServiceUUIDs: [otherService], cachedServiceUUIDs: [otherService] )) } @Test func modifiedServices_ignoreUnrelatedInvalidationWhenBitChatIsStillCached() async throws { let otherService = CBUUID(string: "0000180F-0000-1000-8000-00805F9B34FB") #expect(!BLEService._test_shouldRediscoverBitChatService( invalidatedServiceUUIDs: [otherService], cachedServiceUUIDs: [BLEService.serviceUUID, otherService] )) } } private func makeService() -> BLEService { let keychain = MockKeychain() let identityManager = MockIdentityManager(keychain) let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper()) return BLEService( keychain: keychain, idBridge: idBridge, identityManager: identityManager, initializeBluetoothManagers: false ) } private func makePublicPacket(content: String, sender: PeerID, timestamp: UInt64) -> BitchatPacket { BitchatPacket( type: MessageType.message.rawValue, senderID: Data(hexString: sender.id) ?? Data(), recipientID: nil, timestamp: timestamp, payload: Data(content.utf8), signature: nil, ttl: 3 ) } private final class PublicCaptureDelegate: BitchatDelegate { private let lock = NSLock() private(set) var publicMessages: [BitchatMessage] = [] func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) { let message = BitchatMessage( id: messageID, sender: nickname, content: content, timestamp: timestamp, isRelay: false, originalSender: nil, isPrivate: false, recipientNickname: nil, senderPeerID: peerID, mentions: nil ) lock.lock() publicMessages.append(message) lock.unlock() } func didReceiveMessage(_ message: BitchatMessage) {} func didConnectToPeer(_ peerID: PeerID) {} func didDisconnectFromPeer(_ peerID: PeerID) {} func didUpdatePeerList(_ peers: [PeerID]) {} func didUpdateBluetoothState(_ state: CBManagerState) {} func publicMessagesSnapshot() -> [BitchatMessage] { lock.lock() defer { lock.unlock() } return publicMessages } }