mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-14 14:19:57 +02:00
fix(core): recover unrecoverable device identity
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import XCTest
|
||||
import VnidropCore
|
||||
@testable import VniDrop
|
||||
|
||||
/// Ports app-level assertions: core initialization on launch, destination
|
||||
@@ -50,4 +51,21 @@ final class AppModelTests: XCTestCase {
|
||||
await waitUntil { model.themeMode == .dark }
|
||||
XCTAssertEqual(model.themeMode, .dark)
|
||||
}
|
||||
|
||||
func testMissingEndpointIdentityOffersExplicitResetAndRecoversStartup() async {
|
||||
let core = FakeCoreGateway()
|
||||
core.initializeResult = .failure(
|
||||
VnidropError.SecureStorageMissing(reason: "credential is missing")
|
||||
)
|
||||
let model = makeModel(core, preferences: Fixtures.preferences())
|
||||
|
||||
await waitUntil { model.startupRecovery == .identityUnrecoverable }
|
||||
XCTAssertFalse(core.state.isInitialized)
|
||||
|
||||
await model.resetUnrecoverableIdentity()
|
||||
|
||||
XCTAssertEqual(core.resetUnrecoverableIdentityCount, 1)
|
||||
XCTAssertNil(model.startupRecovery)
|
||||
XCTAssertTrue(core.state.isInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,14 @@ private final class BlockingCoreBindingFactory: CoreBindingFactory, @unchecked S
|
||||
throw BlockingCoreFactoryError.stopped
|
||||
}
|
||||
|
||||
func resetUnrecoverableIdentity(
|
||||
appDataDir: String,
|
||||
eventSink: CoreEventSink,
|
||||
networkConfiguration: RelayConfiguration
|
||||
) throws -> VnidropCore {
|
||||
throw BlockingCoreFactoryError.stopped
|
||||
}
|
||||
|
||||
func waitUntilInitializationStarts() async {
|
||||
await withCheckedContinuation { continuation in
|
||||
lock.lock()
|
||||
|
||||
@@ -27,6 +27,7 @@ final class FakeCoreGateway: CoreGateway {
|
||||
var clearReceiveHistoryResult: Result<UInt64, Error> = .success(0)
|
||||
var initializeResult: Result<Void, Error> = .success(())
|
||||
var initializeResults: [Result<Void, Error>] = []
|
||||
var resetUnrecoverableIdentityResult: Result<Void, Error> = .success(())
|
||||
|
||||
// Recorded calls
|
||||
private(set) var responses: [(id: String, accepted: Bool, reason: String?)] = []
|
||||
@@ -38,6 +39,7 @@ final class FakeCoreGateway: CoreGateway {
|
||||
private(set) var lastReceiveReceiverName: String?
|
||||
private(set) var lastShareAccessPolicy: ShareAccessPolicy?
|
||||
private(set) var initializedNetworkConfigurations: [RelayConfiguration] = []
|
||||
private(set) var resetUnrecoverableIdentityCount = 0
|
||||
|
||||
func setState(_ state: CoreState) { stateSubject.send(state) }
|
||||
func emit(_ signal: CoreSignal) { signalsSubject.send(signal) }
|
||||
@@ -54,6 +56,19 @@ final class FakeCoreGateway: CoreGateway {
|
||||
stateSubject.send(s)
|
||||
return .success(())
|
||||
}
|
||||
func resetUnrecoverableIdentity(
|
||||
appDataDir: String,
|
||||
networkConfiguration: RelayConfiguration
|
||||
) async -> Result<Void, Error> {
|
||||
resetUnrecoverableIdentityCount += 1
|
||||
guard case .success = resetUnrecoverableIdentityResult else {
|
||||
return resetUnrecoverableIdentityResult
|
||||
}
|
||||
var s = stateSubject.value
|
||||
s.isInitialized = true
|
||||
stateSubject.send(s)
|
||||
return .success(())
|
||||
}
|
||||
func shutdown() {}
|
||||
func shareSources(_ sources: [ShareSource], transferName: String, senderName: String, accessPolicy: ShareAccessPolicy) async -> Result<Share, Error> {
|
||||
lastShareAccessPolicy = accessPolicy
|
||||
|
||||
@@ -39,19 +39,20 @@ final class SavedDeviceCoreContractTests: XCTestCase {
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
let sink = RecordingSink()
|
||||
let first = try VnidropCore.initializeWithLimitsAndNetworkConfig(
|
||||
var first: VnidropCore? = try VnidropCore.initializeWithLimitsAndNetworkConfig(
|
||||
appDataDir: directory.path,
|
||||
eventSink: sink,
|
||||
limits: defaultCoreLimits(),
|
||||
networkConfig: CoreNetworkConfig(mode: .automatic, relayUrls: [])
|
||||
)
|
||||
let endpointId = first.status().endpointId
|
||||
let endpointId = first!.status().endpointId
|
||||
XCTAssertFalse(endpointId.isEmpty)
|
||||
XCTAssertFalse(
|
||||
FileManager.default.fileExists(atPath: directory.appendingPathComponent("iroh.secret").path),
|
||||
"protected identity must not fall back to plaintext"
|
||||
)
|
||||
first.shutdown()
|
||||
first?.shutdown()
|
||||
first = nil
|
||||
|
||||
let restarted = try VnidropCore.initializeWithLimitsAndNetworkConfig(
|
||||
appDataDir: directory.path,
|
||||
@@ -134,6 +135,9 @@ final class SavedDeviceCoreContractTests: XCTestCase {
|
||||
let _: (
|
||||
(String, CoreEventSink, CoreLimits, CoreNetworkConfig) throws -> VnidropCore
|
||||
) = VnidropCore.initializeWithLimitsAndNetworkConfig
|
||||
let _: (
|
||||
(String, CoreEventSink, CoreLimits, CoreNetworkConfig) throws -> VnidropCore
|
||||
) = VnidropCore.resetUnrecoverableIdentityWithLimitsAndNetworkConfig
|
||||
let capabilities: SavedDeviceCapabilities = savedDeviceCapabilities()
|
||||
XCTAssertGreaterThanOrEqual(capabilities.domainContractVersion, 1)
|
||||
XCTAssertNotNil(defaultCoreLimits().maxSavedDevices)
|
||||
@@ -165,6 +169,7 @@ final class SavedDeviceCoreContractTests: XCTestCase {
|
||||
XCTAssertFalse(source.contains("ExperimentalSavedDeviceCapabilities"))
|
||||
XCTAssertFalse(source.contains("experimentalSavedDeviceCapabilities"))
|
||||
XCTAssertTrue(source.contains("initializeWithLimitsAndNetworkConfig"))
|
||||
XCTAssertTrue(source.contains("resetUnrecoverableIdentityWithLimitsAndNetworkConfig"))
|
||||
XCTAssertTrue(source.contains("public struct SavedDeviceCapabilities"))
|
||||
XCTAssertTrue(source.contains("public func savedDeviceCapabilities()"))
|
||||
XCTAssertTrue(source.contains("setSavedDeviceLabel"))
|
||||
|
||||
@@ -63,6 +63,9 @@ final class UserFacingErrorTests: XCTestCase {
|
||||
XCTAssertEqual(VnidropError.InvalidInput(reason: "bad path").toUiText(), .resource(L10n.Error.invalidInput))
|
||||
XCTAssertFalse(VnidropError.FilesystemPermission(reason: "read-only").canRetryWithoutChangingInput)
|
||||
XCTAssertFalse(VnidropError.DestinationExists(reason: "target exists").canRetryWithoutChangingInput)
|
||||
XCTAssertFalse(VnidropError.SecureStorageMissing(reason: "credential is missing").canRetryWithoutChangingInput)
|
||||
XCTAssertFalse(VnidropError.SecureStorageCorrupted(reason: "credential is corrupted").canRetryWithoutChangingInput)
|
||||
XCTAssertTrue(VnidropError.Network(reason: "offline").canRetryWithoutChangingInput)
|
||||
XCTAssertTrue(VnidropError.SecureStorageLocked(reason: "credential store is locked").canRetryWithoutChangingInput)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user