mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 06:05:20 +00:00
Harden PTT audio and the 1.7.1 release (#1423)
Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage. Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives.
This commit is contained in:
@@ -68,6 +68,145 @@ struct MessageOutboxStoreTests {
|
||||
#expect(other.load().isEmpty)
|
||||
}
|
||||
|
||||
@Test func permanentlyMissingDeviceKeyDiscardsOrphanAndNewSavesSurviveRelaunch() {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
|
||||
MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
.save([peerID: [makeMessage("orphaned")]])
|
||||
#expect(FileManager.default.fileExists(atPath: fileURL.path))
|
||||
|
||||
// Models a device restore: Application Support brought the sealed
|
||||
// file across, but its AfterFirstUnlockThisDeviceOnly key cannot.
|
||||
keychain.deleteAll(service: "chat.bitchat.outbox")
|
||||
let restored = MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
#expect(restored.load().isEmpty)
|
||||
#expect(!FileManager.default.fileExists(atPath: fileURL.path))
|
||||
|
||||
restored.save([peerID: [makeMessage("fresh")]])
|
||||
let relaunched = MessageOutboxStore(keychain: keychain, fileURL: fileURL).load()
|
||||
#expect(relaunched[peerID]?.map(\.messageID) == ["fresh"])
|
||||
}
|
||||
|
||||
@Test func temporarilyLockedKeyDoesNotDiscardDurableSnapshot() {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
|
||||
MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
.save([peerID: [makeMessage("durable")]])
|
||||
let durableBytes = try? Data(contentsOf: fileURL)
|
||||
|
||||
keychain.simulatedGenericReadError = .deviceLocked
|
||||
let restored = MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
#expect(restored.load().isEmpty)
|
||||
#expect((try? Data(contentsOf: fileURL)) == durableBytes)
|
||||
|
||||
keychain.simulatedGenericReadError = nil
|
||||
let recovered = restored.retryDeferredLoad()
|
||||
#expect(recovered?[peerID]?.map(\.messageID) == ["durable"])
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func panicWipeInvalidatesQueuedRecoveryCallback() async {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
.save([peerID: [makeMessage("durable")]])
|
||||
|
||||
var protectedDataUnavailable = true
|
||||
let restored = MessageOutboxStore(
|
||||
keychain: keychain,
|
||||
fileURL: fileURL,
|
||||
readData: { url in
|
||||
if protectedDataUnavailable {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoPermissionError)
|
||||
}
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
)
|
||||
#expect(restored.load().isEmpty)
|
||||
var deliveredRecoveries: [MessageOutboxStore.Snapshot] = []
|
||||
restored.setRecoveryHandler { deliveredRecoveries.append($0) }
|
||||
|
||||
protectedDataUnavailable = false
|
||||
restored.retryDeferredLoad() // queues the handler onto MainActor
|
||||
restored.wipe() // invalidates it before the queued Task runs
|
||||
await Task.yield()
|
||||
|
||||
#expect(deliveredRecoveries.isEmpty)
|
||||
#expect(!FileManager.default.fileExists(atPath: fileURL.path))
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func wipeBetweenRecoveryUnlockAndNotificationDropsRecoveredSnapshot() async {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
.save([peerID: [makeMessage("durable")]])
|
||||
|
||||
var protectedDataUnavailable = true
|
||||
var gapAction: (() -> Void)?
|
||||
let restored = MessageOutboxStore(
|
||||
keychain: keychain,
|
||||
fileURL: fileURL,
|
||||
readData: { url in
|
||||
if protectedDataUnavailable {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoPermissionError)
|
||||
}
|
||||
return try Data(contentsOf: url)
|
||||
},
|
||||
beforeRecoveryNotification: { gapAction?() }
|
||||
)
|
||||
#expect(restored.load().isEmpty)
|
||||
var deliveredRecoveries: [MessageOutboxStore.Snapshot] = []
|
||||
restored.setRecoveryHandler { deliveredRecoveries.append($0) }
|
||||
gapAction = { restored.wipe() }
|
||||
|
||||
protectedDataUnavailable = false
|
||||
restored.retryDeferredLoad()
|
||||
await Task.yield()
|
||||
|
||||
#expect(deliveredRecoveries.isEmpty)
|
||||
#expect(!FileManager.default.fileExists(atPath: fileURL.path))
|
||||
}
|
||||
|
||||
@Test func deferredRemovalTombstoneFiltersUnseenDurableMessage() {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
.save([peerID: [makeMessage("durable")]])
|
||||
|
||||
var protectedDataUnavailable = true
|
||||
let restored = MessageOutboxStore(
|
||||
keychain: keychain,
|
||||
fileURL: fileURL,
|
||||
readData: { url in
|
||||
if protectedDataUnavailable {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoPermissionError)
|
||||
}
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
)
|
||||
#expect(restored.load().isEmpty)
|
||||
restored.recordRemoval(messageID: "durable")
|
||||
restored.save([:])
|
||||
|
||||
protectedDataUnavailable = false
|
||||
let recovered = restored.retryDeferredLoad()
|
||||
#expect(recovered?.isEmpty == true)
|
||||
#expect(MessageOutboxStore(keychain: keychain, fileURL: fileURL).load().isEmpty)
|
||||
}
|
||||
|
||||
@Test func wipeRemovesFileAndKey() {
|
||||
let fileURL = makeTempURL()
|
||||
let keychain = MockKeychain()
|
||||
@@ -89,4 +228,69 @@ struct MessageOutboxStoreTests {
|
||||
store.save([peerID: []])
|
||||
#expect(!FileManager.default.fileExists(atPath: fileURL.path))
|
||||
}
|
||||
|
||||
@Test func protectedDataReadFailureDefersWriteAndMergesOnRecovery() {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
|
||||
let seed = MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
seed.save([peerID: [makeMessage("durable")]])
|
||||
let durableBytes = try? Data(contentsOf: fileURL)
|
||||
|
||||
var protectedDataUnavailable = true
|
||||
let restored = MessageOutboxStore(
|
||||
keychain: keychain,
|
||||
fileURL: fileURL,
|
||||
readData: { url in
|
||||
if protectedDataUnavailable {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoPermissionError)
|
||||
}
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
)
|
||||
#expect(restored.load().isEmpty)
|
||||
restored.save([peerID: [makeMessage("during-wake")]])
|
||||
|
||||
// The unreadable durable snapshot was not replaced by the partial
|
||||
// in-memory outbox from the locked restoration.
|
||||
#expect((try? Data(contentsOf: fileURL)) == durableBytes)
|
||||
|
||||
protectedDataUnavailable = false
|
||||
let recovered = restored.retryDeferredLoad()
|
||||
#expect(Set(recovered?[peerID]?.map(\.messageID) ?? []) == ["durable", "during-wake"])
|
||||
|
||||
let relaunched = MessageOutboxStore(keychain: keychain, fileURL: fileURL).load()
|
||||
#expect(Set(relaunched[peerID]?.map(\.messageID) ?? []) == ["durable", "during-wake"])
|
||||
}
|
||||
|
||||
@Test func lockedWakeRemovalDoesNotResurrectPendingMessageOnRecovery() {
|
||||
let fileURL = makeTempURL()
|
||||
defer { try? FileManager.default.removeItem(at: fileURL) }
|
||||
let keychain = MockKeychain()
|
||||
let peerID = PeerID(str: "0000000000000001")
|
||||
MessageOutboxStore(keychain: keychain, fileURL: fileURL)
|
||||
.save([peerID: [makeMessage("durable")]])
|
||||
|
||||
var protectedDataUnavailable = true
|
||||
let restored = MessageOutboxStore(
|
||||
keychain: keychain,
|
||||
fileURL: fileURL,
|
||||
readData: { url in
|
||||
if protectedDataUnavailable {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoPermissionError)
|
||||
}
|
||||
return try Data(contentsOf: url)
|
||||
}
|
||||
)
|
||||
#expect(restored.load().isEmpty)
|
||||
restored.save([peerID: [makeMessage("wake")]])
|
||||
// A later delivery ack produces the complete empty in-memory view.
|
||||
restored.save([:])
|
||||
|
||||
protectedDataUnavailable = false
|
||||
let recovered = restored.retryDeferredLoad()
|
||||
#expect(recovered?[peerID]?.map(\.messageID) == ["durable"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user