TeraSessionResourceTests.swift (5737B)
1 import Foundation 2 import RadrootsKit 3 @testable import TeraApp 4 import XCTest 5 6 final class TeraSessionResourceTests: XCTestCase { 7 @MainActor 8 func testAppModelStartsBothProductStoresAfterSessionAuthorization() async throws { 9 let fixture = try await Fixture() 10 defer { fixture.state.remove() } 11 let model = TeraAppModel( 12 sessionStore: fixture.session, runtimeClient: fixture.client, 13 lifecycleCoordinator: .disabled() 14 ) 15 let stores = try XCTUnwrap(model.productStores) 16 XCTAssertEqual(stores.add.state, .idle) 17 XCTAssertNil(stores.today.presentation.readFailure) 18 await model.start() 19 if case .running = model.phase {} else { 20 XCTFail("The authorized session must run") 21 } 22 // This session fixture deliberately rejects product queries. Observing both 23 // typed failures proves launch dispatched them without another resume event. 24 await TeraScopeFixtures.eventually { 25 if case .failed = stores.add.state { 26 return stores.today.presentation.readFailure != nil 27 } 28 return false 29 } 30 await model.stop() 31 XCTAssertEqual(stores.today.observationState, .stopped) 32 XCTAssertEqual(stores.add.observationState, .stopped) 33 } 34 35 @MainActor 36 func testAppModelDoesNotStartProductStoresBeforeIdentityAuthorization() async throws { 37 let fixture = try await Fixture(createIdentity: false) 38 defer { fixture.state.remove() } 39 let model = TeraAppModel( 40 sessionStore: fixture.session, runtimeClient: fixture.client, 41 lifecycleCoordinator: .disabled() 42 ) 43 let stores = try XCTUnwrap(model.productStores) 44 await model.start() 45 XCTAssertEqual(model.phase, .identityRequired) 46 XCTAssertEqual(stores.add.state, .idle) 47 XCTAssertTrue(stores.add.schemas.isEmpty) 48 XCTAssertTrue(stores.today.cards.isEmpty) 49 XCTAssertNil(stores.today.presentation.readFailure) 50 XCTAssertEqual(stores.today.observationState, .stopped) 51 XCTAssertEqual(stores.add.observationState, .stopped) 52 await model.stop() 53 } 54 55 func testStaleStartupSnapshotDoesNotStopNewSharedSession() async throws { 56 let fixture = try await Fixture() 57 defer { fixture.state.remove() } 58 let initial = await fixture.session.start() 59 guard case .running = initial else { return XCTFail("Initial session must run") } 60 let pause = ResourceTestPause() 61 await fixture.backend.pauseSnapshot(pause) 62 let old = Task { await fixture.session.start() } 63 await pause.entered.wait() 64 let current = await fixture.session.start() 65 guard case .running = current else { return XCTFail("New session must adopt the shared runtime") } 66 let commands = await fixture.backend.identityCommands 67 await pause.resume.open() 68 _ = await old.value 69 try await assertRunning(fixture, phase: current) 70 let after = await fixture.backend.identityCommands 71 XCTAssertEqual(after, commands, "Superseded reconciliation cannot issue identity commands in the live session") 72 _ = await fixture.session.stop() 73 } 74 75 func testStaleIdentityReconciliationDoesNotStopNewSharedSession() async throws { 76 let fixture = try await Fixture() 77 defer { fixture.state.remove() } 78 let pause = ResourceTestPause() 79 await fixture.backend.pauseSettings(pause) 80 let old = Task { await fixture.session.start() } 81 await pause.entered.wait() 82 let current = await fixture.session.start() 83 guard case .running = current else { return XCTFail("New session must adopt the shared runtime") } 84 let commands = await fixture.backend.identityCommands 85 await pause.resume.open() 86 _ = await old.value 87 try await assertRunning(fixture, phase: current) 88 let after = await fixture.backend.identityCommands 89 XCTAssertEqual(after, commands, "Superseded reconciliation cannot issue identity commands in the live session") 90 _ = await fixture.session.stop() 91 } 92 93 private func assertRunning(_ fixture: Fixture, phase: TeraSessionPhase) async throws { 94 let observed = await fixture.session.currentPhase() 95 let closes = await fixture.backend.shutdownCount 96 let snapshot = try await fixture.client.snapshot() 97 XCTAssertEqual(observed, phase) 98 XCTAssertFalse(snapshot.isClosed) 99 XCTAssertEqual(closes, 0) 100 } 101 102 private struct Fixture { 103 let state: StateFixture 104 let backend: ResourceTestBackend 105 let client: TeraRuntimeClient 106 let session: TeraSessionStore 107 108 init(createIdentity: Bool = true) async throws { 109 state = try StateFixture() 110 let secure = InMemorySecureStore() 111 let custody = try RadrootsIdentityCustody( 112 configuration: RadrootsIdentityCustodyConfiguration( 113 namespace: "radroots_identity_v1", secretPolicy: .secureLocalSecret 114 ), 115 secureStore: secure, metadataStore: InMemoryIdentityMetadataStore(), 116 userPresence: AllowingUserPresence() 117 ) 118 let identity = TeraIdentityStore( 119 custody: custody, secureStore: secure, 120 servicePrefix: "org.tera.tests.session.\(UUID().uuidString.lowercased())" 121 ) 122 let publicKey: String 123 if createIdentity { 124 let created = try await identity.create() 125 publicKey = try XCTUnwrap(created.publicKeyHex) 126 } else { 127 publicKey = String(repeating: "ab", count: 32) 128 } 129 let backend = ResourceTestBackend(publicKeyHex: publicKey) 130 self.backend = backend 131 let client = TeraRuntimeClient(factory: { _ in await backend.start() }) 132 self.client = client 133 session = TeraSessionStore( 134 configurationStore: TeraConfigurationStore(bootstrap: state.bootstrap, roots: state.roots), 135 identityStore: identity, runtimeClient: client, roots: state.roots, 136 protectedData: TeraProtectedDataMonitor(available: true) 137 ) 138 } 139 } 140 }