mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 04:25:21 +00:00
Make panic wipe deterministic and device-bound
This commit is contained in:
@@ -188,6 +188,21 @@ struct ChatMediaTransferCoordinatorContextTests {
|
||||
#expect(coordinator.messageIDToTransferId.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func resetForPanic_cancelsEveryTransportTransferAndClearsMappings() {
|
||||
let context = MockChatMediaTransferContext()
|
||||
let coordinator = ChatMediaTransferCoordinator(context: context)
|
||||
coordinator.registerTransfer(transferId: "t1", messageID: "m1")
|
||||
coordinator.registerTransfer(transferId: "t1", messageID: "m2")
|
||||
coordinator.registerTransfer(transferId: "t2", messageID: "m3")
|
||||
|
||||
coordinator.resetForPanic()
|
||||
|
||||
#expect(Set(context.cancelledTransfers) == Set(["t1", "t2"]))
|
||||
#expect(coordinator.transferIdToMessageIDs.isEmpty)
|
||||
#expect(coordinator.messageIDToTransferId.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func sendVoiceNote_blockedContextRemovesFileAndExplains() async throws {
|
||||
let context = MockChatMediaTransferContext()
|
||||
|
||||
@@ -15,7 +15,9 @@ import BitFoundation
|
||||
|
||||
/// Creates a ChatViewModel with mock dependencies for testing
|
||||
@MainActor
|
||||
private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) {
|
||||
private func makeTestableViewModel(
|
||||
panicMediaWipe: (() throws -> Void)? = nil
|
||||
) -> (viewModel: ChatViewModel, transport: MockTransport) {
|
||||
let keychain = MockKeychain()
|
||||
let keychainHelper = MockKeychainHelper()
|
||||
let idBridge = NostrIdentityBridge(keychain: keychainHelper)
|
||||
@@ -26,7 +28,8 @@ private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: Mo
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
identityManager: identityManager,
|
||||
transport: transport
|
||||
transport: transport,
|
||||
panicMediaWipe: panicMediaWipe
|
||||
)
|
||||
|
||||
return (viewModel, transport)
|
||||
@@ -1097,6 +1100,18 @@ struct ChatViewModelBluetoothTests {
|
||||
|
||||
struct ChatViewModelPanicTests {
|
||||
|
||||
@Test @MainActor
|
||||
func panicClearAllData_finishesMediaWipeBeforeReturning() {
|
||||
var wipeFinished = false
|
||||
let (viewModel, _) = makeTestableViewModel {
|
||||
wipeFinished = true
|
||||
}
|
||||
|
||||
viewModel.panicClearAllData()
|
||||
|
||||
#expect(wipeFinished)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func panicClearAllData_delegatesToTransport() async {
|
||||
let (viewModel, transport) = makeTestableViewModel()
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import BitFoundation
|
||||
@testable import bitchat
|
||||
|
||||
@Suite("PreviewKeychainManager Tests")
|
||||
struct PreviewKeychainManagerTests {
|
||||
|
||||
@Test("Install lifecycle distinguishes upgrade, reinstall, bootstrap, and unreadable keychain")
|
||||
func installLifecycleDecision() {
|
||||
#expect(KeychainManager.installLifecycleAction(
|
||||
containerKnowsMarker: true,
|
||||
markerRead: .success(Data([1]))
|
||||
) == .markerPresent)
|
||||
#expect(KeychainManager.installLifecycleAction(
|
||||
containerKnowsMarker: false,
|
||||
markerRead: .success(Data([1]))
|
||||
) == .clearStaleKeys)
|
||||
#expect(KeychainManager.installLifecycleAction(
|
||||
containerKnowsMarker: false,
|
||||
markerRead: .itemNotFound
|
||||
) == .bootstrapMarker)
|
||||
#expect(KeychainManager.installLifecycleAction(
|
||||
containerKnowsMarker: false,
|
||||
markerRead: .deviceLocked
|
||||
) == .retryLater)
|
||||
}
|
||||
|
||||
@Test("Preview keychain manager stores identity and service-scoped data in memory")
|
||||
func previewKeychainManagerRoundTripsData() {
|
||||
let manager = PreviewKeychainManager()
|
||||
|
||||
@@ -370,6 +370,46 @@ struct BLEFileTransferHandlerTests {
|
||||
#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)
|
||||
}
|
||||
}
|
||||
|
||||
private func expectNoSideEffects(_ recorder: Recorder) {
|
||||
#expect(recorder.signedNameQueries.isEmpty)
|
||||
#expect(recorder.trackedPackets.isEmpty)
|
||||
|
||||
Reference in New Issue
Block a user