import BitFoundation import Foundation import Testing @testable import bitchat struct BLEFileTransferHandlerTests { private final class Recorder { var localNickname = "Me" var peers: [PeerID: BLEPeerInfo] = [:] var signedName: String? var signatureVerifies = false var saveResult: URL? = URL(fileURLWithPath: "/tmp/files/incoming/sample.pdf") var signatureVerifyCount = 0 var signedNameQueries: [PeerID] = [] var blockedPeers: Set = [] var trackedPackets: [BitchatPacket] = [] var quotaReservations: [Int] = [] var saveCalls: [(data: Data, preferredName: String?, subdirectory: String, fallbackExtension: String?, defaultPrefix: String)] = [] var receiptStates: [String: BLEPrivateMediaReceiptState] = [:] var receiptCommits: [(messageID: String, storedURL: URL)] = [] var receiptCommitSucceeds = true var removedIncomingFiles: [URL] = [] var finishedIncomingFileDeliveries: [URL] = [] var lastSeenUpdates: [PeerID] = [] var deliveryAcks: [(messageID: String, peerID: PeerID)] = [] var deliveredMessages: [BitchatMessage] = [] var shouldAcceptDelivery = true var deliveryOutcome = TransportEventDeliveryOutcome.accepted var saveOverride: (( _ data: Data, _ preferredName: String?, _ subdirectory: String, _ fallbackExtension: String?, _ defaultPrefix: String ) -> URL?)? var receiptStateOverride: ((String) -> BLEPrivateMediaReceiptState)? var receiptCommitOverride: ((String, URL) -> Bool)? var removeIncomingFileOverride: ((URL) -> Void)? var finishIncomingFileDeliveryOverride: ((URL) -> Void)? } private let localPeerID = PeerID(str: "0102030405060708") private let remotePeerID = PeerID(str: "1122334455667788") private let sampleSigningKey = Data(repeating: 0xAB, count: 32) @Test @MainActor func deliveryGateFinalizesInitialRejection() { var completions = 0 var finalizations = 0 TransportEventDeliveryGate.attempt( shouldDeliver: { false }, deliver: { Issue.record("delivery sink must not run") return .accepted }, completion: { completions += 1 }, finalization: { _ in finalizations += 1 } ) #expect(completions == 0) #expect(finalizations == 1) } @Test @MainActor func deliveryGateFinalizesMissingOrRejectingSink() { var completions = 0 var finalizations = 0 TransportEventDeliveryGate.attempt( shouldDeliver: { true }, deliver: { .rejected }, completion: { completions += 1 }, finalization: { _ in finalizations += 1 } ) #expect(completions == 0) #expect(finalizations == 1) } @Test @MainActor func deliveryGateFinalizesPostInsertionRejection() { var deliveryChecks = 0 var completions = 0 var finalizations = 0 TransportEventDeliveryGate.attempt( shouldDeliver: { deliveryChecks += 1 return deliveryChecks == 1 }, deliver: { .accepted }, completion: { completions += 1 }, finalization: { _ in finalizations += 1 } ) #expect(deliveryChecks == 2) #expect(completions == 0) #expect(finalizations == 1) } @Test @MainActor func deliveryGatePreservesInvokedUnconfirmedOutcomeWithoutAck() { var completions = 0 var outcomes: [TransportEventDeliveryOutcome] = [] TransportEventDeliveryGate.attempt( shouldDeliver: { true }, deliver: { .invokedUnconfirmed }, completion: { completions += 1 }, finalization: { outcomes.append($0) } ) #expect(completions == 0) #expect(outcomes == [.invokedUnconfirmed]) } private func makeHandler(recorder: Recorder) -> BLEFileTransferHandler { let environment = BLEFileTransferHandlerEnvironment( localPeerID: { [localPeerID] in localPeerID }, localNickname: { recorder.localNickname }, peersSnapshot: { recorder.peers }, verifyPacketSignature: { _, _ in recorder.signatureVerifyCount += 1 return recorder.signatureVerifies }, localSigningPublicKey: { [sampleSigningKey] in sampleSigningKey }, signedSenderDisplayName: { _, peerID in recorder.signedNameQueries.append(peerID) return recorder.signedName }, trackPacketSeen: { packet in recorder.trackedPackets.append(packet) }, enforceStorageQuota: { reservingBytes in recorder.quotaReservations.append(reservingBytes) }, saveIncomingFile: { data, preferredName, subdirectory, fallbackExtension, defaultPrefix in recorder.saveCalls.append((data, preferredName, subdirectory, fallbackExtension, defaultPrefix)) if let saveOverride = recorder.saveOverride { return saveOverride(data, preferredName, subdirectory, fallbackExtension, defaultPrefix) } return recorder.saveResult }, privateMediaReceiptState: { messageID in if let receiptStateOverride = recorder.receiptStateOverride { return receiptStateOverride(messageID) } return recorder.receiptStates[messageID] ?? .absent }, commitPrivateMediaFile: { messageID, storedURL in recorder.receiptCommits.append((messageID, storedURL)) if let receiptCommitOverride = recorder.receiptCommitOverride { return receiptCommitOverride(messageID, storedURL) } guard recorder.receiptCommitSucceeds else { return false } recorder.receiptStates[messageID] = .accepted(storedURL) return true }, removeIncomingFile: { storedURL in recorder.removedIncomingFiles.append(storedURL) recorder.removeIncomingFileOverride?(storedURL) }, finishIncomingFileDelivery: { storedURL in recorder.finishedIncomingFileDeliveries.append(storedURL) recorder.finishIncomingFileDeliveryOverride?(storedURL) }, isPrivateMediaSenderBlocked: { peerID in recorder.blockedPeers.contains(peerID) }, updatePeerLastSeen: { peerID in recorder.lastSeenUpdates.append(peerID) }, acknowledgePrivateMedia: { messageID, peerID in recorder.deliveryAcks.append((messageID, peerID)) }, deliverMessage: { message, shouldDeliver, completion, finalization in var outcome = TransportEventDeliveryOutcome.rejected defer { finalization(outcome) } guard recorder.shouldAcceptDelivery else { return } guard shouldDeliver() else { return } recorder.deliveredMessages.append(message) guard shouldDeliver() else { return } if recorder.deliveryOutcome == .invokedUnconfirmed { outcome = .invokedUnconfirmed return } outcome = .accepted completion() } ) return BLEFileTransferHandler(environment: environment) } @Test func broadcastFileFromVerifiedPeerIsSavedAndDelivered() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey)] recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let content = Data("%PDF-1.7".utf8) let packet = try makeFileTransferPacket(sender: remotePeerID, mimeType: "application/pdf", content: content) #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.signatureVerifyCount == 1) #expect(recorder.signedNameQueries.isEmpty) #expect(recorder.trackedPackets.count == 1) #expect(recorder.quotaReservations == [content.count]) #expect(recorder.saveCalls.count == 1) #expect(recorder.saveCalls.first?.data == content) #expect(recorder.saveCalls.first?.preferredName == "sample") #expect(recorder.saveCalls.first?.subdirectory == "files/incoming") #expect(recorder.saveCalls.first?.fallbackExtension == "pdf") #expect(recorder.saveCalls.first?.defaultPrefix == "file") #expect(recorder.lastSeenUpdates.isEmpty) #expect(recorder.deliveredMessages.count == 1) let message = recorder.deliveredMessages.first #expect(message?.sender == "Alice") #expect(message?.content == "[file] sample.pdf") #expect(message?.isPrivate == false) #expect(message?.senderPeerID == remotePeerID) #expect(message?.timestamp == Date(timeIntervalSince1970: 900)) #expect(message?.deliveryStatus == nil) } @Test func selfEchoIsDropped() throws { let recorder = Recorder() recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket(sender: localPeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8), ttl: 3) #expect(!handler.handle(packet, from: localPeerID)) expectNoSideEffects(recorder) } @Test func unknownPeerWithoutValidSignatureIsDropped() throws { let recorder = Recorder() let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket(sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8)) #expect(!handler.handle(packet, from: remotePeerID)) #expect(recorder.signedNameQueries == [remotePeerID]) #expect(recorder.trackedPackets.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func broadcastFromConnectedUnverifiedPeerWithoutSignatureIsDropped() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Bob", isVerified: false, isConnected: true)] let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8), hasSignature: false ) // Failed sender authentication must also stop the packet from being // relayed to downstream nodes. #expect(!handler.handle(packet, from: remotePeerID)) // Broadcast files carry an attacker-controllable senderID, so — like // public messages — a connected-but-unverified peer must present a valid // packet signature. No signing key + no signed identity means dropped. #expect(recorder.signedNameQueries.isEmpty) #expect(recorder.trackedPackets.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func broadcastFromConnectedUnverifiedPeerWithSignedIdentityIsAccepted() throws { let recorder = Recorder() // Connected but nickname not yet verified and no registry signing key — // the persisted-identity signature lookup still authenticates the // sender, so the transfer is accepted under that verified name. recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Bob", isVerified: false, isConnected: true)] recorder.signedName = "Bob" let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket(sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8)) #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.signedNameQueries == [remotePeerID]) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveredMessages.first?.sender == "Bob") } @Test func signedSelfBroadcastReplayIsDelivered() throws { // Our own broadcast file replayed via gossip sync arrives with ttl==0; // it is verified against our local signing key before delivery. let recorder = Recorder() recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: localPeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8), ttl: 0 ) #expect(handler.handle(packet, from: localPeerID)) #expect(recorder.signatureVerifyCount == 1) #expect(recorder.signedNameQueries.isEmpty) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveredMessages.first?.sender == "Me") } @Test func broadcastFromPeerNotInRegistryAcceptedViaSignedIdentity() throws { let recorder = Recorder() recorder.signedName = "Carol" let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket(sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8)) #expect(handler.handle(packet, from: remotePeerID)) // Peer absent from the registry: fall back to the persisted-identity // signature lookup (mirrors BLEPublicMessageHandler). #expect(recorder.signedNameQueries == [remotePeerID]) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveredMessages.first?.sender == "Carol") } @Test func spoofedBroadcastVoiceNoteWithoutSignatureIsDropped() throws { // Regression for the PR #1406 finding: an in-range peer that observed a // public voice burst tries to overwrite the live bubble by broadcasting // a `voice_.m4a` note under the talker's senderID. Without a // valid signature the note never reaches the coordinator's absorption. let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Mallory", isVerified: false, isConnected: true)] let handler = makeHandler(recorder: recorder) let m4a = Data([0x00, 0x00, 0x00, 0x18]) + Data("ftypM4A ".utf8) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "audio/mp4", content: m4a, fileName: "voice_1122334455667788", hasSignature: false ) // The spoofed note must be dropped locally AND not relayed onward. #expect(!handler.handle(packet, from: remotePeerID)) #expect(recorder.deliveredMessages.isEmpty) } @Test func rawDirectedFileWithoutVerifiableSignatureIsDroppedWithoutWriteOrRelay() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Bob", isVerified: false, isConnected: true)] let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8), recipientID: Data(hexString: localPeerID.id), hasSignature: false ) #expect(!handler.handle(packet, from: remotePeerID)) #expect(recorder.signatureVerifyCount == 0) #expect(recorder.signedNameQueries.isEmpty) #expect(recorder.trackedPackets.isEmpty) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func fileDirectedToAnotherPeerIsIgnored() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey)] recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8), recipientID: Data(hexString: "AABBCCDDEEFF0011") ) // Not for us, but it must keep relaying toward the real recipient. #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.trackedPackets.isEmpty) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func privateFileUpdatesLastSeenAndDeliversPrivateMessage() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey)] recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8), recipientID: Data(hexString: localPeerID.id) ) #expect(handler.handle(packet, from: remotePeerID)) // Directed transfers are not tracked for gossip sync. #expect(recorder.trackedPackets.isEmpty) #expect(recorder.lastSeenUpdates == [remotePeerID]) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveredMessages.first?.isPrivate == true) #expect(recorder.deliveredMessages.first?.id.hasPrefix("media-") == false) // Must be explicit: BitchatMessage defaults private messages to // .sending, which the media views render as an in-flight send // (empty reveal mask, disabled reveal tap). #expect(recorder.deliveredMessages.first?.deliveryStatus == .delivered(to: "Me", at: Date(timeIntervalSince1970: 900))) } @Test func bit8EncryptedPrivateFileKeepsStableIDAndAckWithoutBit9Proof() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)] let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF]) + Data(repeating: 0x41, count: 128) let fileName = "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg" let file = BitchatFilePacket( fileName: fileName, fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) let timestamp = Date(timeIntervalSince1970: 1_234) #expect(handler.handlePrivatePayload(payload, from: remotePeerID, timestamp: timestamp)) #expect(recorder.signatureVerifyCount == 0) #expect(recorder.signedNameQueries.isEmpty) #expect(recorder.trackedPackets.isEmpty) #expect(recorder.quotaReservations == [content.count]) #expect(recorder.saveCalls.first?.data == content) #expect(recorder.lastSeenUpdates == [remotePeerID]) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveredMessages.first?.isPrivate == true) #expect(recorder.deliveredMessages.first?.timestamp == timestamp) #expect(recorder.deliveredMessages.first?.id == PrivateMediaMessageIdentity.stableID( senderPeerID: remotePeerID, recipientPeerID: localPeerID, fileName: fileName )) #expect(recorder.receiptCommits.count == 1) #expect(recorder.deliveryAcks.count == 1) #expect(recorder.deliveryAcks.first?.messageID == recorder.deliveredMessages.first?.id) } @Test func rejectedStableDeliveryReleasesPendingPayloadOwnership() throws { let root = FileManager.default.temporaryDirectory.appendingPathComponent( "private-media-handler-rejected-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: root) } let store = BLEIncomingFileStore(baseDirectory: root) let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true )] recorder.shouldAcceptDelivery = false recorder.saveOverride = { store.save( data: $0, preferredName: $1, subdirectory: $2, fallbackExtension: $3, defaultPrefix: $4 ) } recorder.receiptStateOverride = { store.privateMediaReceiptState(messageID: $0) } recorder.receiptCommitOverride = { store.commitPrivateMediaFile(messageID: $0, storedURL: $1) } recorder.removeIncomingFileOverride = { store.removeIncomingFile(at: $0) } recorder.finishIncomingFileDeliveryOverride = { store.finishIncomingFileDelivery(at: $0) } let content = Data([0xFF, 0xD8, 0xFF, 0xD9]) let fileName = "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg" let file = BitchatFilePacket( fileName: fileName, fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) let stableID = try #require(PrivateMediaMessageIdentity.stableID( senderPeerID: remotePeerID, recipientPeerID: localPeerID, fileName: fileName )) #expect(makeHandler(recorder: recorder).handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(recorder.deliveredMessages.isEmpty) #expect(recorder.deliveryAcks.isEmpty) #expect(recorder.finishedIncomingFileDeliveries.count == 1) #expect(store.reservePrivateMediaDeletion( messageIDs: [stableID], payloadRelativePaths: [:] ) != nil) } @Test func rawLegacyPrivateFileWithRetryShapedNameNeverUsesReceiptLedger() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey )] recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF, 0xD9]) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "image/jpeg", content: content, recipientID: Data(hexString: localPeerID.id), fileName: "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg" ) #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.receiptCommits.isEmpty) #expect(recorder.deliveryAcks.isEmpty) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveredMessages.first?.id.hasPrefix("media-") == false) } @Test func rejectedRawDeliveryRemovesUIUnownedPayload() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey )] recorder.signatureVerifies = true recorder.shouldAcceptDelivery = false let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "image/jpeg", content: Data([0xFF, 0xD8, 0xFF, 0xD9]), recipientID: Data(hexString: localPeerID.id), fileName: "raw-rejected.jpg" ) #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.deliveredMessages.isEmpty) #expect(recorder.removedIncomingFiles.count == 1) #expect(recorder.removedIncomingFiles.first == recorder.saveResult) #expect(recorder.finishedIncomingFileDeliveries.isEmpty) } @Test func plainDelegateRawDeliveryPreservesPayloadWithoutSynchronousAck() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey )] recorder.signatureVerifies = true recorder.deliveryOutcome = .invokedUnconfirmed let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket( sender: remotePeerID, mimeType: "image/jpeg", content: Data([0xFF, 0xD8, 0xFF, 0xD9]), recipientID: Data(hexString: localPeerID.id), fileName: "plain-delegate.jpg" ) #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveryAcks.isEmpty) #expect(recorder.removedIncomingFiles.isEmpty) #expect(recorder.finishedIncomingFileDeliveries.count == 1) #expect( recorder.finishedIncomingFileDeliveries.first == recorder.saveResult ) } @Test func repeatedLegacyPrivateImageNamesKeepDistinctRandomMessageIDs() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)] let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF]) + Data(repeating: 0x41, count: 128) let file = BitchatFilePacket( fileName: "photo.jpg", fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_235) )) #expect(recorder.deliveredMessages.count == 2) #expect(recorder.deliveredMessages[0].id != recorder.deliveredMessages[1].id) #expect(recorder.deliveredMessages.allSatisfy { !$0.id.hasPrefix("media-") }) } @Test func lostCapabilityProofThenStableRetryReusesDurableIDWithoutSecondDiskWrite() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)] let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF]) + Data(repeating: 0x41, count: 128) let fileName = "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg" let file = BitchatFilePacket( fileName: fileName, fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) let expectedID = try #require(PrivateMediaMessageIdentity.stableID( senderPeerID: remotePeerID, recipientPeerID: localPeerID, fileName: fileName )) // First encrypted arrival may precede the sender's authenticated bit-9 // proof. It still uses the bit-8 stable ID/ACK contract. #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) // A later automatic retry after proof must resolve the same durable ID // rather than create a legacy random-ID bubble. #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_235) )) #expect(recorder.quotaReservations == [content.count]) #expect(recorder.saveCalls.count == 1) // The handler re-offers a durable duplicate so a relaunched UI can // restore its bubble; the synchronous conversation sink deduplicates. #expect(recorder.deliveredMessages.count == 2) #expect(recorder.lastSeenUpdates == [remotePeerID, remotePeerID]) #expect(recorder.deliveryAcks.count == 2) #expect(recorder.deliveryAcks.allSatisfy { $0.messageID == expectedID && $0.peerID == remotePeerID }) } @Test func acceptedPrivateMediaAfterRelaunchRedeliversDurableURLBeforeAck() throws { let root = FileManager.default.temporaryDirectory.appendingPathComponent( "private-media-handler-relaunch-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: root) } let store = BLEIncomingFileStore(baseDirectory: root) let content = Data([0xFF, 0xD8, 0xFF, 0xD9]) let file = BitchatFilePacket( fileName: "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg", fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) func configure(_ recorder: Recorder) { recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true )] recorder.saveOverride = { data, preferredName, subdirectory, fallbackExtension, defaultPrefix in store.save( data: data, preferredName: preferredName, subdirectory: subdirectory, fallbackExtension: fallbackExtension, defaultPrefix: defaultPrefix ) } recorder.receiptStateOverride = { store.privateMediaReceiptState(messageID: $0) } recorder.receiptCommitOverride = { store.commitPrivateMediaFile(messageID: $0, storedURL: $1) } recorder.removeIncomingFileOverride = { store.removeIncomingFile(at: $0) } recorder.finishIncomingFileDeliveryOverride = { store.finishIncomingFileDelivery(at: $0) } } let first = Recorder() configure(first) #expect(makeHandler(recorder: first).handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) let originalMessage = try #require(first.deliveredMessages.first) #expect(first.deliveryAcks.count == 1) // A fresh handler models process relaunch: its in-memory reservation // cache is empty, so only the durable receipt can suppress disk work. let relaunched = Recorder() configure(relaunched) #expect(makeHandler(recorder: relaunched).handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_235) )) #expect(relaunched.quotaReservations.isEmpty) #expect(relaunched.saveCalls.isEmpty) #expect(relaunched.receiptCommits.isEmpty) #expect(relaunched.deliveredMessages.count == 1) #expect(relaunched.deliveredMessages.first?.id == originalMessage.id) #expect(relaunched.deliveredMessages.first?.content == originalMessage.content) #expect(relaunched.deliveryAcks.count == 1) #expect(relaunched.deliveryAcks.first?.messageID == originalMessage.id) } @Test func inFlightStableDuplicateIsNotAcknowledgedAndFailedSaveRemainsRetryable() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)] let content = Data([0xFF, 0xD8, 0xFF]) + Data(repeating: 0x41, count: 128) let file = BitchatFilePacket( fileName: "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg", fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) var handler: BLEFileTransferHandler! var nestedResult: Bool? var failFirstSave = true recorder.saveOverride = { _, _, _, _, _ in if failFirstSave { failFirstSave = false nestedResult = handler.handlePrivatePayload( payload, from: self.remotePeerID, timestamp: Date(timeIntervalSince1970: 1_235) ) return nil } return recorder.saveResult } handler = makeHandler(recorder: recorder) // The nested arrival sees the first reservation as pending. It is // coalesced without an ACK; then the first durable save fails. #expect(!handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(nestedResult == true) #expect(recorder.saveCalls.count == 1) #expect(recorder.deliveryAcks.isEmpty) #expect(recorder.deliveredMessages.isEmpty) // Failure released the reservation, so the sender's later retry can // persist and deliver normally. #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_236) )) #expect(recorder.saveCalls.count == 2) #expect(recorder.deliveryAcks.count == 1) #expect(recorder.deliveredMessages.count == 1) } @Test func unavailableDurableReceiptStateWithholdsDiskDeliveryAndAck() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true )] let fileName = "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg" let messageID = try #require(PrivateMediaMessageIdentity.stableID( senderPeerID: remotePeerID, recipientPeerID: localPeerID, fileName: fileName )) recorder.receiptStates[messageID] = .unavailable let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF, 0xD9]) let payload = try #require(BitchatFilePacket( fileName: fileName, fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ).encode()) #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.receiptCommits.isEmpty) #expect(recorder.deliveredMessages.isEmpty) #expect(recorder.deliveryAcks.isEmpty) } @Test func durableReceiptCommitFailureRollsBackAndWithholdsDeliveryAck() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo( remotePeerID, nickname: "Alice", isVerified: true )] recorder.receiptCommitSucceeds = false let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF, 0xD9]) let payload = try #require(BitchatFilePacket( fileName: "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg", fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ).encode()) #expect(!handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(recorder.saveCalls.count == 1) #expect(recorder.receiptCommits.count == 1) #expect(recorder.removedIncomingFiles.count == 1) #expect(recorder.removedIncomingFiles.first == recorder.saveResult) #expect(recorder.deliveredMessages.isEmpty) #expect(recorder.deliveryAcks.isEmpty) } @Test func blockedPrivateMediaIsDroppedBeforeQuotaDiskAndDedupState() throws { let recorder = Recorder() recorder.blockedPeers = [remotePeerID] recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)] let handler = makeHandler(recorder: recorder) let content = Data([0xFF, 0xD8, 0xFF]) + Data(repeating: 0x41, count: 128) let file = BitchatFilePacket( fileName: "img_20260725_105708_1CC2760D-76AA-40C3-8013-C7FAA6C2EF99.jpg", fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content ) let payload = try #require(file.encode()) #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.lastSeenUpdates.isEmpty) #expect(recorder.deliveryAcks.isEmpty) #expect(recorder.deliveredMessages.isEmpty) // Unblocking must allow a retry through; the blocked attempt cannot // poison the stable-ID dedup reservation. recorder.blockedPeers = [] #expect(handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_235) )) #expect(recorder.saveCalls.count == 1) #expect(recorder.deliveredMessages.count == 1) #expect(recorder.deliveryAcks.count == 1) } @Test func decryptedPrivateFileOverPayloadCapIsRejectedBeforeQuotaOrDiskWrite() { let recorder = Recorder() let handler = makeHandler(recorder: recorder) let oversizedCount = FileTransferLimits.maxPayloadBytes + 1 var length = UInt32(oversizedCount).bigEndian var payload = Data([0x04]) // BitchatFilePacket CONTENT TLV withUnsafeBytes(of: &length) { payload.append(contentsOf: $0) } payload.append(Data(repeating: 0x41, count: oversizedCount)) #expect(!handler.handlePrivatePayload( payload, from: remotePeerID, timestamp: Date(timeIntervalSince1970: 1_234) )) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.lastSeenUpdates.isEmpty) #expect(recorder.deliveryAcks.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func malformedPayloadIsTrackedForSyncButDropped() { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey)] recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let packet = BitchatPacket( type: MessageType.fileTransfer.rawValue, senderID: Data(hexString: remotePeerID.id) ?? Data(), recipientID: nil, timestamp: 900_000, payload: Data([0x01, 0x02, 0x03]), signature: Data(repeating: 0x5A, count: 64), ttl: TransportConfig.messageTTLDefault ) // Local decode failures are not proof of forgery; the packet stays relayable. #expect(handler.handle(packet, from: remotePeerID)) // Sync tracking happens before payload validation, matching the original order. #expect(recorder.trackedPackets.count == 1) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func unsupportedMimeIsDroppedBeforeQuotaAndSave() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey)] recorder.signatureVerifies = true let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket(sender: remotePeerID, mimeType: nil, content: Data([0x4D, 0x5A, 0x00, 0x00])) #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.trackedPackets.count == 1) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func saveFailureSkipsDelivery() throws { let recorder = Recorder() recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true, signingPublicKey: sampleSigningKey)] recorder.signatureVerifies = true recorder.saveResult = nil let handler = makeHandler(recorder: recorder) let packet = try makeFileTransferPacket(sender: remotePeerID, mimeType: "application/pdf", content: Data("%PDF-1.7".utf8)) // A local save failure must not stop the mesh relay. #expect(handler.handle(packet, from: remotePeerID)) #expect(recorder.quotaReservations.count == 1) #expect(recorder.saveCalls.count == 1) #expect(recorder.lastSeenUpdates.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } @Test func quotaEvictionForFinalizedArrivalSkipsInFlightLiveCaptures() throws { let base = FileManager.default.temporaryDirectory .appendingPathComponent("quota-live-capture-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: base) } let store = BLEIncomingFileStore(baseDirectory: base) let incoming = try store.incomingDirectory(subdirectory: "voicenotes/incoming") // The in-flight partial is the LRU-oldest eviction candidate; without // the voice_live_ pattern guard it would be deleted first, unlinking // the inode under the coordinator's open FileHandle. let inFlight = incoming.appendingPathComponent("voice_live_00112233445566ff_1122334455667788_dm.aac") let evictable = incoming.appendingPathComponent("voice_old.m4a") try Data(count: 51 * 1024 * 1024).write(to: inFlight) try Data(count: 51 * 1024 * 1024).write(to: evictable) try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSinceNow: -7200)], ofItemAtPath: inFlight.path) try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSinceNow: -60)], ofItemAtPath: evictable.path) // 102 MB used against the 100 MB quota forces one eviction. This is // the finalized-file arrival path (BLEFileTransferHandler via // BLEService), which knows nothing about in-flight captures — the // store itself must protect them. store.enforceQuota(reservingBytes: 0) #expect(FileManager.default.fileExists(atPath: inFlight.path)) #expect(!FileManager.default.fileExists(atPath: evictable.path)) } @Test func panicWipeDeletesEveryManagedMediaFileAndRecreatesEmptyDirectories() throws { let base = FileManager.default.temporaryDirectory .appendingPathComponent("panic-media-wipe-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: base) } let store = BLEIncomingFileStore(baseDirectory: base) let subdirectories = [ "voicenotes/incoming", "voicenotes/outgoing", "images/incoming", "images/outgoing", "files/incoming", "files/outgoing" ] for subdirectory in subdirectories { let directory = base .appendingPathComponent("files", isDirectory: true) .appendingPathComponent(subdirectory, isDirectory: true) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) try Data("secret".utf8).write(to: directory.appendingPathComponent("artifact.bin")) } let unmanaged = base.appendingPathComponent("files/legacy/secret.bin") try FileManager.default.createDirectory(at: unmanaged.deletingLastPathComponent(), withIntermediateDirectories: true) try Data("legacy".utf8).write(to: unmanaged) try store.panicWipe() #expect(!FileManager.default.fileExists(atPath: unmanaged.path)) for subdirectory in subdirectories { let directory = base .appendingPathComponent("files", isDirectory: true) .appendingPathComponent(subdirectory, isDirectory: true) var isDirectory: ObjCBool = false #expect(FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory)) #expect(isDirectory.boolValue) #expect(try FileManager.default.contentsOfDirectory(atPath: directory.path).isEmpty) } } @Test func panicWipeClearsCachedPrivateMediaReceiptDecisions() throws { let base = FileManager.default.temporaryDirectory .appendingPathComponent( "panic-receipt-cache-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: base) } let messageID = "media-00112233445566778899aabbccddeeff" let seed = BLEPrivateMediaReceiptStore(baseDirectory: base) let payload = base .appendingPathComponent( "files/images/incoming", isDirectory: true ) .appendingPathComponent("panic-receipt.jpg") try FileManager.default.createDirectory( at: payload.deletingLastPathComponent(), withIntermediateDirectories: true ) try Data("secret".utf8).write(to: payload, options: .atomic) #expect(seed.commitAccepted( messageID: messageID, storedURL: payload )) #expect(seed.recordDeleted(messageID: messageID)) let store = BLEIncomingFileStore(baseDirectory: base) #expect( store.privateMediaReceiptState(messageID: messageID) == .tombstoned ) try store.panicWipe() #expect( store.privateMediaReceiptState(messageID: messageID) == .absent ) } @Test func panicWipeInvalidatesPayloadCoordinationReservations() throws { let base = FileManager.default.temporaryDirectory .appendingPathComponent( "panic-payload-coordination-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: base) } let store = BLEIncomingFileStore(baseDirectory: base) let pendingName = "pending-before-panic.jpg" let pendingURL = try #require(store.save( data: Data("old".utf8), preferredName: pendingName, subdirectory: "images/incoming", fallbackExtension: "jpg", defaultPrefix: "image" )) let messageID = "media-aabbccddeeff00112233445566778899" let reservation = try #require(store.reservePrivateMediaDeletion( messageIDs: [messageID], payloadRelativePaths: [ messageID: "images/incoming/delete-before-panic.jpg" ] )) try store.panicWipe() #expect(!store.commitPrivateMediaDeletion( reservation: reservation, messageIDs: [messageID], payloadRelativePaths: [ messageID: "images/incoming/delete-before-panic.jpg" ], protectedPayloadRelativePaths: [] )) let postPanicURL = try #require(store.save( data: Data("new".utf8), preferredName: pendingName, subdirectory: "images/incoming", fallbackExtension: "jpg", defaultPrefix: "image" )) #expect(pendingURL.lastPathComponent == pendingName) #expect(postPanicURL.lastPathComponent == pendingName) } @Test func panicWipeAttemptsDeletionWhenMarkerPersistenceFails() throws { enum MarkerFailure: Error { case unavailable } let base = FileManager.default.temporaryDirectory .appendingPathComponent( "panic-marker-failure-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: base) } let secret = base .appendingPathComponent("files/images/outgoing", isDirectory: true) .appendingPathComponent("secret.jpg") try FileManager.default.createDirectory( at: secret.deletingLastPathComponent(), withIntermediateDirectories: true ) try Data("secret".utf8).write(to: secret) let store = BLEIncomingFileStore( baseDirectory: base, panicMarkerWriter: { _, _ in throw MarkerFailure.unavailable } ) do { try store.panicWipe(hasDurablePendingMarker: false) Issue.record("Expected the missing durable marker to fail closed") } catch { // The marker error is reported only after the deletion attempt. } #expect(!FileManager.default.fileExists(atPath: secret.path)) #expect( FileManager.default.fileExists( atPath: secret.deletingLastPathComponent().path ) ) } @Test func externalMarkerAllowsDeletionToCommitWhenFileMarkerFails() throws { enum MarkerFailure: Error { case unavailable } let base = FileManager.default.temporaryDirectory .appendingPathComponent( "panic-external-marker-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: base) } let secret = base .appendingPathComponent("files/voicenotes/incoming", isDirectory: true) .appendingPathComponent("secret.m4a") try FileManager.default.createDirectory( at: secret.deletingLastPathComponent(), withIntermediateDirectories: true ) try Data("secret".utf8).write(to: secret) let store = BLEIncomingFileStore( baseDirectory: base, panicMarkerWriter: { _, _ in throw MarkerFailure.unavailable } ) try store.panicWipe(hasDurablePendingMarker: true) #expect(!FileManager.default.fileExists(atPath: secret.path)) } @Test func panicRecoveryMarkerPersistsUntilExplicitCommit() throws { let base = FileManager.default.temporaryDirectory .appendingPathComponent( "panic-recovery-marker-\(UUID().uuidString)", isDirectory: true ) defer { try? FileManager.default.removeItem(at: base) } let store = BLEIncomingFileStore(baseDirectory: base) try store.markPanicRecoveryPending() #expect(try store.isPanicRecoveryPending()) try store.panicWipe(hasDurablePendingMarker: true) #expect(try store.isPanicRecoveryPending()) try store.completePanicRecovery() #expect(try !store.isPanicRecoveryPending()) } private func expectNoSideEffects(_ recorder: Recorder) { #expect(recorder.signedNameQueries.isEmpty) #expect(recorder.trackedPackets.isEmpty) #expect(recorder.quotaReservations.isEmpty) #expect(recorder.saveCalls.isEmpty) #expect(recorder.lastSeenUpdates.isEmpty) #expect(recorder.deliveredMessages.isEmpty) } private func makePeerInfo( _ peerID: PeerID, nickname: String, isVerified: Bool, isConnected: Bool = true, signingPublicKey: Data? = nil ) -> BLEPeerInfo { BLEPeerInfo( peerID: peerID, nickname: nickname, isConnected: isConnected, noisePublicKey: nil, signingPublicKey: signingPublicKey, isVerifiedNickname: isVerified, lastSeen: Date(timeIntervalSince1970: 999) ) } private func makeFileTransferPacket( sender: PeerID, mimeType: String?, content: Data, ttl: UInt8 = TransportConfig.messageTTLDefault, recipientID: Data? = nil, fileName: String = "sample", hasSignature: Bool = true ) throws -> BitchatPacket { let filePacket = BitchatFilePacket( fileName: fileName, fileSize: UInt64(content.count), mimeType: mimeType, content: content ) let payload = try #require(filePacket.encode()) return BitchatPacket( type: MessageType.fileTransfer.rawValue, senderID: Data(hexString: sender.id) ?? Data(), recipientID: recipientID, timestamp: 900_000, payload: payload, signature: hasSignature ? Data(repeating: 0x5A, count: 64) : nil, ttl: ttl ) } }