TeraRecoveryCompletionTests.swift (7431B)
1 import Foundation 2 import RadrootsKit 3 @testable import TeraApp 4 import TeraKitBindings 5 import XCTest 6 7 final class TeraRecoveryCompletionTests: XCTestCase { 8 @MainActor 9 func testLegacyCompletionErrorsPreserveNativeEvidenceForExactRecovery() async throws { 10 let fixture = try BackgroundUploadFixture() 11 defer { fixture.remove() } 12 let backend = try TeraScopeBackend() 13 let client = try await TeraScopeFixtures.client(backend) 14 let transfer = BackgroundTransferHarness() 15 let opened = try await fixture.coordinator(transfer: transfer).open([fixture.media]) 16 defer { opened.close() } 17 let handle = try XCTUnwrap(opened.handles.first) 18 let media = AddMediaHarness() 19 let response = TeraAddBackgroundUploadReceipt(identifier: "retained", draftID: fixture.draftID, expectedRevision: 2, 20 statusCode: 200, mediaType: "application/json", contentEncoding: nil, body: Data("{}".utf8)) 21 for code in ["ios.runtime.timeout", "storage_failure", "invalid_native_upload_completion"] { 22 await backend.failLegacyCompletion(with: .local(operation: "test.legacy", code: code, safeMessage: "Unconfirmed completion.")) 23 do { 24 _ = try await TeraAddUploadCompletion.complete(response, handle: handle, media: media, runtimeClient: client) 25 XCTFail("Completion should fail without settling") 26 } catch {} 27 let settlements = await media.settlementValues() 28 XCTAssertEqual(settlements, []) 29 } 30 _ = try await client.stop() 31 } 32 33 @MainActor 34 func testPendingReceiptWaitsForDurableCompletionBeforeSettlement() async throws { 35 let fixture = try BackgroundUploadFixture() 36 defer { fixture.remove() } 37 let transfer = BackgroundTransferHarness() 38 let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) 39 try await transfer.seed(request: fixture.request(job: job), state: .awaitingVerification) 40 let backend = try TeraScopeBackend() 41 await backend.setDrafts([job.draft]) 42 let pause = ResourceTestPause() 43 await backend.setRecoveryCompletion { input, handle in 44 XCTAssertEqual(handle.media, fixture.media) 45 XCTAssertEqual(input.revision, 2) 46 XCTAssertEqual(input.attempt, job.operationID) 47 await pause.wait() 48 return Self.verified(input) 49 } 50 let client = try await TeraScopeFixtures.client(backend) 51 let coordinator = fixture.coordinator(transfer: transfer) 52 let task = Task { try await coordinator.recoverNativeUploads(client: client) } 53 await pause.entered.wait() 54 let pending = await transfer.counts 55 XCTAssertEqual(pending.acceptedSettlement, 0) 56 await pause.resume.open() 57 let progress = try await task.value 58 XCTAssertEqual(progress, .init(visited: 1, remaining: 0, needsAttention: false)) 59 let counts = await transfer.counts 60 XCTAssertEqual(counts.acceptedSettlement, 1) 61 XCTAssertEqual(counts.enqueue, 0) 62 XCTAssertEqual(counts.retry, 0) 63 _ = try await client.stop() 64 } 65 66 @MainActor 67 func testCancelledCompletionRetainsNativeReceiptAndRestartCompletesWithoutUpload() async throws { 68 let fixture = try BackgroundUploadFixture() 69 defer { fixture.remove() } 70 let transfer = BackgroundTransferHarness() 71 let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) 72 try await transfer.seed(request: fixture.request(job: job), state: .awaitingVerification) 73 let backend = try TeraScopeBackend() 74 await backend.setDrafts([job.draft]) 75 let pause = ResourceTestPause() 76 await backend.setRecoveryCompletion { input, _ in 77 await pause.wait() 78 return Self.verified(input) 79 } 80 let client = try await TeraScopeFixtures.client(backend) 81 let coordinator = fixture.coordinator(transfer: transfer) 82 let task = Task { try await coordinator.recoverNativeUploads(client: client) } 83 await pause.entered.wait() 84 task.cancel() 85 await pause.resume.open() 86 do { _ = try await task.value; XCTFail("Cancelled recovery must retain the receipt") } catch is CancellationError {} 87 let counts = await transfer.counts 88 XCTAssertEqual(counts.acceptedSettlement, 0) 89 await backend.setRecoveryCompletion { input, _ in Self.verified(input) } 90 let restarted = fixture.coordinator(transfer: transfer) 91 let progress = try await restarted.recoverNativeUploads(client: client) 92 XCTAssertFalse(progress.needsAttention) 93 let final = await transfer.counts 94 XCTAssertEqual(final.acceptedSettlement, 1) 95 XCTAssertEqual(final.enqueue, 0) 96 XCTAssertEqual(final.retry, 0) 97 _ = try await client.stop() 98 } 99 100 @MainActor 101 func testUnconfirmedRustReceiptCannotSettleNativeEvidence() async throws { 102 let fixture = try BackgroundUploadFixture() 103 defer { fixture.remove() } 104 let transfer = BackgroundTransferHarness() 105 let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) 106 try await transfer.seed(request: fixture.request(job: job), state: .awaitingVerification) 107 let backend = try TeraScopeBackend() 108 await backend.setDrafts([job.draft]) 109 await backend.setRecoveryCompletion { input, _ in 110 .init(parent: String(repeating: "b", count: 32), attempt: input.attempt, canonicalURL: input.media.remoteURL ?? "", 111 sha256: input.media.sha256, mediaType: input.media.mediaType, byteSize: input.media.byteSize, verifiedAtUnixMS: 1) 112 } 113 let client = try await TeraScopeFixtures.client(backend) 114 let result = try await fixture.coordinator(transfer: transfer).recoverNativeUploads(client: client) 115 XCTAssertTrue(result.needsAttention) 116 let counts = await transfer.counts 117 XCTAssertEqual(counts.acceptedSettlement, 0) 118 XCTAssertEqual(counts.cancel, 0) 119 _ = try await client.stop() 120 } 121 122 func testGeneratedCompletionRejectsUnsupportedNativeEvidenceBeforeMediaConversion() async throws { 123 let fixture = try MediaOwnershipFixture() 124 defer { fixture.remove() } 125 let runtime = try await fixture.runtime() 126 let opened = try TeraMediaFileFixture.open([fixture.media], bytes: fixture.bytes) 127 defer { opened.close() } 128 var media = try XCTUnwrap(opened.handles.first).generatedValue 129 media.schemaVersion = 0 // If converted first, this would return a different error. 130 let receipt = FfiRecoveryUploadReceipt(schemaVersion: 2, parent: String(repeating: "1", count: 32), revision: 2, 131 attempt: String(repeating: "a", count: 32), uploadUrl: "http://127.0.0.1:3000/upload", 132 sha256: fixture.media.sha256, mediaType: "image/png", byteSize: fixture.media.byteSize, 133 response: .init(schemaVersion: 1, statusCode: 200, mediaType: "application/json", contentEncoding: nil, body: Data("{}".utf8))) 134 do { 135 _ = try await runtime.recoverNativeUpload(receipt: receipt, media: media) 136 XCTFail("Unsupported receipt must fail") 137 } catch let TeraAppError.Failure(report) { 138 XCTAssertEqual(report.code, "invalid_recovery_receipt") 139 } 140 _ = try await runtime.shutdown() 141 } 142 143 private static func verified(_ input: TeraRecoveryUploadReceipt) -> TeraRecoveryCompletionReceipt { 144 .init(parent: input.parent, attempt: input.attempt, canonicalURL: input.media.remoteURL ?? "", 145 sha256: input.media.sha256, mediaType: input.media.mediaType, byteSize: input.media.byteSize, verifiedAtUnixMS: 1_800_000_000_000) 146 } 147 }