mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
[codex] Refactor BLE transport event handling (#1266)
* Refactor BLE transport event handling * Make image output paths unique * Keep queued Nostr read receipts alive * Allow self-authored RSR ingress replies --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -92,6 +92,77 @@ struct BLEServiceCoreTests {
|
||||
_ = await TestHelpers.waitUntil({ !ble.currentPeerSnapshots().isEmpty }, timeout: 0.3)
|
||||
#expect(ble.currentPeerSnapshots().isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func ingressAllowsRelayedSenderOnBoundLink() async throws {
|
||||
let ble = makeService()
|
||||
let boundPeer = PeerID(str: "1122334455667788")
|
||||
let relayedSender = PeerID(str: "8899aabbccddeeff")
|
||||
let packet = makePublicPacket(
|
||||
content: "Relayed",
|
||||
sender: relayedSender,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
)
|
||||
|
||||
#expect(ble._test_acceptsIngress(packet: packet, boundPeerID: boundPeer))
|
||||
}
|
||||
|
||||
@Test
|
||||
func ingressRejectsDirectAnnounceThatConflictsWithBoundLink() async throws {
|
||||
let ble = makeService()
|
||||
let boundPeer = PeerID(str: "1122334455667788")
|
||||
let claimedPeer = PeerID(str: "8899aabbccddeeff")
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.announce.rawValue,
|
||||
senderID: Data(hexString: claimedPeer.id) ?? Data(),
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: Data(),
|
||||
signature: nil,
|
||||
ttl: 7
|
||||
)
|
||||
|
||||
#expect(!ble._test_acceptsIngress(packet: packet, boundPeerID: boundPeer))
|
||||
}
|
||||
|
||||
@Test
|
||||
func ingressRejectsSelfLoopbackBeforeSpoofChecks() async throws {
|
||||
let ble = makeService()
|
||||
let packet = makePublicPacket(
|
||||
content: "Loopback",
|
||||
sender: ble.myPeerID,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
)
|
||||
|
||||
#expect(!ble._test_acceptsIngress(packet: packet, boundPeerID: PeerID(str: "1122334455667788")))
|
||||
}
|
||||
|
||||
@Test
|
||||
func ingressAllowsSelfAuthoredRSRWithTTLZeroFromBoundPeer() async throws {
|
||||
let ble = makeService()
|
||||
var packet = makePublicPacket(
|
||||
content: "Recovered by sync",
|
||||
sender: ble.myPeerID,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
)
|
||||
packet.isRSR = true
|
||||
packet.ttl = 0
|
||||
|
||||
#expect(ble._test_acceptsIngress(packet: packet, boundPeerID: PeerID(str: "1122334455667788")))
|
||||
}
|
||||
|
||||
@Test
|
||||
func ingressRecordSuppressesSecondLinkDuplicate() async throws {
|
||||
let ble = makeService()
|
||||
let packet = makePublicPacket(
|
||||
content: "Duplicate link copy",
|
||||
sender: PeerID(str: "1122334455667788"),
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000)
|
||||
)
|
||||
|
||||
#expect(ble._test_recordIngressIfNew(packet: packet, linkID: "central-a"))
|
||||
#expect(!ble._test_recordIngressIfNew(packet: packet, linkID: "central-b"))
|
||||
}
|
||||
}
|
||||
|
||||
private func makeService() -> BLEService {
|
||||
|
||||
@@ -42,6 +42,7 @@ struct ChatViewModelInitializationTests {
|
||||
|
||||
// The viewModel should set itself as the transport delegate
|
||||
#expect(transport.delegate === viewModel)
|
||||
#expect(transport.eventDelegate === viewModel)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
@@ -421,6 +422,7 @@ struct ChatViewModelReceivingTests {
|
||||
// Message may or may not appear due to rate limiting/pipeline batching
|
||||
// The important thing is no crash and delegate was called
|
||||
#expect(transport.delegate === viewModel)
|
||||
#expect(transport.eventDelegate === viewModel)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
|
||||
@@ -64,4 +64,19 @@ struct ImageUtilsTests {
|
||||
#expect(data.starts(with: Data([0xFF, 0xD8])))
|
||||
#expect(data.count > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func processImage_usesUniqueOutputURLs() throws {
|
||||
let image = makePlatformImage(size: CGSize(width: 64, height: 64))
|
||||
let firstURL = try ImageUtils.processImage(image, maxDimension: 64)
|
||||
let secondURL = try ImageUtils.processImage(image, maxDimension: 64)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: firstURL)
|
||||
try? FileManager.default.removeItem(at: secondURL)
|
||||
}
|
||||
|
||||
#expect(firstURL != secondURL)
|
||||
#expect(FileManager.default.fileExists(atPath: firstURL.path))
|
||||
#expect(FileManager.default.fileExists(atPath: secondURL.path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,12 +195,39 @@ struct GossipSyncManagerTests {
|
||||
manager._performMaintenanceSynchronously(now: Date())
|
||||
|
||||
let sentPackets = delegate.packets
|
||||
#expect(sentPackets.count == 3)
|
||||
#expect(sentPackets.count == 1)
|
||||
let decoded = sentPackets.compactMap { RequestSyncPacket.decode(from: $0.payload) }
|
||||
#expect(decoded.count == 3)
|
||||
#expect(decoded[0].types == .publicMessages)
|
||||
#expect(decoded[1].types == .fragment)
|
||||
#expect(decoded[2].types == .fileTransfer)
|
||||
#expect(decoded.count == 1)
|
||||
let types = try #require(decoded.first?.types)
|
||||
#expect(types.contains(.announce))
|
||||
#expect(types.contains(.message))
|
||||
#expect(types.contains(.fragment))
|
||||
#expect(types.contains(.fileTransfer))
|
||||
}
|
||||
|
||||
@Test func initialSyncCoalescesEnabledTypes() async throws {
|
||||
var config = GossipSyncManager.Config()
|
||||
config.seenCapacity = 10
|
||||
config.fragmentCapacity = 5
|
||||
config.fileTransferCapacity = 4
|
||||
config.fragmentSyncIntervalSeconds = 1
|
||||
config.fileTransferSyncIntervalSeconds = 1
|
||||
|
||||
let requestSyncManager = RequestSyncManager()
|
||||
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
|
||||
let delegate = RecordingDelegate()
|
||||
manager.delegate = delegate
|
||||
|
||||
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
|
||||
|
||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||
let packet = try #require(delegate.packets.first)
|
||||
let request = try #require(RequestSyncPacket.decode(from: packet.payload))
|
||||
let types = try #require(request.types)
|
||||
#expect(types.contains(.announce))
|
||||
#expect(types.contains(.message))
|
||||
#expect(types.contains(.fragment))
|
||||
#expect(types.contains(.fileTransfer))
|
||||
}
|
||||
|
||||
@Test func handleRequestSyncHonorsTypeFilter() async throws {
|
||||
|
||||
@@ -19,6 +19,7 @@ final class MockTransport: Transport {
|
||||
// MARK: - Protocol Properties
|
||||
|
||||
weak var delegate: BitchatDelegate?
|
||||
weak var eventDelegate: TransportEventDelegate?
|
||||
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||
|
||||
var myPeerID: PeerID = PeerID(str: "TESTPEER")
|
||||
|
||||
@@ -97,6 +97,27 @@ struct NotificationStreamAssemblerTests {
|
||||
#expect(decoded.timestamp == packet.timestamp)
|
||||
}
|
||||
|
||||
@Test func discardsPKCSPaddingBetweenNotificationFrames() throws {
|
||||
var assembler = NotificationStreamAssembler()
|
||||
let packet1 = makePacket(timestamp: 0x111)
|
||||
let packet2 = makePacket(timestamp: 0x222)
|
||||
let paddedFrame1 = try #require(packet1.toBinaryData(padding: true), "Failed to encode first padded packet")
|
||||
let paddedFrame2 = try #require(packet2.toBinaryData(padding: true), "Failed to encode second padded packet")
|
||||
|
||||
var result = assembler.append(paddedFrame1)
|
||||
#expect(result.frames.count == 1)
|
||||
#expect(result.droppedPrefixes.isEmpty)
|
||||
#expect(result.reset == false)
|
||||
|
||||
result = assembler.append(paddedFrame2)
|
||||
#expect(result.frames.count == 1)
|
||||
#expect(result.droppedPrefixes.isEmpty)
|
||||
#expect(result.reset == false)
|
||||
|
||||
let decoded = try #require(BinaryProtocol.decode(result.frames[0]), "Failed to decode second frame")
|
||||
#expect(decoded.timestamp == packet2.timestamp)
|
||||
}
|
||||
|
||||
func testAssemblesCompressedLargeFrame() throws {
|
||||
var assembler = NotificationStreamAssembler()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ private final class DefaultDelegateProbe: BitchatDelegate {
|
||||
|
||||
private final class DefaultTransportProbe: Transport {
|
||||
weak var delegate: BitchatDelegate?
|
||||
weak var eventDelegate: TransportEventDelegate?
|
||||
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||
|
||||
let subject = CurrentValueSubject<[TransportPeerSnapshot], Never>([])
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct BLEOutboundWriteBufferTests {
|
||||
@Test
|
||||
func enqueueOrdersWritesByPriority() {
|
||||
var buffer = BLEOutboundWriteBuffer()
|
||||
let peerID = "peer-1"
|
||||
|
||||
_ = buffer.enqueue(
|
||||
data: Data(repeating: 0x01, count: 4),
|
||||
for: peerID,
|
||||
priority: .fileTransfer,
|
||||
capBytes: 64
|
||||
)
|
||||
_ = buffer.enqueue(
|
||||
data: Data(repeating: 0x02, count: 4),
|
||||
for: peerID,
|
||||
priority: .high,
|
||||
capBytes: 64
|
||||
)
|
||||
_ = buffer.enqueue(
|
||||
data: Data(repeating: 0x03, count: 4),
|
||||
for: peerID,
|
||||
priority: .fragment(totalFragments: 2),
|
||||
capBytes: 64
|
||||
)
|
||||
|
||||
let writes = buffer.takeAll(for: peerID)
|
||||
|
||||
#expect(writes.map { Int($0.data.first ?? 0) } == [0x02, 0x03, 0x01])
|
||||
}
|
||||
|
||||
@Test
|
||||
func enqueueTrimsLowestPriorityItemsToCap() {
|
||||
var buffer = BLEOutboundWriteBuffer()
|
||||
let peerID = "peer-1"
|
||||
|
||||
_ = buffer.enqueue(data: Data(repeating: 0x01, count: 8), for: peerID, priority: .low, capBytes: 16)
|
||||
_ = buffer.enqueue(data: Data(repeating: 0x02, count: 8), for: peerID, priority: .fileTransfer, capBytes: 16)
|
||||
let result = buffer.enqueue(data: Data(repeating: 0x03, count: 8), for: peerID, priority: .high, capBytes: 16)
|
||||
|
||||
if case let .enqueued(trimmedBytes, remainingBytes) = result {
|
||||
#expect(trimmedBytes == 8)
|
||||
#expect(remainingBytes == 16)
|
||||
} else {
|
||||
Issue.record("Expected buffered write to trim, not drop as oversized")
|
||||
}
|
||||
|
||||
let writes = buffer.takeAll(for: peerID)
|
||||
|
||||
#expect(writes.map { Int($0.data.first ?? 0) } == [0x03, 0x02])
|
||||
}
|
||||
|
||||
@Test
|
||||
func enqueueRejectsOversizedSingleChunk() {
|
||||
var buffer = BLEOutboundWriteBuffer()
|
||||
|
||||
let result = buffer.enqueue(
|
||||
data: Data(repeating: 0x01, count: 32),
|
||||
for: "peer-1",
|
||||
priority: .high,
|
||||
capBytes: 16
|
||||
)
|
||||
|
||||
if case let .oversized(bytes) = result {
|
||||
#expect(bytes == 32)
|
||||
} else {
|
||||
Issue.record("Expected oversized write to be rejected")
|
||||
}
|
||||
|
||||
#expect(buffer.peripheralIDs.isEmpty)
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,7 @@ struct NostrTransportTests {
|
||||
let secondPayload = try decodeEmbeddedPayload(from: secondEvent, recipient: recipient).payload
|
||||
#expect(secondPayload.type == .readReceipt)
|
||||
#expect(String(data: secondPayload.data, encoding: .utf8) == "read-2")
|
||||
withExtendedLifetime(transport) {}
|
||||
}
|
||||
|
||||
@Test("Concurrent read receipt enqueue does not crash")
|
||||
|
||||
Reference in New Issue
Block a user