TeraSubmissionReservationTests.swift (7225B)
1 import Foundation 2 @testable import TeraApp 3 import TeraKitBindings 4 import XCTest 5 6 final class TeraSubmissionReservationTests: XCTestCase { 7 private let scope = TeraComposerScope( 8 authorPublicKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", localNetworkID: "nearby" 9 ) 10 11 func testProductionReservationSurvivesSessionChangeAndNewerEditingWithoutSigning() async throws { 12 let fixture = try MediaOwnershipFixture() 13 defer { fixture.remove() } 14 let signer = ComposerForbiddenSigner() 15 let client = TeraRuntimeClient.production() 16 let configuration = configuration(fixture, signer: signer) 17 _ = try await client.start(configuration: configuration) 18 let firstLifecycle = await client.lifecycle() 19 let id = try await client.reserveComposerID() 20 var form = TeraComposerForm(commandType: .createEvent) 21 form.content = "PRIVATE unfinished café" 22 form.eventStartDate = "2026-09-" 23 let saved = try await client.saveComposer(request: TeraComposerSaveRequest( 24 scope: scope, id: id, expectedRevision: nil, editSequence: 1, form: form 25 )) 26 let commandID = try await client.reserveSubmissionID() 27 let request = TeraSubmissionRequest(commandID: commandID, scope: scope, composerID: id, expectedRevision: 1) 28 let first = try await client.reserveSubmission(request: request) 29 XCTAssertEqual(first.captured, saved.draft) 30 XCTAssertFalse(first.replayed) 31 form.content = "later editing" 32 let latest = try await client.saveComposer(request: TeraComposerSaveRequest( 33 scope: scope, id: id, expectedRevision: 1, editSequence: 2, form: form 34 )) 35 _ = try await client.stop() 36 _ = try await client.start(configuration: configuration) 37 let secondLifecycle = await client.lifecycle() 38 XCTAssertNotEqual(firstLifecycle, secondLifecycle) 39 let replay = try await client.reserveSubmission(request: request) 40 XCTAssertTrue(replay.replayed) 41 XCTAssertEqual(replay.captured, saved.draft) 42 XCTAssertEqual(replay.reservationID, first.reservationID) 43 XCTAssertEqual(replay.reservedAtUnixMilliseconds, first.reservedAtUnixMilliseconds) 44 let current = try await client.loadComposer(scope: scope, id: id) 45 XCTAssertEqual(current, latest.draft) 46 let operations = try await client.draftHeads() 47 XCTAssertTrue(operations.isEmpty) 48 _ = try await client.stop() 49 let calls = await signer.requests 50 XCTAssertEqual(calls, 0) 51 } 52 53 func testGeneratedReservationsReuseDuplicatesAndRejectChangedSources() async throws { 54 let fixture = try MediaOwnershipFixture() 55 defer { fixture.remove() } 56 let runtime = try await fixture.runtime() 57 let id = try composerReserveId().id 58 let form = TeraComposerForm(commandType: .createUpdate) 59 let saved = try await runtime.composerSave(request: TeraComposerSaveRequest( 60 scope: scope, id: id, expectedRevision: nil, editSequence: 1, form: form 61 ).generatedValue) 62 let request = try FfiSubmissionReservationRequest(schemaVersion: 1, commandId: submissionReserveId().id, 63 scope: scope.generatedValue, composerId: id, expectedRevision: 1) 64 async let left = runtime.submissionReserve(request: request) 65 async let right = runtime.submissionReserve(request: request) 66 let (first, replay) = try await (left, right) 67 XCTAssertEqual(first.reservationId, replay.reservationId) 68 XCTAssertNotEqual(first.replayed, replay.replayed) 69 XCTAssertEqual(first.captured, saved.draft) 70 var changed = request 71 changed.expectedRevision = 2 72 try await assertFailure(runtime, request: changed, code: "idempotency_conflict") 73 changed = request 74 changed.scope.localNetworkId = "elsewhere" 75 try await assertFailure(runtime, request: changed, code: "idempotency_conflict") 76 changed = request 77 changed.commandId = try submissionReserveId().id 78 let intentional = try await runtime.submissionReserve(request: changed) 79 XCTAssertEqual(intentional.captured, first.captured) 80 XCTAssertNotEqual(intentional.reservationId, first.reservationId) 81 _ = try await runtime.shutdown() 82 } 83 84 func testGeneratedVersionAndIdentityFailuresAndUnconfirmedReceiptsFailClosed() async throws { 85 let fixture = try MediaOwnershipFixture() 86 defer { fixture.remove() } 87 let runtime = try await fixture.runtime() 88 let id = try composerReserveId().id 89 let saved = try await runtime.composerSave(request: TeraComposerSaveRequest( 90 scope: scope, id: id, expectedRevision: nil, editSequence: 1, form: TeraComposerForm(commandType: .createUpdate) 91 ).generatedValue) 92 let request = try FfiSubmissionReservationRequest(schemaVersion: 1, commandId: submissionReserveId().id, 93 scope: scope.generatedValue, composerId: id, expectedRevision: 1) 94 var invalid = request 95 invalid.schemaVersion = 2 96 try await assertFailure(runtime, request: invalid, code: "submission_schema_unsupported") 97 invalid = request 98 invalid.scope.schemaVersion = 2 99 try await assertFailure(runtime, request: invalid, code: "composer_schema_unsupported") 100 invalid = request 101 invalid.commandId = String(repeating: "0", count: 32) 102 try await assertFailure(runtime, request: invalid, code: "submission_command_id_invalid") 103 let receipt = try await runtime.submissionReserve(request: request) 104 let input = TeraSubmissionRequest(commandID: request.commandId, scope: scope, composerID: id, expectedRevision: 1) 105 XCTAssertEqual(try TeraGeneratedSubmission.translate(receipt, request: input).captured, try saved.draft.composerAppValue) 106 var wrong = receipt 107 wrong.captured.revision = 2 108 XCTAssertThrowsError(try TeraGeneratedSubmission.translate(wrong, request: input)) 109 wrong = receipt 110 wrong.schemaVersion = 2 111 XCTAssertThrowsError(try TeraGeneratedSubmission.translate(wrong, request: input)) 112 wrong = receipt 113 wrong.reservationId = String(repeating: "0", count: 32) 114 XCTAssertThrowsError(try TeraGeneratedSubmission.translate(wrong, request: input)) 115 _ = try await runtime.shutdown() 116 } 117 118 private func assertFailure(_ runtime: TeraRuntime, request: FfiSubmissionReservationRequest, code: String) async throws { 119 do { 120 _ = try await runtime.submissionReserve(request: request) 121 XCTFail("Invalid reservation must not be acknowledged.") 122 } catch let TeraAppError.Failure(report) { 123 XCTAssertEqual(report.code, code) 124 XCTAssertFalse(report.retryable) 125 } 126 } 127 128 private func configuration(_ fixture: MediaOwnershipFixture, signer: ComposerForbiddenSigner) -> TeraRuntimeLaunchConfiguration { 129 TeraRuntimeLaunchConfiguration( 130 applicationSupportDirectory: fixture.root.path, publicKeyHex: scope.authorPublicKey, 131 sourceGenerationHex: String(repeating: "04", count: 32), sourceGenerationCreatedAtUnixMilliseconds: 1_800_000_000_000, 132 protectedData: .available, networkProfile: .publicNetwork, writableRelays: ["wss://relay.example"], blossom: nil, 133 app: TeraRuntimeAppMetadata(bundleIdentifier: "test.submission", version: "1", buildNumber: "1", buildSHA: nil), 134 signerGeneration: "submission-test", signer: signer, adoptBootstrapSettings: false 135 ) 136 } 137 }