field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }