mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 18:05:21 +00:00
Make panic wipe deterministic and device-bound (#1431)
* Make panic wipe deterministic and device-bound * Scope install markers to iOS * Harden panic recovery and service shutdown * Invalidate queued BLE ingress during panic * Harden panic keychain and media cleanup --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: jack <jack@deck.local>
This commit is contained in:
@@ -15,8 +15,13 @@ import BitFoundation
|
||||
|
||||
/// Creates a ChatViewModel with mock dependencies for testing
|
||||
@MainActor
|
||||
private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) {
|
||||
let keychain = MockKeychain()
|
||||
private func makeTestableViewModel(
|
||||
keychain injectedKeychain: MockKeychain? = nil,
|
||||
panicMediaWipe: (() throws -> Void)? = nil,
|
||||
panicRecoveryOperations: PanicRecoveryOperations? = nil,
|
||||
panicNetworkLifecycle: PanicNetworkLifecycle = .noop
|
||||
) -> (viewModel: ChatViewModel, transport: MockTransport) {
|
||||
let keychain = injectedKeychain ?? MockKeychain()
|
||||
let keychainHelper = MockKeychainHelper()
|
||||
let idBridge = NostrIdentityBridge(keychain: keychainHelper)
|
||||
let identityManager = MockIdentityManager(keychain)
|
||||
@@ -26,7 +31,10 @@ private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: Mo
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
identityManager: identityManager,
|
||||
transport: transport
|
||||
transport: transport,
|
||||
panicMediaWipe: panicMediaWipe,
|
||||
panicRecoveryOperations: panicRecoveryOperations,
|
||||
panicNetworkLifecycle: panicNetworkLifecycle
|
||||
)
|
||||
|
||||
return (viewModel, transport)
|
||||
@@ -1116,6 +1124,159 @@ struct ChatViewModelBluetoothTests {
|
||||
|
||||
struct ChatViewModelPanicTests {
|
||||
|
||||
@Test @MainActor
|
||||
func panicClearAllData_finishesMediaWipeBeforeReturning() {
|
||||
var wipeFinished = false
|
||||
let (viewModel, _) = makeTestableViewModel(panicMediaWipe: {
|
||||
wipeFinished = true
|
||||
})
|
||||
|
||||
viewModel.panicClearAllData()
|
||||
|
||||
#expect(wipeFinished)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func panicClearAllData_stopsNetworkBeforeWipeAndRestartsAfterCommit() {
|
||||
var events: [String] = []
|
||||
let lifecycle = PanicNetworkLifecycle(
|
||||
stop: { events.append("stop") },
|
||||
restart: { events.append("restart") }
|
||||
)
|
||||
let (viewModel, _) = makeTestableViewModel(
|
||||
panicMediaWipe: { events.append("wipe") },
|
||||
panicNetworkLifecycle: lifecycle
|
||||
)
|
||||
|
||||
let completed = viewModel.panicClearAllData()
|
||||
|
||||
#expect(completed)
|
||||
#expect(events == ["stop", "wipe", "restart"])
|
||||
#expect(viewModel.networkActivationAllowed)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func panicKeychainFailureKeepsRecoveryPendingAndServicesStopped() {
|
||||
let keychain = MockKeychain()
|
||||
keychain.simulatedDeleteAllResult = false
|
||||
var events: [String] = []
|
||||
let operations = PanicRecoveryOperations(
|
||||
isPending: { false },
|
||||
begin: {
|
||||
events.append("begin")
|
||||
return PanicRecoveryIntent(
|
||||
fileMarkerEstablished: true,
|
||||
externalMarkerEstablished: false
|
||||
)
|
||||
},
|
||||
wipeMedia: { _ in events.append("wipe") },
|
||||
complete: { events.append("complete") }
|
||||
)
|
||||
let lifecycle = PanicNetworkLifecycle(
|
||||
stop: { events.append("stop") },
|
||||
restart: { events.append("restart") }
|
||||
)
|
||||
let (viewModel, transport) = makeTestableViewModel(
|
||||
keychain: keychain,
|
||||
panicRecoveryOperations: operations,
|
||||
panicNetworkLifecycle: lifecycle
|
||||
)
|
||||
let startsBeforePanic = transport.startServicesCallCount
|
||||
|
||||
let completed = viewModel.panicClearAllData()
|
||||
|
||||
#expect(!completed)
|
||||
#expect(events == ["stop", "begin", "wipe"])
|
||||
#expect(keychain.deleteAllCallCount == 1)
|
||||
#expect(transport.startServicesCallCount == startsBeforePanic)
|
||||
#expect(!viewModel.networkActivationAllowed)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func pendingPanicRecoveryCompletesBeforeTransportBootstrap() {
|
||||
var events: [String] = []
|
||||
let operations = PanicRecoveryOperations(
|
||||
isPending: {
|
||||
events.append("read")
|
||||
return true
|
||||
},
|
||||
begin: {
|
||||
events.append("begin")
|
||||
return PanicRecoveryIntent(
|
||||
fileMarkerEstablished: true,
|
||||
externalMarkerEstablished: false
|
||||
)
|
||||
},
|
||||
wipeMedia: { _ in events.append("wipe") },
|
||||
complete: { events.append("complete") }
|
||||
)
|
||||
|
||||
let (viewModel, transport) = makeTestableViewModel(
|
||||
panicRecoveryOperations: operations
|
||||
)
|
||||
|
||||
#expect(events == ["read", "begin", "wipe", "complete"])
|
||||
#expect(transport.emergencyDisconnectCallCount == 1)
|
||||
#expect(transport.startServicesCallCount == 1)
|
||||
#expect(viewModel.networkActivationAllowed)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func failedStartupRecoveryLeavesTransportAndNetworkBlocked() {
|
||||
enum WipeFailure: Error { case failed }
|
||||
var completedMarker = false
|
||||
let operations = PanicRecoveryOperations(
|
||||
isPending: { true },
|
||||
begin: {
|
||||
PanicRecoveryIntent(
|
||||
fileMarkerEstablished: true,
|
||||
externalMarkerEstablished: false
|
||||
)
|
||||
},
|
||||
wipeMedia: { _ in throw WipeFailure.failed },
|
||||
complete: { completedMarker = true }
|
||||
)
|
||||
|
||||
let (viewModel, transport) = makeTestableViewModel(
|
||||
panicRecoveryOperations: operations
|
||||
)
|
||||
|
||||
#expect(!completedMarker)
|
||||
#expect(transport.emergencyDisconnectCallCount == 1)
|
||||
#expect(transport.startServicesCallCount == 0)
|
||||
#expect(!viewModel.networkActivationAllowed)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func failedStartupKeychainRecoveryLeavesIntentAndTransportBlocked() {
|
||||
let keychain = MockKeychain()
|
||||
keychain.simulatedDeleteAllResult = false
|
||||
var events: [String] = []
|
||||
let operations = PanicRecoveryOperations(
|
||||
isPending: { true },
|
||||
begin: {
|
||||
events.append("begin")
|
||||
return PanicRecoveryIntent(
|
||||
fileMarkerEstablished: true,
|
||||
externalMarkerEstablished: true
|
||||
)
|
||||
},
|
||||
wipeMedia: { _ in events.append("wipe") },
|
||||
complete: { events.append("complete") }
|
||||
)
|
||||
|
||||
let (viewModel, transport) = makeTestableViewModel(
|
||||
keychain: keychain,
|
||||
panicRecoveryOperations: operations
|
||||
)
|
||||
|
||||
#expect(events == ["begin", "wipe"])
|
||||
#expect(keychain.deleteAllCallCount == 1)
|
||||
#expect(transport.emergencyDisconnectCallCount == 1)
|
||||
#expect(transport.startServicesCallCount == 0)
|
||||
#expect(!viewModel.networkActivationAllowed)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func panicClearAllData_delegatesToTransport() async {
|
||||
let (viewModel, transport) = makeTestableViewModel()
|
||||
|
||||
Reference in New Issue
Block a user