TeraIdentityCancellationTests.swift (2911B)
1 import Foundation 2 import RadrootsKit 3 @testable import TeraApp 4 import XCTest 5 6 final class TeraIdentityCancellationTests: XCTestCase { 7 func testCancelledHostImportCannotInstallAfterSuccessfulPresenceCallback() async throws { 8 let secure = InMemorySecureStore() 9 let presence = IdentityCancellationPresence() 10 let custody = try RadrootsIdentityCustody( 11 configuration: RadrootsIdentityCustodyConfiguration(namespace: "radroots_identity_v1", secretPolicy: .secureLocalSecret), 12 secureStore: secure, metadataStore: InMemoryIdentityMetadataStore(), userPresence: presence 13 ) 14 let prefix = "tera.identity.cancellation.\(UUID().uuidString.lowercased())" 15 let defaults = try XCTUnwrap(UserDefaults(suiteName: prefix)) 16 defer { UserDefaults(suiteName: prefix)?.removePersistentDomain(forName: prefix) } 17 let store = TeraIdentityStore(custody: custody, secureStore: secure, servicePrefix: prefix, userDefaults: defaults) 18 let task = Task { 19 try await store.importIdentity(RadrootsIdentitySecretMaterial(rawRepresentation: Data(repeating: 1, count: 32))) 20 } 21 for await _ in presence.entered { 22 break 23 } 24 task.cancel() 25 await presence.release() 26 do { 27 _ = try await task.value 28 XCTFail("Cancellation must reject a later successful presence callback") 29 } catch { 30 XCTAssertEqual(error as? RadrootsIdentityCustodyError, .cancelled) 31 } 32 let state = await store.snapshot() 33 XCTAssertEqual(state.state, .absent) 34 XCTAssertFalse(try secure.contains(RadrootsSecureStoreKey(namespace: "radroots_identity_v1", name: "active_secret_v1"))) 35 } 36 37 func testCustodyErrorPresentationDoesNotExposeSecretOrInternalText() { 38 let sentinel = "synthetic-secret-sentinel" 39 let error = TeraIdentityStoreError.custody(sentinel) 40 XCTAssertFalse(error.localizedDescription.contains(sentinel)) 41 XCTAssertEqual(error.localizedDescription, "The local identity needs attention before Tera can continue.") 42 } 43 } 44 45 private actor IdentityCancellationPresence: RadrootsUserPresence { 46 nonisolated let entered: AsyncStream<Void> 47 private let signal: AsyncStream<Void>.Continuation 48 private var pending: CheckedContinuation<Void, Never>? 49 init() { 50 (entered, signal) = AsyncStream.makeStream() 51 } 52 53 func currentStatus() async throws -> RadrootsUserPresenceStatus { 54 .unavailable 55 } 56 57 func release() { 58 pending?.resume() 59 pending = nil 60 } 61 62 func verify(_ request: RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult { 63 await withCheckedContinuation { continuation in 64 pending = continuation 65 signal.yield(()) 66 } 67 return RadrootsUserPresenceResult(policy: request.policy, verified: true) 68 } 69 }