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 }