Feature/fragmentation fixes (#453)

* Fix fragmentation + BLE long-write + padding

- Accumulate CBATTRequest long writes by offset and decode once per central
- Decode original packet after fragment reassembly (preserve flags/compression)
- Switch MessagePadding to strict PKCS#7 and validate before unpadding
- Make BinaryProtocol.decode robust: try raw first, then unpad fallback
- Unpad frames before BLE notify; fragment when exceeding centrals' max update length
- Skip notify path when max update length < 21 bytes (protocol minimum)

Verified large PMs and announces route without decode errors and peers show reliably.

* Tests: fix weak delegate lifetime, legacy constants, and unused vars; fragment unpadded frames

- BLEServiceTests: hold strong reference to MockBitchatDelegate
- IntegrationTests: fix inline comment braces; replace removed types with test-safe values; use numeric 0x06 for legacy handshake resp checks
- BinaryProtocolTests: remove unused minResult variable
- BLEService: fragment the unpadded frame so fragments are efficient

* tests: add Noise rehandshake recovery test; document in-memory test bus and autoFlood; stabilize large-network broadcast

* tests: align suite with current behavior (compression, padding, routing, nonce); deflake and stabilize

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2025-08-19 01:22:21 +02:00
committed by GitHub
co-authored by jack
parent 4c0bb5f93a
commit a30b73dd99
14 changed files with 741 additions and 517 deletions
+13 -58
View File
@@ -18,6 +18,7 @@ final class PrivateChatE2ETests: XCTestCase {
override func setUp() {
super.setUp()
MockBLEService.resetTestBus()
// Create services
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
@@ -80,12 +81,15 @@ final class PrivateChatE2ETests: XCTestCase {
}
}
// Alice sends private message to Bob only
alice.sendPrivateMessage(
TestConstants.testMessage1,
to: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
// 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,
to: TestConstants.testPeerID2,
recipientNickname: TestConstants.testNickname2
)
}
wait(for: [bobExpectation, charlieExpectation], timeout: TestConstants.shortTimeout)
}
@@ -270,58 +274,8 @@ final class PrivateChatE2ETests: XCTestCase {
// NOTE: This test relied on MessageRetryService which has been removed
func testDuplicateAckPrevention() {
simulateConnection(alice, bob)
let messageID = UUID().uuidString
var ackCount = 0
alice.packetDeliveryHandler = { packet in
if packet.type == 0x03 {
ackCount += 1
}
}
// Create message
let message = BitchatMessage(
id: messageID,
sender: TestConstants.testNickname1,
content: TestConstants.testMessage1,
timestamp: Date(),
isRelay: false,
originalSender: nil,
isPrivate: true,
recipientNickname: TestConstants.testNickname2,
senderPeerID: TestConstants.testPeerID1,
mentions: nil
)
// Generate multiple ACKs for same message
// NOTE: DeliveryTracker has been removed - this test is no longer applicable
/*
for _ in 0..<3 {
if let ack = deliveryTracker.generateAck(
for: message,
myPeerID: TestConstants.testPeerID2,
myNickname: TestConstants.testNickname2,
hopCount: 1
) {
let ackData = ack.encode()!
let ackPacket = TestHelpers.createTestPacket(
type: 0x03,
senderID: TestConstants.testPeerID2,
recipientID: TestConstants.testPeerID1,
payload: ackData
)
alice.simulateIncomingPacket(ackPacket)
}
}
*/
// Should only generate one ACK
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
XCTAssertEqual(ackCount, 1)
}
func testDuplicateAckPrevention() throws {
throw XCTSkip("DeliveryTracker/ACK flow removed; test not applicable")
}
// MARK: - Helper Methods
@@ -330,6 +284,7 @@ final class PrivateChatE2ETests: XCTestCase {
let service = MockBluetoothMeshService()
service.myPeerID = peerID
service.mockNickname = nickname
service._testRegister()
return service
}
+21 -8
View File
@@ -20,6 +20,7 @@ final class PublicChatE2ETests: XCTestCase {
override func setUp() {
super.setUp()
MockBLEService.resetTestBus()
// Create mock services
alice = createMockService(peerID: TestConstants.testPeerID1, nickname: TestConstants.testNickname1)
@@ -156,6 +157,8 @@ final class PublicChatE2ETests: XCTestCase {
// 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 &&
@@ -166,7 +169,9 @@ final class PublicChatE2ETests: XCTestCase {
}
// Alice sends message
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
DispatchQueue.main.asyncAfter(deadline: sendDelay) {
self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
}
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
}
@@ -194,8 +199,12 @@ final class PublicChatE2ETests: XCTestCase {
}
}
// Send message with TTL=2 (should reach Charlie but not David)
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
// 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: alice.peerID)
if let payload = msg.toBinaryPayload() {
let pkt = TestHelpers.createTestPacket(senderID: alice.peerID, payload: payload, ttl: 2)
bob.simulateIncomingPacket(pkt)
}
wait(for: [expectation], timeout: TestConstants.shortTimeout)
}
@@ -336,12 +345,13 @@ final class PublicChatE2ETests: XCTestCase {
setupRelayHandler(bob, nextHops: [charlie])
setupRelayHandler(charlie, nextHops: [david])
setupRelayHandler(david, nextHops: [alice])
let sendDelay2 = DispatchTime.now() + 0.05
var messageIDs = Set<String>()
var receivedCount = 0
let expectation = XCTestExpectation(description: "Message reaches all nodes once")
let checkCompletion = {
if messageIDs.count == 3 { // Bob, Charlie, David should receive
if receivedCount == 3 { // Bob, Charlie, David should receive
expectation.fulfill()
}
}
@@ -349,14 +359,16 @@ final class PublicChatE2ETests: XCTestCase {
for node in [bob!, charlie!, david!] {
node.messageDeliveryHandler = { message in
if message.content == TestConstants.testMessage1 {
messageIDs.insert(message.id)
receivedCount += 1
checkCompletion()
}
}
}
// Alice broadcasts
alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
DispatchQueue.main.asyncAfter(deadline: sendDelay2) {
self.alice.sendMessage(TestConstants.testMessage1, mentions: [], to: nil)
}
wait(for: [expectation], timeout: TestConstants.defaultTimeout)
}
@@ -411,6 +423,7 @@ final class PublicChatE2ETests: XCTestCase {
let service = MockBluetoothMeshService()
service.myPeerID = peerID
service.mockNickname = nickname
service._testRegister()
return service
}
@@ -461,4 +474,4 @@ final class PublicChatE2ETests: XCTestCase {
}
}
}
}
}