field_ios

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

TeraLateSigningTests.swift (12555B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import TeraKitBindings
      5 import XCTest
      6 
      7 @MainActor
      8 final class TeraLateSigningTests: XCTestCase {
      9   func testDurableStopCrossesGeneratedBridgeWhileSignerOwnsAdmission() async throws {
     10     let fixture = try MediaOwnershipFixture()
     11     defer { fixture.remove() }
     12     let signer = try await LateNativeSigner.make()
     13     let client = client(clock: LateSigningClock())
     14     let configuration = configuration(fixture, signer: signer)
     15     _ = try await client.start(configuration: configuration)
     16     var form = TeraAddForm.empty(.createUpdate)
     17     form.content = "Original stopped native capture"
     18     let source = try await client.saveComposer(request: TeraComposerSaveRequest(
     19       scope: scope, id: client.reserveComposerID(), expectedRevision: nil, editSequence: 1, form: TeraComposerForm(editing: form)
     20     ))
     21     let request = try await TeraSubmissionRequest(commandID: client.reserveSubmissionID(), scope: scope,
     22                                                   composerID: source.draft.id, expectedRevision: 1)
     23     let prepared = try await client.prepareSubmission(request: request, media: [])
     24     let waiting = Task { try? await client.advanceSubmission(request: request, expectedRevision: prepared.revision) }
     25     await signer.pause.entered.wait()
     26     let stopped = try await client.requestSubmissionStop(request: request)
     27     XCTAssertTrue(stopped.delivery.isStopped)
     28     XCTAssertEqual(stopped.delivery.state, .notIssued)
     29     let replay = try await client.requestSubmissionStop(request: request)
     30     XCTAssertEqual(replay, stopped)
     31     let whileHeld = try await client.submissionStatus(request: request)
     32     XCTAssertEqual(whileHeld, stopped)
     33     await signer.pause.resume.open()
     34     _ = await waiting.value
     35     let retained = try await signedStatus(client, request: request)
     36     XCTAssertEqual(retained.delivery, stopped.delivery)
     37     XCTAssertEqual(retained.settlement.signed, 1)
     38     XCTAssertEqual(retained.settlement.admitted, 0)
     39     XCTAssertEqual(retained.state, .cancelled)
     40     let context = TeraLocalNetwork(schemaVersion: 1, id: scope.localNetworkID, label: "Nearby",
     41                                    relayURLs: ["ws://127.0.0.1:19999"], locality: nil, followedAuthors: [], generation: 1)
     42     let local = try await client.reconcileSubmissionLocal(request: request, context: context)
     43     XCTAssertEqual(local, retained)
     44     XCTAssertEqual(local.settlement.admitted, 0)
     45     _ = try await client.stop()
     46     await signer.disable()
     47     _ = try await client.start(configuration: configuration)
     48     let reopened = try await client.submissionStatus(request: request)
     49     XCTAssertEqual(reopened, retained)
     50     do {
     51       _ = try await client.advanceSubmission(request: request, expectedRevision: retained.revision)
     52       XCTFail("A retained stop cannot authorize another effect")
     53     } catch { XCTAssertEqual(TeraAddPresentation.failure(for: error)?.code, "submission_stopped") }
     54     let count = await signer.count()
     55     XCTAssertEqual(count, 1)
     56     _ = try await client.stop()
     57   }
     58 
     59   func testCancelledNativeWaitRetainsLateSignatureAcrossRestart() async throws {
     60     try await checkLateSigning(cancelWait: true, failHostClock: false)
     61   }
     62 
     63   func testNativeDeadlineAndMissingHostClockRetainLateSignatureAcrossRestart() async throws {
     64     try await checkLateSigning(cancelWait: false, failHostClock: true)
     65   }
     66 
     67   private func checkLateSigning(cancelWait: Bool, failHostClock: Bool) async throws {
     68     let fixture = try MediaOwnershipFixture()
     69     defer { fixture.remove() }
     70     let signer = try await LateNativeSigner.make()
     71     let clock = LateSigningClock()
     72     let client = client(clock: clock)
     73     let configuration = configuration(fixture, signer: signer)
     74     _ = try await client.start(configuration: configuration)
     75     var form = TeraAddForm.empty(.createUpdate)
     76     form.content = "PRIVATE original native signing capture"
     77     let source = try await client.saveComposer(request: TeraComposerSaveRequest(
     78       scope: scope, id: client.reserveComposerID(), expectedRevision: nil, editSequence: 1,
     79       form: TeraComposerForm(editing: form)
     80     ))
     81     let request = try await TeraSubmissionRequest(commandID: client.reserveSubmissionID(), scope: scope,
     82                                                   composerID: source.draft.id, expectedRevision: 1)
     83     let prepared = try await client.prepareSubmission(request: request, media: [])
     84     let waiting = Task {
     85       do {
     86         _ = try await client.advanceSubmission(request: request, expectedRevision: prepared.revision)
     87         return true
     88       } catch { return false }
     89     }
     90     await signer.pause.entered.wait()
     91     if cancelWait {
     92       waiting.cancel()
     93     }
     94     let returnedSuccess = await waiting.value
     95     XCTAssertFalse(returnedSuccess)
     96     let pending = try await client.submissionStatus(request: request)
     97     XCTAssertEqual(pending.captured, prepared.captured)
     98     XCTAssertEqual(pending.settlement.signed, 0)
     99     do {
    100       _ = try await client.advanceSubmission(request: request, expectedRevision: pending.revision)
    101       XCTFail("The original native callback still owns admission")
    102     } catch {
    103       XCTAssertEqual(TeraAddPresentation.failure(for: error)?.code, "operation_in_progress")
    104     }
    105     try await passSigningDeadline(signer)
    106     if failHostClock {
    107       clock.fail()
    108     }
    109     await signer.pause.resume.open()
    110     let retained = try await signedStatus(client, request: request)
    111     XCTAssertEqual(retained.captured, prepared.captured)
    112     XCTAssertEqual(retained.operationID, prepared.operationID)
    113     XCTAssertEqual(retained.settlement.signed, 1)
    114     XCTAssertEqual(retained.settlement.admitted, 0)
    115     XCTAssertEqual(retained.settlement.deliverySatisfied, 0)
    116     try await checkReconstruction(client, signer: signer, configuration: configuration, request: request, retained: retained)
    117   }
    118 
    119   private func passSigningDeadline(_ signer: LateNativeSigner) async throws {
    120     let capturedDeadline = await signer.deadline()
    121     let deadline = try XCTUnwrap(capturedDeadline)
    122     let now = try TeraClock.system.unixMilliseconds(requirePositive: true)
    123     let remaining = deadline > now ? deadline - now : 0
    124     XCTAssertLessThanOrEqual(remaining, 30000)
    125     try await Task.sleep(nanoseconds: (remaining + 20) * 1_000_000)
    126   }
    127 
    128   private func client(clock: LateSigningClock) -> TeraRuntimeClient {
    129     TeraRuntimeClient(factory: { configuration in
    130       let runtime = try await TeraRuntime.withHostSigner(
    131         applicationSupportDirectory: configuration.applicationSupportDirectory,
    132         publicKeyHex: configuration.publicKeyHex,
    133         sourceGenerationHex: configuration.sourceGenerationHex,
    134         sourceGenerationCreatedAtUnixMs: configuration.sourceGenerationCreatedAtUnixMilliseconds,
    135         protectedData: .available,
    136         hostSigner: TeraGeneratedHostSigner(signer: configuration.signer, clock: TeraClock(now: { clock.now() }))
    137       )
    138       try await runtime.configureSimulatorRelays(loopbackRelays: ["ws://127.0.0.1:19999"])
    139       let backend = TeraGeneratedRuntimeBackend(runtime: runtime)
    140       return try await TeraRuntimeBackendStart(backend: backend, snapshot: backend.snapshot())
    141     }, deadlines: TeraRuntimeDeadlinePolicy(operationNanoseconds: 3_000_000_000))
    142   }
    143 
    144   private func checkReconstruction(
    145     _ client: TeraRuntimeClient, signer: LateNativeSigner, configuration: TeraRuntimeLaunchConfiguration,
    146     request: TeraSubmissionRequest, retained: TeraSubmissionStatus
    147   ) async throws {
    148     _ = try await client.stop()
    149     await signer.disable()
    150     _ = try await client.start(configuration: configuration)
    151     let recovered = try await client.recoverSubmission(request: request)
    152     XCTAssertEqual(recovered, retained)
    153     try await checkLocalReconciliation(client, request: request, retained: retained)
    154     let count = await signer.count()
    155     XCTAssertEqual(count, 1)
    156     _ = try await client.stop()
    157   }
    158 
    159   private func checkLocalReconciliation(_ client: TeraRuntimeClient, request: TeraSubmissionRequest, retained: TeraSubmissionStatus) async throws {
    160     let context = TeraLocalNetwork(schemaVersion: 1, id: scope.localNetworkID, label: "Nearby",
    161                                    relayURLs: ["ws://127.0.0.1:19999"], locality: nil, followedAuthors: [], generation: 1)
    162     let local = try await client.reconcileSubmissionLocal(request: request, context: context)
    163     XCTAssertEqual(local.captured, retained.captured)
    164     XCTAssertEqual(local.operationID, retained.operationID)
    165     XCTAssertEqual(local.settlement.signed, 1)
    166     XCTAssertEqual(local.settlement.admitted, 1)
    167     XCTAssertEqual(local.settlement.deliverySatisfied, 0)
    168     let repeated = try await client.reconcileSubmissionLocal(request: request, context: context)
    169     XCTAssertEqual(repeated, local)
    170     let page = try await client.todayPage(request: .first(context: context, limit: 20,
    171                                                           asOfUnixSeconds: TeraClock.system.unixMilliseconds(requirePositive: true) / 1000))
    172     XCTAssertEqual(page.items.count, 1)
    173     XCTAssertEqual(page.items.first?.localOperationID, local.operationID)
    174     XCTAssertNotEqual(page.items.first?.localOperationState, "complete")
    175   }
    176 
    177   private func signedStatus(_ client: TeraRuntimeClient, request: TeraSubmissionRequest) async throws -> TeraSubmissionStatus {
    178     for _ in 0 ..< 100 {
    179       let status = try await client.submissionStatus(request: request)
    180       if status.settlement.signed == 1 {
    181         return status
    182       }
    183       try await Task.sleep(nanoseconds: 20_000_000)
    184     }
    185     XCTFail("The generated bridge did not retain the already-created signature")
    186     return try await client.submissionStatus(request: request)
    187   }
    188 
    189   private var scope: TeraComposerScope {
    190     TeraComposerScope(authorPublicKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", localNetworkID: "nearby")
    191   }
    192 
    193   private func configuration(_ fixture: MediaOwnershipFixture, signer: LateNativeSigner) -> TeraRuntimeLaunchConfiguration {
    194     TeraRuntimeLaunchConfiguration(applicationSupportDirectory: fixture.root.path, publicKeyHex: scope.authorPublicKey,
    195                                    sourceGenerationHex: String(repeating: "04", count: 32), sourceGenerationCreatedAtUnixMilliseconds: 1_800_000_000_000,
    196                                    protectedData: .available, networkProfile: .publicNetwork, writableRelays: [], blossom: nil,
    197                                    app: TeraRuntimeAppMetadata(bundleIdentifier: "test.late-signing", version: "1", buildNumber: "1", buildSHA: nil),
    198                                    signerGeneration: "late-signing-test", signer: signer, adoptBootstrapSettings: false)
    199   }
    200 }
    201 
    202 private actor LateNativeSigner: TeraRuntimeSigner {
    203   let pause = ResourceTestPause()
    204   private let signer: any TeraRuntimeSigner
    205   private var calls = 0
    206   private var lastDeadline: UInt64?
    207   private var available = true
    208 
    209   init(signer: any TeraRuntimeSigner) {
    210     self.signer = signer
    211   }
    212 
    213   static func make() async throws -> LateNativeSigner {
    214     let secureStore = InMemorySecureStore()
    215     let custody = try RadrootsIdentityCustody(configuration: RadrootsIdentityCustodyConfiguration(
    216       namespace: "radroots_identity_v1", secretPolicy: .secureLocalSecret
    217     ), secureStore: secureStore, metadataStore: InMemoryIdentityMetadataStore(), userPresence: AllowingUserPresence())
    218     let store = TeraIdentityStore(custody: custody, secureStore: secureStore,
    219                                   servicePrefix: "org.tera.tests.late_signing.\(UUID().uuidString)")
    220     let identity = try await store.importIdentity(RadrootsIdentitySecretMaterial(importText: String(repeating: "0", count: 63) + "1"))
    221     return try await LateNativeSigner(signer: store.signer(for: identity))
    222   }
    223 
    224   func availability() async -> TeraRuntimeSignerAvailability {
    225     available ? .ready : .unavailable
    226   }
    227 
    228   func count() -> Int {
    229     calls
    230   }
    231 
    232   func deadline() -> UInt64? {
    233     lastDeadline
    234   }
    235 
    236   func disable() {
    237     available = false
    238   }
    239 
    240   func sign(_ request: TeraRuntimeSigningRequest) async -> TeraRuntimeSigningOutcome {
    241     calls += 1
    242     guard available else { return .unavailable }
    243     lastDeadline = request.deadlineUnixMilliseconds
    244     let result = await signer.sign(request)
    245     await pause.wait()
    246     return result
    247   }
    248 }
    249 
    250 private final class LateSigningClock: @unchecked Sendable {
    251   private let lock = NSLock()
    252   private var failed = false
    253   func fail() {
    254     lock.withLock { failed = true }
    255   }
    256 
    257   func now() -> Date {
    258     lock.withLock { failed ? Date(timeIntervalSince1970: .nan) : Date() }
    259   }
    260 }