diff --git a/.cache/clang/ModuleCache/1O89I04QQ7RUB/SwiftShims-2IMTS4WWRU7VJ.pcm b/.cache/clang/ModuleCache/1O89I04QQ7RUB/SwiftShims-2IMTS4WWRU7VJ.pcm new file mode 100644 index 00000000..3b1b2861 Binary files /dev/null and b/.cache/clang/ModuleCache/1O89I04QQ7RUB/SwiftShims-2IMTS4WWRU7VJ.pcm differ diff --git a/.cache/clang/ModuleCache/1O89I04QQ7RUB/_SwiftConcurrencyShims-2IMTS4WWRU7VJ.pcm b/.cache/clang/ModuleCache/1O89I04QQ7RUB/_SwiftConcurrencyShims-2IMTS4WWRU7VJ.pcm new file mode 100644 index 00000000..f1217f96 Binary files /dev/null and b/.cache/clang/ModuleCache/1O89I04QQ7RUB/_SwiftConcurrencyShims-2IMTS4WWRU7VJ.pcm differ diff --git a/.cache/clang/ModuleCache/3A6TBOROFVNNR/SwiftShims-2IMTS4WWRU7VJ.pcm b/.cache/clang/ModuleCache/3A6TBOROFVNNR/SwiftShims-2IMTS4WWRU7VJ.pcm new file mode 100644 index 00000000..55f7e421 Binary files /dev/null and b/.cache/clang/ModuleCache/3A6TBOROFVNNR/SwiftShims-2IMTS4WWRU7VJ.pcm differ diff --git a/.cache/clang/ModuleCache/3A6TBOROFVNNR/_SwiftConcurrencyShims-2IMTS4WWRU7VJ.pcm b/.cache/clang/ModuleCache/3A6TBOROFVNNR/_SwiftConcurrencyShims-2IMTS4WWRU7VJ.pcm new file mode 100644 index 00000000..e263a6b7 Binary files /dev/null and b/.cache/clang/ModuleCache/3A6TBOROFVNNR/_SwiftConcurrencyShims-2IMTS4WWRU7VJ.pcm differ diff --git a/.cache/clang/ModuleCache/Swift-207X6UTF85MUY.swiftmodule b/.cache/clang/ModuleCache/Swift-207X6UTF85MUY.swiftmodule new file mode 100644 index 00000000..020e9eda --- /dev/null +++ b/.cache/clang/ModuleCache/Swift-207X6UTF85MUY.swiftmodule @@ -0,0 +1,12 @@ +--- +path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/Swift.swiftmodule/arm64e-apple-macos.swiftmodule' +dependencies: + - mtime: 1757258659000000000 + path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/Swift.swiftmodule/arm64e-apple-macos.swiftmodule' + size: 14166264 + - mtime: 1754189697000000000 + path: 'usr/lib/swift/Swift.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 2261306 + sdk_relative: true +version: 1 +... diff --git a/.cache/clang/ModuleCache/SwiftOnoneSupport-3W234PQLRG1JJ.swiftmodule b/.cache/clang/ModuleCache/SwiftOnoneSupport-3W234PQLRG1JJ.swiftmodule new file mode 100644 index 00000000..87a44797 --- /dev/null +++ b/.cache/clang/ModuleCache/SwiftOnoneSupport-3W234PQLRG1JJ.swiftmodule @@ -0,0 +1,16 @@ +--- +path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftmodule' +dependencies: + - mtime: 1757258662000000000 + path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftmodule' + size: 18068 + - mtime: 1754189697000000000 + path: 'usr/lib/swift/Swift.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 2261306 + sdk_relative: true + - mtime: 1754191141000000000 + path: 'usr/lib/swift/SwiftOnoneSupport.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 1224 + sdk_relative: true +version: 1 +... diff --git a/.cache/clang/ModuleCache/_Concurrency-IW47C06QYY26.swiftmodule b/.cache/clang/ModuleCache/_Concurrency-IW47C06QYY26.swiftmodule new file mode 100644 index 00000000..b9a985f0 --- /dev/null +++ b/.cache/clang/ModuleCache/_Concurrency-IW47C06QYY26.swiftmodule @@ -0,0 +1,16 @@ +--- +path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/_Concurrency.swiftmodule/arm64e-apple-macos.swiftmodule' +dependencies: + - mtime: 1757258669000000000 + path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/_Concurrency.swiftmodule/arm64e-apple-macos.swiftmodule' + size: 699544 + - mtime: 1754189697000000000 + path: 'usr/lib/swift/Swift.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 2261306 + sdk_relative: true + - mtime: 1754192470000000000 + path: 'usr/lib/swift/_Concurrency.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 364219 + sdk_relative: true +version: 1 +... diff --git a/.cache/clang/ModuleCache/_StringProcessing-1C2ZFCSIKDXY1.swiftmodule b/.cache/clang/ModuleCache/_StringProcessing-1C2ZFCSIKDXY1.swiftmodule new file mode 100644 index 00000000..8a3b6356 --- /dev/null +++ b/.cache/clang/ModuleCache/_StringProcessing-1C2ZFCSIKDXY1.swiftmodule @@ -0,0 +1,16 @@ +--- +path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftmodule' +dependencies: + - mtime: 1757258664000000000 + path: '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/26.0/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftmodule' + size: 83568 + - mtime: 1754189697000000000 + path: 'usr/lib/swift/Swift.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 2261306 + sdk_relative: true + - mtime: 1754192532000000000 + path: 'usr/lib/swift/_StringProcessing.swiftmodule/arm64e-apple-macos.swiftinterface' + size: 24507 + sdk_relative: true +version: 1 +... diff --git a/.cache/clang/ModuleCache/modules.timestamp b/.cache/clang/ModuleCache/modules.timestamp new file mode 100644 index 00000000..e69de29b diff --git a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift index 0e04776d..5fb0bdb3 100644 --- a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift @@ -6,140 +6,110 @@ // For more information, see // -import XCTest +import Testing import CryptoKit @testable import bitchat -final class PrivateChatE2ETests: XCTestCase { +// TODO: Remove once MockBLEService is refactored to fix race condition +@Suite(.serialized) +struct PrivateChatE2ETests { - var alice: MockBluetoothMeshService! - var bob: MockBluetoothMeshService! - var charlie: MockBluetoothMeshService! + private let alice: MockBLEService + private let bob: MockBLEService + private let charlie: MockBLEService + private let mockKeychain: MockKeychain - private var mockKeychain: MockKeychain! - - override func setUp() { - super.setUp() + init() { MockBLEService.resetTestBus() // Create services - alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1) - bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2) - charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3) + alice = MockBLEService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1) + bob = MockBLEService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2) + charlie = MockBLEService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3) mockKeychain = MockKeychain() - - // Delivery tracking is now handled internally by BLEService - } - - override func tearDown() { - alice = nil - bob = nil - charlie = nil - mockKeychain = nil - super.tearDown() } // MARK: - Basic Private Messaging Tests - - func testSimplePrivateMessageShouldNotBeSentWithoutConnection() { - // Intentionally commented out to test - // simulateConnection(alice, bob) - - let expectation = XCTestExpectation(description: "Bob should not receive a private message") - expectation.isInverted = true - - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && - message.isPrivate && - message.sender == TestConstants.testNickname1 { - expectation.fulfill() + + @Test func simplePrivateMessageShouldNotBeSentWithoutConnection() async { + // Intentionally not connecting alice and bob to test + + var bobReceivedMessage = false + + await confirmation("Bob should not receive a private message", expectedCount: 0) { bobReceivesMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && + message.isPrivate && + message.sender == TestConstants.testNickname1 { + bobReceivedMessage = true + bobReceivesMessage() + } } + + // Alice sends private message to Bob + alice.sendPrivateMessage( + TestConstants.testMessage1, + to: TestConstants.testPeerID2, + recipientNickname: TestConstants.testNickname2 + ) + + // Wait a bit to ensure message would have been delivered if it was going to be + try? await Task.sleep(nanoseconds: UInt64(TestConstants.shortTimeout * 1_000_000_000)) } - - // Alice sends private message to Bob - alice.sendPrivateMessage( - TestConstants.testMessage1, - to: TestConstants.testPeerID2, - recipientNickname: TestConstants.testNickname2 - ) - - wait(for: [expectation], timeout: TestConstants.shortTimeout) + + #expect(!bobReceivedMessage, "Bob should not have received the message") } - - func testSimplePrivateMessage() { - simulateConnection(alice, bob) + + @Test func simplePrivateMessage() async { + alice.simulateConnection(with: bob) - let expectation = XCTestExpectation(description: "Bob receives private message") - - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && - message.isPrivate && - message.sender == TestConstants.testNickname1 { - expectation.fulfill() + await confirmation("Bob receives private message") { bobReceivesMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && + message.isPrivate && + message.sender == TestConstants.testNickname1 { + bobReceivesMessage() + } } - } - - // Alice sends private message to Bob - alice.sendPrivateMessage( - TestConstants.testMessage1, - to: TestConstants.testPeerID2, - recipientNickname: TestConstants.testNickname2 - ) - - wait(for: [expectation], timeout: TestConstants.shortTimeout) - } - - func testPrivateMessageNotReceivedByOthers() { - // Connect all three - simulateConnection(alice, bob) - simulateConnection(alice, charlie) - - let bobExpectation = XCTestExpectation(description: "Bob receives private message") - let charlieExpectation = XCTestExpectation(description: "Charlie should not receive") - charlieExpectation.isInverted = true - - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && message.isPrivate { - bobExpectation.fulfill() - } - } - - charlie.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - charlieExpectation.fulfill() // Should not happen - } - } - - // Small delay to ensure connections are registered before send - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - // Alice sends private message to Bob only - self.alice.sendPrivateMessage( + + // Alice sends private message to Bob + alice.sendPrivateMessage( TestConstants.testMessage1, to: TestConstants.testPeerID2, recipientNickname: TestConstants.testNickname2 ) } - - wait(for: [bobExpectation, charlieExpectation], timeout: TestConstants.shortTimeout) } - // MARK: - Delivery Acknowledgment Tests - - // NOTE: DeliveryTracker has been removed in BLEService. - // Delivery tracking is now handled internally. - - - // MARK: - Message Retry Tests - - // NOTE: MessageRetryService has been removed in BLEService. - // Retry logic is now handled internally. - - + @Test func privateMessageNotReceivedByOthers() async { + alice.simulateConnection(with: bob) + alice.simulateConnection(with: charlie) + + await confirmation("Bob receives private message") { bobReceivesMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && message.isPrivate { + bobReceivesMessage() + } + } + + charlie.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + Issue.record("Charlie should not receive") + } + } + + alice.sendPrivateMessage( + TestConstants.testMessage1, + to: TestConstants.testPeerID2, + recipientNickname: TestConstants.testNickname2 + ) + } + } // MARK: - End-to-End Encryption Tests - func testPrivateMessageEncryption() { - simulateConnection(alice, bob) + @Test func privateMessageEncryption() async { + alice.simulateConnection(with: bob) // Setup Noise sessions let aliceKey = Curve25519.KeyAgreement.PrivateKey() @@ -155,161 +125,139 @@ final class PrivateChatE2ETests: XCTestCase { let handshake3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: handshake2)! _ = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: handshake3) } catch { - XCTFail("Failed to establish Noise session: \(error)") + Issue.record("Failed to establish Noise session: \(error)") } - let expectation = XCTestExpectation(description: "Encrypted message received") - - // Setup packet handlers for encryption - alice.packetDeliveryHandler = { packet in - // Encrypt outgoing private messages - if packet.type == 0x01, - let message = BitchatMessage(packet.payload), - message.isPrivate { - do { - let encrypted = try aliceManager.encrypt(packet.payload, for: TestConstants.testPeerID2) - let encryptedPacket = BitchatPacket( - type: 0x02, // Encrypted message type - senderID: packet.senderID, - recipientID: packet.recipientID, - timestamp: packet.timestamp, - payload: encrypted, - signature: packet.signature, - ttl: packet.ttl - ) - self.bob.simulateIncomingPacket(encryptedPacket) - } catch { - XCTFail("Encryption failed: \(error)") - } - } - } - - bob.packetDeliveryHandler = { packet in - // Decrypt incoming encrypted messages - if packet.type == 0x02 { - do { - let decrypted = try bobManager.decrypt(packet.payload, from: TestConstants.testPeerID1) - if let message = BitchatMessage(decrypted) { - XCTAssertEqual(message.content, TestConstants.testMessage1) - XCTAssertTrue(message.isPrivate) - expectation.fulfill() + await confirmation("Encrypted message received") { receiveEncryptedMessage in + // Setup packet handlers for encryption + alice.packetDeliveryHandler = { packet in + // Encrypt outgoing private messages + if packet.type == 0x01, + let message = BitchatMessage(packet.payload), + message.isPrivate { + do { + let encrypted = try aliceManager.encrypt(packet.payload, for: TestConstants.testPeerID2) + let encryptedPacket = BitchatPacket( + type: 0x02, // Encrypted message type + senderID: packet.senderID, + recipientID: packet.recipientID, + timestamp: packet.timestamp, + payload: encrypted, + signature: packet.signature, + ttl: packet.ttl + ) + self.bob.simulateIncomingPacket(encryptedPacket) + } catch { + Issue.record("Encryption failed: \(error)") } - } catch { - XCTFail("Decryption failed: \(error)") } } - } - - // Send encrypted private message - alice.sendPrivateMessage( - TestConstants.testMessage1, - to: TestConstants.testPeerID2, - recipientNickname: TestConstants.testNickname2 - ) - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) - } - - // MARK: - Multi-hop Private Message Tests - - func testPrivateMessageRelay() { - // Setup: Alice -> Bob -> Charlie - simulateConnection(alice, bob) - simulateConnection(bob, charlie) - - let expectation = XCTestExpectation(description: "Private message relayed to Charlie") - - // Bob relays private messages for Charlie - bob.packetDeliveryHandler = { packet in - if let recipientID = packet.recipientID, - String(data: recipientID, encoding: .utf8) == TestConstants.testPeerID3 { - // Relay to Charlie - var relayPacket = packet - relayPacket.ttl = packet.ttl - 1 - self.charlie.simulateIncomingPacket(relayPacket) - } - } - - charlie.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && - message.isPrivate && - message.recipientNickname == TestConstants.testNickname3 { - expectation.fulfill() - } - } - - // Alice sends private message to Charlie (through Bob) - alice.sendPrivateMessage( - TestConstants.testMessage1, - to: TestConstants.testPeerID3, - recipientNickname: TestConstants.testNickname3 - ) - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) - } - - // MARK: - Performance Tests - - func testPrivateMessageThroughput() { - simulateConnection(alice, bob) - - let messageCount = 100 - var receivedCount = 0 - let expectation = XCTestExpectation(description: "All private messages received") - - bob.messageDeliveryHandler = { message in - if message.isPrivate && message.sender == TestConstants.testNickname1 { - receivedCount += 1 - if receivedCount == messageCount { - expectation.fulfill() + + bob.packetDeliveryHandler = { packet in + // Decrypt incoming encrypted messages + if packet.type == 0x02 { + do { + let decrypted = try bobManager.decrypt(packet.payload, from: TestConstants.testPeerID1) + if let message = BitchatMessage(decrypted) { + #expect(message.content == TestConstants.testMessage1) + #expect(message.isPrivate) + receiveEncryptedMessage() + } + } catch { + Issue.record("Decryption failed: \(error)") + } } } - } - - // Send many private messages - for i in 0.. Bob -> Charlie + alice.simulateConnection(with: bob) + bob.simulateConnection(with: charlie) - let expectation = XCTestExpectation(description: "Large private message received") + await confirmation("Private message relayed to Charlie") { charlieReceivesMessage in + // Bob relays private messages for Charlie + bob.packetDeliveryHandler = { packet in + if let recipientID = packet.recipientID, + String(data: recipientID, encoding: .utf8) == TestConstants.testPeerID3 { + // Relay to Charlie + var relayPacket = packet + relayPacket.ttl = packet.ttl - 1 + self.charlie.simulateIncomingPacket(relayPacket) + } + } + + charlie.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && + message.isPrivate && + message.recipientNickname == TestConstants.testNickname3 { + charlieReceivesMessage() + } + } + + // Alice sends private message to Charlie (through Bob) + alice.sendPrivateMessage( + TestConstants.testMessage1, + to: TestConstants.testPeerID3, + recipientNickname: TestConstants.testNickname3 + ) + } + } + + // MARK: - Performance Tests + + @Test func privateMessageThroughput() async { + alice.simulateConnection(with: bob) - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testLongMessage && message.isPrivate { - expectation.fulfill() + let messageCount = 100 + var receivedCount = 0 + + await confirmation("All private messages received") { receivePrivateMessage in + bob.messageDeliveryHandler = { message in + if message.isPrivate && message.sender == TestConstants.testNickname1 { + receivedCount += 1 + if receivedCount == messageCount { + receivePrivateMessage() + } + } + } + + // Send many private messages + for i in 0.. MockBluetoothMeshService { - let service = MockBluetoothMeshService() - service.myPeerID = peerID.id - service.mockNickname = nickname - return service - } - - private func simulateConnection(_ peer1: MockBluetoothMeshService, _ peer2: MockBluetoothMeshService) { - peer1.simulateConnectedPeer(peer2.peerID) - peer2.simulateConnectedPeer(peer1.peerID) + + @Test func largePrivateMessage() async { + alice.simulateConnection(with: bob) + + await confirmation("Large private message received") { receiveLargeMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testLongMessage && message.isPrivate { + receiveLargeMessage() + } + } + + alice.sendPrivateMessage( + TestConstants.testLongMessage, + to: TestConstants.testPeerID2, + recipientNickname: TestConstants.testNickname2 + ) + } } } diff --git a/bitchatTests/EndToEnd/PublicChatE2ETests.swift b/bitchatTests/EndToEnd/PublicChatE2ETests.swift index b3a49a36..fcd52cd7 100644 --- a/bitchatTests/EndToEnd/PublicChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PublicChatE2ETests.swift @@ -6,439 +6,390 @@ // For more information, see // -import XCTest +import Testing @testable import bitchat -final class PublicChatE2ETests: XCTestCase { +@Suite(.serialized) +struct PublicChatE2ETests { - var alice: MockBluetoothMeshService! - var bob: MockBluetoothMeshService! - var charlie: MockBluetoothMeshService! - var david: MockBluetoothMeshService! + private let alice: MockBLEService + private let bob: MockBLEService + private let charlie: MockBLEService + private let david: MockBLEService - var receivedMessages: [String: [BitchatMessage]] = [:] + private var receivedMessages: [String: [BitchatMessage]] = [:] - override func setUp() { - super.setUp() + init() { MockBLEService.resetTestBus() // Create mock services - alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1) - bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2) - charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3) - david = createMockService(peerID: TestConstants.testPeerID4, nickname: TestConstants.testNickname4) - - // Clear received messages - receivedMessages.removeAll() - } - - override func tearDown() { - alice = nil - bob = nil - charlie = nil - david = nil - super.tearDown() + alice = MockBLEService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1) + bob = MockBLEService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2) + charlie = MockBLEService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3) + david = MockBLEService(peerID: TestConstants.testPeerID4, nickname: TestConstants.testNickname4) } // MARK: - Basic Broadcasting Tests - func testSimplePublicMessage() { - // Connect Alice and Bob - simulateConnection(alice, bob) + @Test func simplePublicMessage() async { + alice.simulateConnection(with: bob) - let expectation = XCTestExpectation(description: "Bob receives message") - - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && message.sender == TestConstants.testNickname1 { - expectation.fulfill() + await confirmation("Bob receives message") { bobReceivesMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && message.sender == TestConstants.testNickname1 { + bobReceivesMessage() + } } + + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - // Alice sends public message - alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) - - wait(for: [expectation], timeout: TestConstants.shortTimeout) } - func testMultiRecipientBroadcast() { - // Connect Alice to Bob and Charlie - simulateConnection(alice, bob) - simulateConnection(alice, charlie) + @Test func multiRecipientBroadcast() async { + alice.simulateConnection(with: bob) + alice.simulateConnection(with: charlie) - let bobExpectation = XCTestExpectation(description: "Bob receives message") - let charlieExpectation = XCTestExpectation(description: "Charlie receives message") + var bobReceivedMessage = false + var charlieReceivedMessage = false - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - bobExpectation.fulfill() + await confirmation("Both recieve message", expectedCount: 2) { receiveMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + if !bobReceivedMessage { + bobReceivedMessage = true + receiveMessage() + } else { + Issue.record("Bob received more than once") + } + } } - } - - charlie.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - charlieExpectation.fulfill() + + charlie.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + if !charlieReceivedMessage { + charlieReceivedMessage = true + receiveMessage() + } else { + Issue.record("Charlie received more than once") + } + } } + + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - // Alice broadcasts - alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) - - wait(for: [bobExpectation, charlieExpectation], timeout: TestConstants.shortTimeout) } // MARK: - Message Routing and Relay Tests - func testMessageRelayChain() { + @Test func messageRelayChain() async { // Linear topology: Alice -> Bob -> Charlie - simulateConnection(alice, bob) - simulateConnection(bob, charlie) - - let expectation = XCTestExpectation(description: "Charlie receives relayed message") - - // Set up relay in Bob - bob.packetDeliveryHandler = { packet in - // Bob should relay to Charlie - if let message = BitchatMessage(packet.payload), - message.sender == TestConstants.testNickname1 { - - // Create relay message - let relayMessage = BitchatMessage( - id: message.id, - sender: message.sender, - content: message.content, - timestamp: message.timestamp, - isRelay: true, - originalSender: message.sender, - isPrivate: message.isPrivate, - recipientNickname: message.recipientNickname, - senderPeerID: message.senderPeerID, - mentions: message.mentions - ) - - if let relayPayload = relayMessage.toBinaryPayload() { - let relayPacket = BitchatPacket( - type: packet.type, - senderID: packet.senderID, - recipientID: packet.recipientID, - timestamp: packet.timestamp, - payload: relayPayload, - signature: packet.signature, - ttl: packet.ttl - 1 + alice.simulateConnection(with: bob) + bob.simulateConnection(with: charlie) + + await confirmation("Charlie receives relayed message") { charlieReceivesMessage in + // Set up relay in Bob + bob.packetDeliveryHandler = { packet in + // Bob should relay to Charlie + if let message = BitchatMessage(packet.payload), + message.sender == TestConstants.testNickname1 { + + // Create relay message + let relayMessage = BitchatMessage( + id: message.id, + sender: message.sender, + content: message.content, + timestamp: message.timestamp, + isRelay: true, + originalSender: message.sender, + isPrivate: message.isPrivate, + recipientNickname: message.recipientNickname, + senderPeerID: message.senderPeerID, + mentions: message.mentions ) - - // Simulate relay to Charlie - self.charlie.simulateIncomingPacket(relayPacket) + + if let relayPayload = relayMessage.toBinaryPayload() { + let relayPacket = BitchatPacket( + type: packet.type, + senderID: packet.senderID, + recipientID: packet.recipientID, + timestamp: packet.timestamp, + payload: relayPayload, + signature: packet.signature, + ttl: packet.ttl - 1 + ) + + // Simulate relay to Charlie + self.charlie.simulateIncomingPacket(relayPacket) + } } } - } - - charlie.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && - message.originalSender == TestConstants.testNickname1 && - message.isRelay { - expectation.fulfill() + + charlie.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && + message.originalSender == TestConstants.testNickname1 && + message.isRelay { + charlieReceivesMessage() + } } + + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - // Alice sends message - alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } - func testMultiHopRelay() { + @Test func multiHopRelay() async { // Topology: Alice -> Bob -> Charlie -> David - simulateConnection(alice, bob) - simulateConnection(bob, charlie) - simulateConnection(charlie, david) + alice.simulateConnection(with: bob) + bob.simulateConnection(with: charlie) + charlie.simulateConnection(with: david) - let expectation = XCTestExpectation(description: "David receives multi-hop message") - - // Set up relay chain - setupRelayHandler(bob, nextHops: [charlie]) - setupRelayHandler(charlie, nextHops: [david]) - // Allow handlers to install - let sendDelay = DispatchTime.now() + 0.05 - - david.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 && - message.originalSender == TestConstants.testNickname1 && - message.isRelay { - expectation.fulfill() + await confirmation("David receives multi-hop message") { davidReceivesMessage in + // Set up relay chain + setupRelayHandler(bob, nextHops: [charlie]) + setupRelayHandler(charlie, nextHops: [david]) + + david.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 && + message.originalSender == TestConstants.testNickname1 && + message.isRelay { + davidReceivesMessage() + } } + + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - // Alice sends message - DispatchQueue.main.asyncAfter(deadline: sendDelay) { - self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) - } - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } // MARK: - TTL (Time To Live) Tests - func testTTLDecrement() { + @Test func ttlDecrement() async { // Create a chain longer than TTL - let nodes = [alice!, bob!, charlie!, david!] + let nodes = [alice, bob, charlie, david] // Connect in chain for i in 0.. 0 && i < nodes.count-1 { setupRelayHandler(nodes[i], nextHops: [nodes[i+1]]) } } - let expectation = XCTestExpectation(description: "Message dropped due to TTL") - expectation.isInverted = true // Should NOT be fulfilled - - david.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - expectation.fulfill() // This should not happen + await confirmation("Message dropped due to TTL", expectedCount: 0) { receiveMessage in + david.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + receiveMessage() // This should not happen + } + } + + // Inject at Bob with TTL=2 so Charlie sees it (TTL->1) and does not relay to David + let msg = TestHelpers.createTestMessage( + content: TestConstants.testMessage1, + sender: TestConstants.testNickname1, + senderPeerID: PeerID(str: alice.peerID) + ) + + if let payload = msg.toBinaryPayload() { + let pkt = TestHelpers.createTestPacket(senderID: PeerID(str: alice.peerID), payload: payload, ttl: 2) + bob.simulateIncomingPacket(pkt) } } - - // Inject at Bob with TTL=2 so Charlie sees it (TTL->1) and does not relay to David - let msg = TestHelpers.createTestMessage(content: TestConstants.testMessage1, sender: TestConstants.testNickname1, senderPeerID: PeerID(str: alice.peerID)) - if let payload = msg.toBinaryPayload() { - let pkt = TestHelpers.createTestPacket(senderID: PeerID(str: alice.peerID), payload: payload, ttl: 2) - bob.simulateIncomingPacket(pkt) - } - - wait(for: [expectation], timeout: TestConstants.shortTimeout) } - func testZeroTTLNotRelayed() { - simulateConnection(alice, bob) - simulateConnection(bob, charlie) + @Test func zeroTTLNotRelayed() async { + alice.simulateConnection(with: bob) + bob.simulateConnection(with: charlie) - let expectation = XCTestExpectation(description: "Zero TTL message not relayed") - expectation.isInverted = true - - charlie.messageDeliveryHandler = { message in - if message.content == "Zero TTL message" { - expectation.fulfill() // Should not happen + await confirmation("Zero TTL message not relayed", expectedCount: 0) { receiveMessage in + charlie.messageDeliveryHandler = { message in + if message.content == "Zero TTL message" { + receiveMessage() // Should not happen + } + } + + // Create packet with TTL=0 + let message = TestHelpers.createTestMessage(content: "Zero TTL message") + if let payload = message.toBinaryPayload() { + let packet = TestHelpers.createTestPacket(payload: payload, ttl: 0) + alice.simulateIncomingPacket(packet) } } - - // Create packet with TTL=0 - let message = TestHelpers.createTestMessage(content: "Zero TTL message") - if let payload = message.toBinaryPayload() { - let packet = TestHelpers.createTestPacket(payload: payload, ttl: 0) - alice.simulateIncomingPacket(packet) - } - - wait(for: [expectation], timeout: TestConstants.shortTimeout) } // MARK: - Duplicate Detection Tests - func testDuplicateMessagePrevention() { - simulateConnection(alice, bob) + @Test func duplicateMessagePrevention() async { + alice.simulateConnection(with: bob) var messageCount = 0 - let expectation = XCTestExpectation(description: "Only one message received") - bob.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - messageCount += 1 - if messageCount == 1 { - // Send duplicate after small delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil, messageID: message.id) + await confirmation("Only one message received") { receiveMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + receiveMessage() + messageCount += 1 + if messageCount == 1 { + // Send duplicate after small delay + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil, messageID: message.id) + } else { + Issue.record("Duplicate message was not filtered") } - } else { - XCTFail("Duplicate message was not filtered") } } + + // Send original message + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } + } + + @Test func duplicateContentAsNewMessageNotPrevented() async { + alice.simulateConnection(with: bob) - // Send original message - alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) + var messageCount = 0 - // Wait to ensure duplicate would have been received - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - XCTAssertEqual(messageCount, 1) - expectation.fulfill() + await confirmation("Only one message received", expectedCount: 2) { receiveMessage in + bob.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + receiveMessage() + messageCount += 1 + if messageCount == 1 { + // Send the same content as a new message + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) + } + } + } + + // Send original message + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } // MARK: - Mention Tests - func testMessageWithMentions() { - simulateConnection(alice, bob) - simulateConnection(alice, charlie) - - let expectation = XCTestExpectation(description: "Mentioned users receive notification") + @Test func messageWithMentions() async { + alice.simulateConnection(with: bob) + alice.simulateConnection(with: charlie) var mentionedUsers: Set = [] - bob.messageDeliveryHandler = { message in - if let mentions = message.mentions, mentions.contains(TestConstants.testNickname2) { - mentionedUsers.insert(TestConstants.testNickname2) + await confirmation("Mentioned users receive notification", expectedCount: 2) { receiveMention in + bob.messageDeliveryHandler = { message in + if message.mentions?.contains(TestConstants.testNickname2) == true { + mentionedUsers.insert(TestConstants.testNickname2) + receiveMention() + } } - } - - charlie.messageDeliveryHandler = { message in - if let mentions = message.mentions, mentions.contains(TestConstants.testNickname3) { - mentionedUsers.insert(TestConstants.testNickname3) + + charlie.messageDeliveryHandler = { message in + if message.mentions?.contains(TestConstants.testNickname3) == true { + mentionedUsers.insert(TestConstants.testNickname3) + receiveMention() + } } + + // Alice mentions Bob and Charlie + alice.sendMessage( + "Hey @\(TestConstants.testNickname2) and @\(TestConstants.testNickname3)!", + mentions: [TestConstants.testNickname2, TestConstants.testNickname3], + to: nil + ) } - // Alice mentions Bob and Charlie - alice.sendMessage( - "Hey @\(TestConstants.testNickname2) and @\(TestConstants.testNickname3)!", - mentions: [TestConstants.testNickname2, TestConstants.testNickname3], - to: nil - ) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - XCTAssertEqual(mentionedUsers, [TestConstants.testNickname2, TestConstants.testNickname3]) - expectation.fulfill() - } - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) + #expect(mentionedUsers == [TestConstants.testNickname2, TestConstants.testNickname3]) } // MARK: - Network Topology Tests - func testMeshTopologyBroadcast() { + @Test func meshTopologyBroadcast() async { // Create mesh: Everyone connected to everyone - let nodes = [alice!, bob!, charlie!, david!] + let nodes = [alice, bob, charlie, david] for i in 0.. 0 { - node.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - receivedCount += 1 - if receivedCount == 3 { // Bob, Charlie, David - expectation.fulfill() + await confirmation("All nodes receive message", expectedCount: 3) { receiveMessage in + for (index, node) in nodes.enumerated() where index > 0 { + node.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + receiveMessage() } } } + + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - // Alice broadcasts - alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } - func testPartialMeshRelay() { + @Test func partialMeshRelay() async { // Partial mesh: Alice -> Bob, Bob -> Charlie, Charlie -> David, David -> Alice - simulateConnection(alice, bob) - simulateConnection(bob, charlie) - simulateConnection(charlie, david) - simulateConnection(david, alice) + alice.simulateConnection(with: bob) + bob.simulateConnection(with: charlie) + charlie.simulateConnection(with: david) + david.simulateConnection(with: alice) // Setup relay handlers setupRelayHandler(bob, nextHops: [charlie]) setupRelayHandler(charlie, nextHops: [david]) setupRelayHandler(david, nextHops: [alice]) - let sendDelay2 = DispatchTime.now() + 0.05 - var receivedCount = 0 - let expectation = XCTestExpectation(description: "Message reaches all nodes once") - - let checkCompletion = { - if receivedCount == 3 { // Bob, Charlie, David should receive - expectation.fulfill() - } - } - - for node in [bob!, charlie!, david!] { - node.messageDeliveryHandler = { message in - if message.content == TestConstants.testMessage1 { - receivedCount += 1 - checkCompletion() + await confirmation("Message reaches all nodes once", expectedCount: 3) { receiveMessage in + for node in [bob, charlie, david] { + node.messageDeliveryHandler = { message in + if message.content == TestConstants.testMessage1 { + receiveMessage() + } } } + + alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) } - - // Alice broadcasts - DispatchQueue.main.asyncAfter(deadline: sendDelay2) { - self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil) - } - - wait(for: [expectation], timeout: TestConstants.defaultTimeout) } // MARK: - Performance and Stress Tests - func testHighVolumeMessaging() { - simulateConnection(alice, bob) + @Test func highVolumeMessaging() async { + alice.simulateConnection(with: bob) let messageCount = 100 - var receivedCount = 0 - let expectation = XCTestExpectation(description: "All messages received") - bob.messageDeliveryHandler = { message in - if message.sender == TestConstants.testNickname1 { - receivedCount += 1 - if receivedCount == messageCount { - expectation.fulfill() + await confirmation("All messages received", expectedCount: messageCount) { receiveMessage in + bob.messageDeliveryHandler = { message in + if message.sender == TestConstants.testNickname1 { + receiveMessage() } } - } - - // Send many messages rapidly - for i in 0.. MockBluetoothMeshService { - let service = MockBluetoothMeshService() - service.myPeerID = peerID.id - service.mockNickname = nickname - return service - } - - private func simulateConnection(_ peer1: MockBluetoothMeshService, _ peer2: MockBluetoothMeshService) { - peer1.simulateConnectedPeer(peer2.peerID) - peer2.simulateConnectedPeer(peer1.peerID) - } - - private func setupRelayHandler(_ node: MockBluetoothMeshService, nextHops: [MockBluetoothMeshService]) { + + private func setupRelayHandler(_ node: MockBLEService, nextHops: [MockBLEService]) { node.packetDeliveryHandler = { packet in // Check if should relay guard packet.ttl > 1 else { return } - + if let message = BitchatMessage(packet.payload) { // Don't relay own messages - guard message.senderPeerID != node.peerID else { return } + guard message.senderPeerID?.id != node.peerID else { return } // Create relay message let relayMessage = BitchatMessage( diff --git a/bitchatTests/Fragmentation/FragmentationTests.swift b/bitchatTests/Fragmentation/FragmentationTests.swift index ba3173b1..b3b41e7a 100644 --- a/bitchatTests/Fragmentation/FragmentationTests.swift +++ b/bitchatTests/Fragmentation/FragmentationTests.swift @@ -6,26 +6,125 @@ // For more information, see // -import XCTest +import Testing +import Foundation @testable import bitchat -final class FragmentationTests: XCTestCase { +struct FragmentationTests { - private var mockKeychain: MockKeychain! - private var mockIdentityManager: MockIdentityManager! + private let mockKeychain: MockKeychain + private let mockIdentityManager: MockIdentityManager - override func setUp() { - super.setUp() + init() { mockKeychain = MockKeychain() mockIdentityManager = MockIdentityManager(mockKeychain) } - override func tearDown() { - mockKeychain = nil - mockIdentityManager = nil - super.tearDown() + @Test("Reassembly from fragments delivers a public message") + func reassemblyFromFragmentsDeliversPublicMessage() async throws { + let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) + let capture = CaptureDelegate() + ble.delegate = capture + + // Construct a big packet (3KB) from a remote sender (not our own ID) + let remoteShortID = "1122334455667788" + let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 3_000) + + // Use a small fragment size to ensure multiple pieces + let fragments = fragmentPacket(original, fragmentSize: 400) + + // Shuffle fragments to simulate out-of-order arrival + let shuffled = fragments.shuffled() + + // Inject fragments spaced out to avoid concurrent mutation inside BLEService + for (i, fragment) in shuffled.enumerated() { + let delay = UInt64(5 * i) * 1_000_000 // nanoseconds + Task { + try await Task.sleep(nanoseconds: delay) + ble._test_handlePacket(fragment, fromPeerID: remoteShortID) + } + } + + // Allow async processing + try await Task.sleep(nanoseconds: 500_000_000) // 0.5s + + #expect(capture.publicMessages.count == 1) + #expect(capture.publicMessages.first?.content.count == 3_000) } + + @Test("Duplicate fragment does not break reassembly") + func duplicateFragmentDoesNotBreakReassembly() async throws { + let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) + let capture = CaptureDelegate() + ble.delegate = capture + + let remoteShortID = "A1B2C3D4E5F60708" + let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 2048) + var frags = fragmentPacket(original, fragmentSize: 300) + + // Duplicate one fragment + if let dup = frags.first { + frags.insert(dup, at: 1) + } + + for (i, fragment) in frags.enumerated() { + let delay = UInt64(5 * i) * 1_000_000 // nanoseconds + Task { + try await Task.sleep(nanoseconds: delay) + ble._test_handlePacket(fragment, fromPeerID: remoteShortID) + } + } + + // Allow async processing + try await Task.sleep(nanoseconds: 500_000_000) // 0.5s + + #expect(capture.publicMessages.count == 1) + #expect(capture.publicMessages.first?.content.count == 2048) + } + + @Test("Invalid fragment header is ignored") + func invalidFragmentHeaderIsIgnored() async throws { + let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) + let capture = CaptureDelegate() + ble.delegate = capture + + let remoteShortID = "0011223344556677" + let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 1000) + let fragments = fragmentPacket(original, fragmentSize: 250) + + // Corrupt one fragment: make payload too short (header incomplete) + var corrupted = fragments + if !corrupted.isEmpty { + var p = corrupted[0] + p = BitchatPacket( + type: p.type, + senderID: p.senderID, + recipientID: p.recipientID, + timestamp: p.timestamp, + payload: Data([0x00, 0x01, 0x02]), // invalid header + signature: nil, + ttl: p.ttl + ) + corrupted[0] = p + } + + for (i, fragment) in corrupted.enumerated() { + let delay = UInt64(5 * i) * 1_000_000 // nanoseconds + Task { + try await Task.sleep(nanoseconds: delay) + ble._test_handlePacket(fragment, fromPeerID: remoteShortID) + } + } + + // Allow async processing + try await Task.sleep(nanoseconds: 500_000_000) // 0.5s + + // Should not deliver since one fragment is invalid and reassembly can't complete + #expect(capture.publicMessages.isEmpty) + } +} +extension FragmentationTests { private final class CaptureDelegate: BitchatDelegate { var publicMessages: [(peerID: String, nickname: String, content: String)] = [] func didReceiveMessage(_ message: BitchatMessage) {} @@ -88,101 +187,4 @@ final class FragmentationTests: XCTestCase { } return packets } - - func test_reassembly_from_fragments_delivers_public_message() { - let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) - let capture = CaptureDelegate() - ble.delegate = capture - - // Construct a big packet (3KB) from a remote sender (not our own ID) - let remoteShortID = "1122334455667788" - let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 3_000) - // Use a small fragment size to ensure multiple pieces - let fragments = fragmentPacket(original, fragmentSize: 400) - - // Shuffle fragments to simulate out-of-order arrival - let shuffled = fragments.shuffled() - - // Inject fragments spaced out to avoid concurrent mutation inside BLEService - for (i, f) in shuffled.enumerated() { - let delay = DispatchTime.now() + .milliseconds(5 * i) - DispatchQueue.global().asyncAfter(deadline: delay) { - ble._test_handlePacket(f, fromPeerID: remoteShortID) - } - } - - // Allow async processing - let exp = expectation(description: "reassembled") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } - wait(for: [exp], timeout: 2.0) - - XCTAssertEqual(capture.publicMessages.count, 1) - XCTAssertEqual(capture.publicMessages.first?.content.count, 3_000) - } - - func test_duplicate_fragment_does_not_break_reassembly() { - let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) - let capture = CaptureDelegate() - ble.delegate = capture - - let remoteShortID = "A1B2C3D4E5F60708" - let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 2048) - var frags = fragmentPacket(original, fragmentSize: 300) - // Duplicate one fragment - if let dup = frags.first { frags.insert(dup, at: 1) } - - for (i, f) in frags.enumerated() { - let delay = DispatchTime.now() + .milliseconds(5 * i) - DispatchQueue.global().asyncAfter(deadline: delay) { - ble._test_handlePacket(f, fromPeerID: remoteShortID) - } - } - - let exp = expectation(description: "reassembled2") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } - wait(for: [exp], timeout: 2.0) - - XCTAssertEqual(capture.publicMessages.count, 1) - XCTAssertEqual(capture.publicMessages.first?.content.count, 2048) - } - - func test_invalid_fragment_header_is_ignored() { - let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager) - let capture = CaptureDelegate() - ble.delegate = capture - - let remoteShortID = "0011223344556677" - let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 1000) - let fragments = fragmentPacket(original, fragmentSize: 250) - - // Corrupt one fragment: make payload too short (header incomplete) - var corrupted = fragments - if !corrupted.isEmpty { - var p = corrupted[0] - p = BitchatPacket( - type: p.type, - senderID: p.senderID, - recipientID: p.recipientID, - timestamp: p.timestamp, - payload: Data([0x00, 0x01, 0x02]), // invalid header - signature: nil, - ttl: p.ttl - ) - corrupted[0] = p - } - - for (i, f) in corrupted.enumerated() { - let delay = DispatchTime.now() + .milliseconds(5 * i) - DispatchQueue.global().asyncAfter(deadline: delay) { - ble._test_handlePacket(f, fromPeerID: remoteShortID) - } - } - - let exp = expectation(description: "no reassembly") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } - wait(for: [exp], timeout: 2.0) - - // Should not deliver since one fragment is invalid and reassembly can't complete - XCTAssertEqual(capture.publicMessages.count, 0) - } } diff --git a/bitchatTests/Mocks/MockBLEService.swift b/bitchatTests/Mocks/MockBLEService.swift index 6d564725..c84c5d2f 100644 --- a/bitchatTests/Mocks/MockBLEService.swift +++ b/bitchatTests/Mocks/MockBLEService.swift @@ -349,3 +349,18 @@ final class MockBLEService: NSObject { // Backward compatibility for older tests typealias MockSimplifiedBluetoothService = MockBLEService + +// MARK: - Helpers + +extension MockBLEService { + convenience init(peerID: PeerID, nickname: String) { + self.init() + myPeerID = peerID.id + mockNickname = nickname + } + + func simulateConnection(with otherPeer: MockBLEService) { + simulateConnectedPeer(otherPeer.myPeerID) + otherPeer.simulateConnectedPeer(myPeerID) + } +}