RadrootsLegacyMigrationCancellationTests.swift (2344B)
1 import Foundation 2 import RadrootsKitTesting 3 import Testing 4 5 @testable import RadrootsKit 6 7 @Test func cancellationAfterImportRetainsCommittedKeyAndLegacyUntilExplicitRecovery() async throws { 8 let key = RadrootsSecureStoreKey(namespace: "legacy", name: "selected_secret_hex") 9 let store = CancelOnActiveReadStore() 10 let legacy = Data(String(repeating: "01", count: 32).utf8) 11 try store.put(legacy, for: key) 12 let custody = try RadrootsIdentityCustody( 13 configuration: RadrootsIdentityCustodyConfiguration(namespace: UUID().uuidString.lowercased()), 14 secureStore: store, metadataStore: RadrootsInMemoryIdentityMetadataStore(), 15 userPresence: RadrootsFakeUserPresence() 16 ) 17 let task = Task { try await custody.migrateLegacyIdentity(from: key) } 18 await #expect(throws: RadrootsIdentityCustodyError.cancelled) { try await task.value } 19 let retained = await custody.snapshot() 20 let expected = try #require(retained.identity?.publicKeyHex) 21 #expect(retained.state == .unlocked) 22 #expect(try store.get(key) == legacy) 23 let recovered = try await custody.migrateLegacyIdentity(from: key, expectedPublicKeyHex: expected) 24 #expect(recovered.identity == retained.identity) 25 #expect(try store.get(key) == nil) 26 } 27 28 // The backing store synchronizes bytes; the lock protects the one-shot fault. 29 private final class CancelOnActiveReadStore: RadrootsSecureStore, @unchecked Sendable { 30 private let backing = RadrootsInMemorySecureStore() 31 private let lock = NSLock() 32 private var shouldCancel = true 33 34 func put(_ value: Data, for key: RadrootsSecureStoreKey, policy: RadrootsSecretAccessPolicy) throws { 35 try backing.put(value, for: key, policy: policy) 36 } 37 func contains(_ key: RadrootsSecureStoreKey) throws -> Bool { try backing.contains(key) } 38 func delete(_ key: RadrootsSecureStoreKey) throws { try backing.delete(key) } 39 func deleteNamespace(_ namespace: String) throws { try backing.deleteNamespace(namespace) } 40 func get(_ key: RadrootsSecureStoreKey) throws -> Data? { 41 let value = try backing.get(key) 42 lock.lock() 43 let cancel = key.name == "active_secret_v1" && shouldCancel 44 if cancel { shouldCancel = false } 45 lock.unlock() 46 if cancel { withUnsafeCurrentTask { $0?.cancel() } } 47 return value 48 } 49 }