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:
@@ -6,26 +6,125 @@
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user