mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
Swift Testing (#748)
* Swift Testing: `PrivateChatE2ETests` + minor refactor * Swift Testing: `PublicChatE2ETests` * Swift Testing: `FragmentationTests` * Fix MockBLEService init to accept PeerID and remove _testRegister call * Add peerID property to MockBLEService and fix ttlDecrement test * Remove duplicate peerID property and fix type comparisons --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
...
|
||||||
@@ -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
|
||||||
|
...
|
||||||
@@ -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
|
||||||
|
...
|
||||||
@@ -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
|
||||||
|
...
|
||||||
@@ -6,140 +6,110 @@
|
|||||||
// For more information, see <https://unlicense.org>
|
// For more information, see <https://unlicense.org>
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import Testing
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
final class PrivateChatE2ETests: XCTestCase {
|
// TODO: Remove once MockBLEService is refactored to fix race condition
|
||||||
|
@Suite(.serialized)
|
||||||
|
struct PrivateChatE2ETests {
|
||||||
|
|
||||||
var alice: MockBluetoothMeshService!
|
private let alice: MockBLEService
|
||||||
var bob: MockBluetoothMeshService!
|
private let bob: MockBLEService
|
||||||
var charlie: MockBluetoothMeshService!
|
private let charlie: MockBLEService
|
||||||
|
private let mockKeychain: MockKeychain
|
||||||
|
|
||||||
private var mockKeychain: MockKeychain!
|
init() {
|
||||||
|
|
||||||
override func setUp() {
|
|
||||||
super.setUp()
|
|
||||||
MockBLEService.resetTestBus()
|
MockBLEService.resetTestBus()
|
||||||
|
|
||||||
// Create services
|
// Create services
|
||||||
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
alice = MockBLEService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
||||||
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
bob = MockBLEService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
||||||
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
charlie = MockBLEService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
||||||
mockKeychain = MockKeychain()
|
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
|
// MARK: - Basic Private Messaging Tests
|
||||||
|
|
||||||
func testSimplePrivateMessageShouldNotBeSentWithoutConnection() {
|
@Test func simplePrivateMessageShouldNotBeSentWithoutConnection() async {
|
||||||
// Intentionally commented out to test
|
// Intentionally not connecting alice and bob to test
|
||||||
// simulateConnection(alice, bob)
|
|
||||||
|
var bobReceivedMessage = false
|
||||||
let expectation = XCTestExpectation(description: "Bob should not receive a private message")
|
|
||||||
expectation.isInverted = true
|
await confirmation("Bob should not receive a private message", expectedCount: 0) { bobReceivesMessage in
|
||||||
|
bob.messageDeliveryHandler = { message in
|
||||||
bob.messageDeliveryHandler = { message in
|
if message.content == TestConstants.testMessage1 &&
|
||||||
if message.content == TestConstants.testMessage1 &&
|
message.isPrivate &&
|
||||||
message.isPrivate &&
|
message.sender == TestConstants.testNickname1 {
|
||||||
message.sender == TestConstants.testNickname1 {
|
bobReceivedMessage = true
|
||||||
expectation.fulfill()
|
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
|
#expect(!bobReceivedMessage, "Bob should not have received the message")
|
||||||
alice.sendPrivateMessage(
|
|
||||||
TestConstants.testMessage1,
|
|
||||||
to: TestConstants.testPeerID2,
|
|
||||||
recipientNickname: TestConstants.testNickname2
|
|
||||||
)
|
|
||||||
|
|
||||||
wait(for: [expectation], timeout: TestConstants.shortTimeout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSimplePrivateMessage() {
|
@Test func simplePrivateMessage() async {
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Bob receives private message")
|
await confirmation("Bob receives private message") { bobReceivesMessage in
|
||||||
|
bob.messageDeliveryHandler = { message in
|
||||||
bob.messageDeliveryHandler = { message in
|
if message.content == TestConstants.testMessage1 &&
|
||||||
if message.content == TestConstants.testMessage1 &&
|
message.isPrivate &&
|
||||||
message.isPrivate &&
|
message.sender == TestConstants.testNickname1 {
|
||||||
message.sender == TestConstants.testNickname1 {
|
bobReceivesMessage()
|
||||||
expectation.fulfill()
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Alice sends private message to Bob
|
||||||
// Alice sends private message to Bob
|
alice.sendPrivateMessage(
|
||||||
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(
|
|
||||||
TestConstants.testMessage1,
|
TestConstants.testMessage1,
|
||||||
to: TestConstants.testPeerID2,
|
to: TestConstants.testPeerID2,
|
||||||
recipientNickname: TestConstants.testNickname2
|
recipientNickname: TestConstants.testNickname2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
wait(for: [bobExpectation, charlieExpectation], timeout: TestConstants.shortTimeout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delivery Acknowledgment Tests
|
@Test func privateMessageNotReceivedByOthers() async {
|
||||||
|
alice.simulateConnection(with: bob)
|
||||||
// NOTE: DeliveryTracker has been removed in BLEService.
|
alice.simulateConnection(with: charlie)
|
||||||
// Delivery tracking is now handled internally.
|
|
||||||
|
await confirmation("Bob receives private message") { bobReceivesMessage in
|
||||||
|
bob.messageDeliveryHandler = { message in
|
||||||
// MARK: - Message Retry Tests
|
if message.content == TestConstants.testMessage1 && message.isPrivate {
|
||||||
|
bobReceivesMessage()
|
||||||
// NOTE: MessageRetryService has been removed in BLEService.
|
}
|
||||||
// Retry logic is now handled internally.
|
}
|
||||||
|
|
||||||
|
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
|
// MARK: - End-to-End Encryption Tests
|
||||||
|
|
||||||
func testPrivateMessageEncryption() {
|
@Test func privateMessageEncryption() async {
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
|
|
||||||
// Setup Noise sessions
|
// Setup Noise sessions
|
||||||
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
@@ -155,161 +125,139 @@ final class PrivateChatE2ETests: XCTestCase {
|
|||||||
let handshake3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: handshake2)!
|
let handshake3 = try aliceManager.handleIncomingHandshake(from: TestConstants.testPeerID2, message: handshake2)!
|
||||||
_ = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: handshake3)
|
_ = try bobManager.handleIncomingHandshake(from: TestConstants.testPeerID1, message: handshake3)
|
||||||
} catch {
|
} catch {
|
||||||
XCTFail("Failed to establish Noise session: \(error)")
|
Issue.record("Failed to establish Noise session: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Encrypted message received")
|
await confirmation("Encrypted message received") { receiveEncryptedMessage in
|
||||||
|
// Setup packet handlers for encryption
|
||||||
// Setup packet handlers for encryption
|
alice.packetDeliveryHandler = { packet in
|
||||||
alice.packetDeliveryHandler = { packet in
|
// Encrypt outgoing private messages
|
||||||
// Encrypt outgoing private messages
|
if packet.type == 0x01,
|
||||||
if packet.type == 0x01,
|
let message = BitchatMessage(packet.payload),
|
||||||
let message = BitchatMessage(packet.payload),
|
message.isPrivate {
|
||||||
message.isPrivate {
|
do {
|
||||||
do {
|
let encrypted = try aliceManager.encrypt(packet.payload, for: TestConstants.testPeerID2)
|
||||||
let encrypted = try aliceManager.encrypt(packet.payload, for: TestConstants.testPeerID2)
|
let encryptedPacket = BitchatPacket(
|
||||||
let encryptedPacket = BitchatPacket(
|
type: 0x02, // Encrypted message type
|
||||||
type: 0x02, // Encrypted message type
|
senderID: packet.senderID,
|
||||||
senderID: packet.senderID,
|
recipientID: packet.recipientID,
|
||||||
recipientID: packet.recipientID,
|
timestamp: packet.timestamp,
|
||||||
timestamp: packet.timestamp,
|
payload: encrypted,
|
||||||
payload: encrypted,
|
signature: packet.signature,
|
||||||
signature: packet.signature,
|
ttl: packet.ttl
|
||||||
ttl: packet.ttl
|
)
|
||||||
)
|
self.bob.simulateIncomingPacket(encryptedPacket)
|
||||||
self.bob.simulateIncomingPacket(encryptedPacket)
|
} catch {
|
||||||
} catch {
|
Issue.record("Encryption failed: \(error)")
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
XCTFail("Decryption failed: \(error)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
bob.packetDeliveryHandler = { packet in
|
||||||
// Send encrypted private message
|
// Decrypt incoming encrypted messages
|
||||||
alice.sendPrivateMessage(
|
if packet.type == 0x02 {
|
||||||
TestConstants.testMessage1,
|
do {
|
||||||
to: TestConstants.testPeerID2,
|
let decrypted = try bobManager.decrypt(packet.payload, from: TestConstants.testPeerID1)
|
||||||
recipientNickname: TestConstants.testNickname2
|
if let message = BitchatMessage(decrypted) {
|
||||||
)
|
#expect(message.content == TestConstants.testMessage1)
|
||||||
|
#expect(message.isPrivate)
|
||||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
receiveEncryptedMessage()
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
// MARK: - Multi-hop Private Message Tests
|
Issue.record("Decryption failed: \(error)")
|
||||||
|
}
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Send encrypted private message
|
||||||
// Send many private messages
|
|
||||||
for i in 0..<messageCount {
|
|
||||||
alice.sendPrivateMessage(
|
alice.sendPrivateMessage(
|
||||||
"Private message \(i)",
|
TestConstants.testMessage1,
|
||||||
to: TestConstants.testPeerID2,
|
to: TestConstants.testPeerID2,
|
||||||
recipientNickname: TestConstants.testNickname2
|
recipientNickname: TestConstants.testNickname2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
|
||||||
XCTAssertEqual(receivedCount, messageCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLargePrivateMessage() {
|
// MARK: - Multi-hop Private Message Tests
|
||||||
simulateConnection(alice, bob)
|
|
||||||
|
@Test func privateMessageRelay() async {
|
||||||
|
// Setup: Alice -> 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
|
let messageCount = 100
|
||||||
if message.content == TestConstants.testLongMessage && message.isPrivate {
|
var receivedCount = 0
|
||||||
expectation.fulfill()
|
|
||||||
|
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..<messageCount {
|
||||||
|
alice.sendPrivateMessage(
|
||||||
|
"Private message \(i)",
|
||||||
|
to: TestConstants.testPeerID2,
|
||||||
|
recipientNickname: TestConstants.testNickname2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alice.sendPrivateMessage(
|
|
||||||
TestConstants.testLongMessage,
|
|
||||||
to: TestConstants.testPeerID2,
|
|
||||||
recipientNickname: TestConstants.testNickname2
|
|
||||||
)
|
|
||||||
|
|
||||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
@Test func largePrivateMessage() async {
|
||||||
|
alice.simulateConnection(with: bob)
|
||||||
private func createMockService(peerID: PeerID, nickname: String) -> MockBluetoothMeshService {
|
|
||||||
let service = MockBluetoothMeshService()
|
await confirmation("Large private message received") { receiveLargeMessage in
|
||||||
service.myPeerID = peerID.id
|
bob.messageDeliveryHandler = { message in
|
||||||
service.mockNickname = nickname
|
if message.content == TestConstants.testLongMessage && message.isPrivate {
|
||||||
return service
|
receiveLargeMessage()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private func simulateConnection(_ peer1: MockBluetoothMeshService, _ peer2: MockBluetoothMeshService) {
|
|
||||||
peer1.simulateConnectedPeer(peer2.peerID)
|
alice.sendPrivateMessage(
|
||||||
peer2.simulateConnectedPeer(peer1.peerID)
|
TestConstants.testLongMessage,
|
||||||
|
to: TestConstants.testPeerID2,
|
||||||
|
recipientNickname: TestConstants.testNickname2
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,439 +6,390 @@
|
|||||||
// For more information, see <https://unlicense.org>
|
// For more information, see <https://unlicense.org>
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import Testing
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
final class PublicChatE2ETests: XCTestCase {
|
@Suite(.serialized)
|
||||||
|
struct PublicChatE2ETests {
|
||||||
|
|
||||||
var alice: MockBluetoothMeshService!
|
private let alice: MockBLEService
|
||||||
var bob: MockBluetoothMeshService!
|
private let bob: MockBLEService
|
||||||
var charlie: MockBluetoothMeshService!
|
private let charlie: MockBLEService
|
||||||
var david: MockBluetoothMeshService!
|
private let david: MockBLEService
|
||||||
|
|
||||||
var receivedMessages: [String: [BitchatMessage]] = [:]
|
private var receivedMessages: [String: [BitchatMessage]] = [:]
|
||||||
|
|
||||||
override func setUp() {
|
init() {
|
||||||
super.setUp()
|
|
||||||
MockBLEService.resetTestBus()
|
MockBLEService.resetTestBus()
|
||||||
|
|
||||||
// Create mock services
|
// Create mock services
|
||||||
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
alice = MockBLEService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
|
||||||
bob = createMockService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
bob = MockBLEService(peerID: TestConstants.testPeerID2, nickname: TestConstants.testNickname2)
|
||||||
charlie = createMockService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
charlie = MockBLEService(peerID: TestConstants.testPeerID3, nickname: TestConstants.testNickname3)
|
||||||
david = createMockService(peerID: TestConstants.testPeerID4, nickname: TestConstants.testNickname4)
|
david = MockBLEService(peerID: TestConstants.testPeerID4, nickname: TestConstants.testNickname4)
|
||||||
|
|
||||||
// Clear received messages
|
|
||||||
receivedMessages.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDown() {
|
|
||||||
alice = nil
|
|
||||||
bob = nil
|
|
||||||
charlie = nil
|
|
||||||
david = nil
|
|
||||||
super.tearDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Basic Broadcasting Tests
|
// MARK: - Basic Broadcasting Tests
|
||||||
|
|
||||||
func testSimplePublicMessage() {
|
@Test func simplePublicMessage() async {
|
||||||
// Connect Alice and Bob
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(alice, bob)
|
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Bob receives message")
|
await confirmation("Bob receives message") { bobReceivesMessage in
|
||||||
|
bob.messageDeliveryHandler = { message in
|
||||||
bob.messageDeliveryHandler = { message in
|
if message.content == TestConstants.testMessage1 && message.sender == TestConstants.testNickname1 {
|
||||||
if message.content == TestConstants.testMessage1 && message.sender == TestConstants.testNickname1 {
|
bobReceivesMessage()
|
||||||
expectation.fulfill()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
@Test func multiRecipientBroadcast() async {
|
||||||
// Connect Alice to Bob and Charlie
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: charlie)
|
||||||
simulateConnection(alice, charlie)
|
|
||||||
|
|
||||||
let bobExpectation = XCTestExpectation(description: "Bob receives message")
|
var bobReceivedMessage = false
|
||||||
let charlieExpectation = XCTestExpectation(description: "Charlie receives message")
|
var charlieReceivedMessage = false
|
||||||
|
|
||||||
bob.messageDeliveryHandler = { message in
|
await confirmation("Both recieve message", expectedCount: 2) { receiveMessage in
|
||||||
if message.content == TestConstants.testMessage1 {
|
bob.messageDeliveryHandler = { message in
|
||||||
bobExpectation.fulfill()
|
if message.content == TestConstants.testMessage1 {
|
||||||
|
if !bobReceivedMessage {
|
||||||
|
bobReceivedMessage = true
|
||||||
|
receiveMessage()
|
||||||
|
} else {
|
||||||
|
Issue.record("Bob received more than once")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
charlie.messageDeliveryHandler = { message in
|
||||||
charlie.messageDeliveryHandler = { message in
|
if message.content == TestConstants.testMessage1 {
|
||||||
if message.content == TestConstants.testMessage1 {
|
if !charlieReceivedMessage {
|
||||||
charlieExpectation.fulfill()
|
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
|
// MARK: - Message Routing and Relay Tests
|
||||||
|
|
||||||
func testMessageRelayChain() {
|
@Test func messageRelayChain() async {
|
||||||
// Linear topology: Alice -> Bob -> Charlie
|
// Linear topology: Alice -> Bob -> Charlie
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(bob, charlie)
|
bob.simulateConnection(with: charlie)
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Charlie receives relayed message")
|
await confirmation("Charlie receives relayed message") { charlieReceivesMessage in
|
||||||
|
// Set up relay in Bob
|
||||||
// Set up relay in Bob
|
bob.packetDeliveryHandler = { packet in
|
||||||
bob.packetDeliveryHandler = { packet in
|
// Bob should relay to Charlie
|
||||||
// Bob should relay to Charlie
|
if let message = BitchatMessage(packet.payload),
|
||||||
if let message = BitchatMessage(packet.payload),
|
message.sender == TestConstants.testNickname1 {
|
||||||
message.sender == TestConstants.testNickname1 {
|
|
||||||
|
// Create relay message
|
||||||
// Create relay message
|
let relayMessage = BitchatMessage(
|
||||||
let relayMessage = BitchatMessage(
|
id: message.id,
|
||||||
id: message.id,
|
sender: message.sender,
|
||||||
sender: message.sender,
|
content: message.content,
|
||||||
content: message.content,
|
timestamp: message.timestamp,
|
||||||
timestamp: message.timestamp,
|
isRelay: true,
|
||||||
isRelay: true,
|
originalSender: message.sender,
|
||||||
originalSender: message.sender,
|
isPrivate: message.isPrivate,
|
||||||
isPrivate: message.isPrivate,
|
recipientNickname: message.recipientNickname,
|
||||||
recipientNickname: message.recipientNickname,
|
senderPeerID: message.senderPeerID,
|
||||||
senderPeerID: message.senderPeerID,
|
mentions: message.mentions
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Simulate relay to Charlie
|
if let relayPayload = relayMessage.toBinaryPayload() {
|
||||||
self.charlie.simulateIncomingPacket(relayPacket)
|
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
|
||||||
charlie.messageDeliveryHandler = { message in
|
if message.content == TestConstants.testMessage1 &&
|
||||||
if message.content == TestConstants.testMessage1 &&
|
message.originalSender == TestConstants.testNickname1 &&
|
||||||
message.originalSender == TestConstants.testNickname1 &&
|
message.isRelay {
|
||||||
message.isRelay {
|
charlieReceivesMessage()
|
||||||
expectation.fulfill()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Topology: Alice -> Bob -> Charlie -> David
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(bob, charlie)
|
bob.simulateConnection(with: charlie)
|
||||||
simulateConnection(charlie, david)
|
charlie.simulateConnection(with: david)
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "David receives multi-hop message")
|
await confirmation("David receives multi-hop message") { davidReceivesMessage in
|
||||||
|
// Set up relay chain
|
||||||
// Set up relay chain
|
setupRelayHandler(bob, nextHops: [charlie])
|
||||||
setupRelayHandler(bob, nextHops: [charlie])
|
setupRelayHandler(charlie, nextHops: [david])
|
||||||
setupRelayHandler(charlie, nextHops: [david])
|
|
||||||
// Allow handlers to install
|
david.messageDeliveryHandler = { message in
|
||||||
let sendDelay = DispatchTime.now() + 0.05
|
if message.content == TestConstants.testMessage1 &&
|
||||||
|
message.originalSender == TestConstants.testNickname1 &&
|
||||||
david.messageDeliveryHandler = { message in
|
message.isRelay {
|
||||||
if message.content == TestConstants.testMessage1 &&
|
davidReceivesMessage()
|
||||||
message.originalSender == TestConstants.testNickname1 &&
|
}
|
||||||
message.isRelay {
|
|
||||||
expectation.fulfill()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// MARK: - TTL (Time To Live) Tests
|
||||||
|
|
||||||
func testTTLDecrement() {
|
@Test func ttlDecrement() async {
|
||||||
// Create a chain longer than TTL
|
// Create a chain longer than TTL
|
||||||
let nodes = [alice!, bob!, charlie!, david!]
|
let nodes = [alice, bob, charlie, david]
|
||||||
|
|
||||||
// Connect in chain
|
// Connect in chain
|
||||||
for i in 0..<nodes.count-1 {
|
for i in 0..<nodes.count-1 {
|
||||||
simulateConnection(nodes[i], nodes[i+1])
|
nodes[i].simulateConnection(with: nodes[i+1])
|
||||||
if i > 0 && i < nodes.count-1 {
|
if i > 0 && i < nodes.count-1 {
|
||||||
setupRelayHandler(nodes[i], nextHops: [nodes[i+1]])
|
setupRelayHandler(nodes[i], nextHops: [nodes[i+1]])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Message dropped due to TTL")
|
await confirmation("Message dropped due to TTL", expectedCount: 0) { receiveMessage in
|
||||||
expectation.isInverted = true // Should NOT be fulfilled
|
david.messageDeliveryHandler = { message in
|
||||||
|
if message.content == TestConstants.testMessage1 {
|
||||||
david.messageDeliveryHandler = { message in
|
receiveMessage() // This should not happen
|
||||||
if message.content == TestConstants.testMessage1 {
|
}
|
||||||
expectation.fulfill() // 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() {
|
@Test func zeroTTLNotRelayed() async {
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(bob, charlie)
|
bob.simulateConnection(with: charlie)
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Zero TTL message not relayed")
|
await confirmation("Zero TTL message not relayed", expectedCount: 0) { receiveMessage in
|
||||||
expectation.isInverted = true
|
charlie.messageDeliveryHandler = { message in
|
||||||
|
if message.content == "Zero TTL message" {
|
||||||
charlie.messageDeliveryHandler = { message in
|
receiveMessage() // Should not happen
|
||||||
if message.content == "Zero TTL message" {
|
}
|
||||||
expectation.fulfill() // 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
|
// MARK: - Duplicate Detection Tests
|
||||||
|
|
||||||
func testDuplicateMessagePrevention() {
|
@Test func duplicateMessagePrevention() async {
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
|
|
||||||
var messageCount = 0
|
var messageCount = 0
|
||||||
let expectation = XCTestExpectation(description: "Only one message received")
|
|
||||||
|
|
||||||
bob.messageDeliveryHandler = { message in
|
await confirmation("Only one message received") { receiveMessage in
|
||||||
if message.content == TestConstants.testMessage1 {
|
bob.messageDeliveryHandler = { message in
|
||||||
messageCount += 1
|
if message.content == TestConstants.testMessage1 {
|
||||||
if messageCount == 1 {
|
receiveMessage()
|
||||||
// Send duplicate after small delay
|
messageCount += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
if messageCount == 1 {
|
||||||
self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil, messageID: message.id)
|
// 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
|
var messageCount = 0
|
||||||
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
|
|
||||||
|
|
||||||
// Wait to ensure duplicate would have been received
|
await confirmation("Only one message received", expectedCount: 2) { receiveMessage in
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
bob.messageDeliveryHandler = { message in
|
||||||
XCTAssertEqual(messageCount, 1)
|
if message.content == TestConstants.testMessage1 {
|
||||||
expectation.fulfill()
|
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
|
// MARK: - Mention Tests
|
||||||
|
|
||||||
func testMessageWithMentions() {
|
@Test func messageWithMentions() async {
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(alice, charlie)
|
alice.simulateConnection(with: charlie)
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Mentioned users receive notification")
|
|
||||||
|
|
||||||
var mentionedUsers: Set<String> = []
|
var mentionedUsers: Set<String> = []
|
||||||
|
|
||||||
bob.messageDeliveryHandler = { message in
|
await confirmation("Mentioned users receive notification", expectedCount: 2) { receiveMention in
|
||||||
if let mentions = message.mentions, mentions.contains(TestConstants.testNickname2) {
|
bob.messageDeliveryHandler = { message in
|
||||||
mentionedUsers.insert(TestConstants.testNickname2)
|
if message.mentions?.contains(TestConstants.testNickname2) == true {
|
||||||
|
mentionedUsers.insert(TestConstants.testNickname2)
|
||||||
|
receiveMention()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
charlie.messageDeliveryHandler = { message in
|
||||||
charlie.messageDeliveryHandler = { message in
|
if message.mentions?.contains(TestConstants.testNickname3) == true {
|
||||||
if let mentions = message.mentions, mentions.contains(TestConstants.testNickname3) {
|
mentionedUsers.insert(TestConstants.testNickname3)
|
||||||
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
|
#expect(mentionedUsers == [TestConstants.testNickname2, TestConstants.testNickname3])
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Network Topology Tests
|
// MARK: - Network Topology Tests
|
||||||
|
|
||||||
func testMeshTopologyBroadcast() {
|
@Test func meshTopologyBroadcast() async {
|
||||||
// Create mesh: Everyone connected to everyone
|
// Create mesh: Everyone connected to everyone
|
||||||
let nodes = [alice!, bob!, charlie!, david!]
|
let nodes = [alice, bob, charlie, david]
|
||||||
for i in 0..<nodes.count {
|
for i in 0..<nodes.count {
|
||||||
for j in i+1..<nodes.count {
|
for j in i+1..<nodes.count {
|
||||||
simulateConnection(nodes[i], nodes[j])
|
nodes[i].simulateConnection(with: nodes[j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var receivedCount = 0
|
await confirmation("All nodes receive message", expectedCount: 3) { receiveMessage in
|
||||||
let expectation = XCTestExpectation(description: "All nodes receive message")
|
for (index, node) in nodes.enumerated() where index > 0 {
|
||||||
|
node.messageDeliveryHandler = { message in
|
||||||
for (index, node) in nodes.enumerated() where index > 0 {
|
if message.content == TestConstants.testMessage1 {
|
||||||
node.messageDeliveryHandler = { message in
|
receiveMessage()
|
||||||
if message.content == TestConstants.testMessage1 {
|
|
||||||
receivedCount += 1
|
|
||||||
if receivedCount == 3 { // Bob, Charlie, David
|
|
||||||
expectation.fulfill()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Partial mesh: Alice -> Bob, Bob -> Charlie, Charlie -> David, David -> Alice
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
simulateConnection(bob, charlie)
|
bob.simulateConnection(with: charlie)
|
||||||
simulateConnection(charlie, david)
|
charlie.simulateConnection(with: david)
|
||||||
simulateConnection(david, alice)
|
david.simulateConnection(with: alice)
|
||||||
|
|
||||||
// Setup relay handlers
|
// Setup relay handlers
|
||||||
setupRelayHandler(bob, nextHops: [charlie])
|
setupRelayHandler(bob, nextHops: [charlie])
|
||||||
setupRelayHandler(charlie, nextHops: [david])
|
setupRelayHandler(charlie, nextHops: [david])
|
||||||
setupRelayHandler(david, nextHops: [alice])
|
setupRelayHandler(david, nextHops: [alice])
|
||||||
let sendDelay2 = DispatchTime.now() + 0.05
|
|
||||||
|
|
||||||
var receivedCount = 0
|
await confirmation("Message reaches all nodes once", expectedCount: 3) { receiveMessage in
|
||||||
let expectation = XCTestExpectation(description: "Message reaches all nodes once")
|
for node in [bob, charlie, david] {
|
||||||
|
node.messageDeliveryHandler = { message in
|
||||||
let checkCompletion = {
|
if message.content == TestConstants.testMessage1 {
|
||||||
if receivedCount == 3 { // Bob, Charlie, David should receive
|
receiveMessage()
|
||||||
expectation.fulfill()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for node in [bob!, charlie!, david!] {
|
|
||||||
node.messageDeliveryHandler = { message in
|
|
||||||
if message.content == TestConstants.testMessage1 {
|
|
||||||
receivedCount += 1
|
|
||||||
checkCompletion()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// MARK: - Performance and Stress Tests
|
||||||
|
|
||||||
func testHighVolumeMessaging() {
|
@Test func highVolumeMessaging() async {
|
||||||
simulateConnection(alice, bob)
|
alice.simulateConnection(with: bob)
|
||||||
|
|
||||||
let messageCount = 100
|
let messageCount = 100
|
||||||
var receivedCount = 0
|
|
||||||
let expectation = XCTestExpectation(description: "All messages received")
|
|
||||||
|
|
||||||
bob.messageDeliveryHandler = { message in
|
await confirmation("All messages received", expectedCount: messageCount) { receiveMessage in
|
||||||
if message.sender == TestConstants.testNickname1 {
|
bob.messageDeliveryHandler = { message in
|
||||||
receivedCount += 1
|
if message.sender == TestConstants.testNickname1 {
|
||||||
if receivedCount == messageCount {
|
receiveMessage()
|
||||||
expectation.fulfill()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Send many messages rapidly
|
||||||
// Send many messages rapidly
|
for i in 0..<messageCount {
|
||||||
for i in 0..<messageCount {
|
alice.sendMessage("Message \(i)", mentions: [], to: nil)
|
||||||
alice.sendMessage("Message \(i)", mentions: [], to: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
wait(for: [expectation], timeout: TestConstants.longTimeout)
|
|
||||||
XCTAssertEqual(receivedCount, messageCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLargeMessageBroadcast() {
|
|
||||||
simulateConnection(alice, bob)
|
|
||||||
|
|
||||||
let expectation = XCTestExpectation(description: "Large message received")
|
|
||||||
|
|
||||||
bob.messageDeliveryHandler = { message in
|
|
||||||
if message.content == TestConstants.testLongMessage {
|
|
||||||
expectation.fulfill()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func largeMessageBroadcast() async {
|
||||||
|
alice.simulateConnection(with: bob)
|
||||||
|
|
||||||
// Send large message
|
await confirmation("Large message received") { receiveLargeMessage in
|
||||||
alice.sendMessage(TestConstants.testLongMessage, mentions: [], to: nil)
|
bob.messageDeliveryHandler = { message in
|
||||||
|
if message.content == TestConstants.testLongMessage {
|
||||||
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
|
receiveLargeMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alice.sendMessage(TestConstants.testLongMessage, mentions: [], to: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func createMockService(peerID: PeerID, nickname: String) -> MockBluetoothMeshService {
|
private func setupRelayHandler(_ node: MockBLEService, nextHops: [MockBLEService]) {
|
||||||
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]) {
|
|
||||||
node.packetDeliveryHandler = { packet in
|
node.packetDeliveryHandler = { packet in
|
||||||
// Check if should relay
|
// Check if should relay
|
||||||
guard packet.ttl > 1 else { return }
|
guard packet.ttl > 1 else { return }
|
||||||
|
|
||||||
if let message = BitchatMessage(packet.payload) {
|
if let message = BitchatMessage(packet.payload) {
|
||||||
// Don't relay own messages
|
// Don't relay own messages
|
||||||
guard message.senderPeerID != node.peerID else { return }
|
guard message.senderPeerID?.id != node.peerID else { return }
|
||||||
|
|
||||||
// Create relay message
|
// Create relay message
|
||||||
let relayMessage = BitchatMessage(
|
let relayMessage = BitchatMessage(
|
||||||
|
|||||||
@@ -6,26 +6,125 @@
|
|||||||
// For more information, see <https://unlicense.org>
|
// For more information, see <https://unlicense.org>
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import Testing
|
||||||
|
import Foundation
|
||||||
@testable import bitchat
|
@testable import bitchat
|
||||||
|
|
||||||
final class FragmentationTests: XCTestCase {
|
struct FragmentationTests {
|
||||||
|
|
||||||
private var mockKeychain: MockKeychain!
|
private let mockKeychain: MockKeychain
|
||||||
private var mockIdentityManager: MockIdentityManager!
|
private let mockIdentityManager: MockIdentityManager
|
||||||
|
|
||||||
override func setUp() {
|
init() {
|
||||||
super.setUp()
|
|
||||||
mockKeychain = MockKeychain()
|
mockKeychain = MockKeychain()
|
||||||
mockIdentityManager = MockIdentityManager(mockKeychain)
|
mockIdentityManager = MockIdentityManager(mockKeychain)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
@Test("Reassembly from fragments delivers a public message")
|
||||||
mockKeychain = nil
|
func reassemblyFromFragmentsDeliversPublicMessage() async throws {
|
||||||
mockIdentityManager = nil
|
let ble = BLEService(keychain: mockKeychain, identityManager: mockIdentityManager)
|
||||||
super.tearDown()
|
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 {
|
private final class CaptureDelegate: BitchatDelegate {
|
||||||
var publicMessages: [(peerID: String, nickname: String, content: String)] = []
|
var publicMessages: [(peerID: String, nickname: String, content: String)] = []
|
||||||
func didReceiveMessage(_ message: BitchatMessage) {}
|
func didReceiveMessage(_ message: BitchatMessage) {}
|
||||||
@@ -88,101 +187,4 @@ final class FragmentationTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
return packets
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,3 +349,18 @@ final class MockBLEService: NSObject {
|
|||||||
|
|
||||||
// Backward compatibility for older tests
|
// Backward compatibility for older tests
|
||||||
typealias MockSimplifiedBluetoothService = MockBLEService
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user