field_ios

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

TeraRecoverySettlementTests.swift (7059B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import XCTest
      5 
      6 /// Fixture evidence only. Production obtains this receipt from Rust.
      7 enum RecoverySettlementFixture {
      8   static func receipt(_ input: TeraRecoveryUploadReceipt) -> TeraRecoveryCompletionReceipt {
      9     .init(parent: input.parent, attempt: input.attempt, canonicalURL: input.media.remoteURL ?? "",
     10           sha256: input.media.sha256, mediaType: input.media.mediaType, byteSize: input.media.byteSize, verifiedAtUnixMS: 1_800_000_000_000)
     11   }
     12 
     13   static func run(transfer: BackgroundTransferHarness, cursor: String?,
     14                   lookup: @Sendable (String) async throws -> TeraNativeUploadRecoveryOwner?) async throws
     15     -> (progress: TeraNativeRecoveryProgress, cursor: String?)
     16   {
     17     try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: cursor, complete: { snapshot, owner in
     18       let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: owner)
     19       try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: receipt(input), transfer: transfer)
     20     }, lookup: lookup)
     21   }
     22 }
     23 
     24 final class TeraRecoverySettlementTests: XCTestCase {
     25   @MainActor
     26   func testVerifiedRustDraftReconcilesAwaitingReceiptAfterRelaunch() async throws {
     27     for legacyPath in [false, true] {
     28     let fixture = try BackgroundUploadFixture()
     29     defer { fixture.remove() }
     30     let transfer = BackgroundTransferHarness()
     31     let coordinator = fixture.coordinator(transfer: transfer)
     32     let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32))
     33     try await transfer.seed(request: fixture.request(job: job, remoteURL: legacyPath ? job.remoteURL : nil),
     34                             state: .awaitingVerification)
     35 
     36     let backend = try TeraScopeBackend()
     37     let draft = fixture.draft(revision: 3, stage: .verified)
     38     await backend.setDrafts([draft])
     39     await backend.setRecoveryCompletion { input, _ in
     40       XCTAssertEqual(input.revision, 2)
     41       XCTAssertEqual(input.uploadURL, legacyPath ? job.remoteURL : job.uploadURL)
     42       return RecoverySettlementFixture.receipt(input)
     43     }
     44     let client = try await TeraScopeFixtures.client(backend)
     45     try await coordinator.reconcileBackgroundUploads(drafts: [draft], client: client)
     46     _ = try await client.stop()
     47 
     48     let counts = await transfer.counts
     49     let state = await transfer.state
     50     XCTAssertEqual(counts.acceptedSettlement, 1)
     51     XCTAssertEqual(state, .completed)
     52     }
     53   }
     54 
     55   @MainActor
     56   func testDuplicateDisplayHintsCannotDuplicateSettlement() async throws {
     57     let fixture = try BackgroundUploadFixture()
     58     defer { fixture.remove() }
     59     let transfer = BackgroundTransferHarness()
     60     let coordinator = fixture.coordinator(transfer: transfer)
     61     let draft = fixture.draft(revision: 3, stage: .verified)
     62     try await transfer.seed(request: fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))), state: .awaitingVerification)
     63     let backend = try TeraScopeBackend()
     64     await backend.setDrafts([draft])
     65     await backend.setRecoveryCompletion { input, _ in RecoverySettlementFixture.receipt(input) }
     66     let client = try await TeraScopeFixtures.client(backend)
     67     try await coordinator.reconcileBackgroundUploads(drafts: [draft, draft], client: client)
     68     let counts = await transfer.counts
     69     XCTAssertEqual(counts.acceptedSettlement, 1)
     70     _ = try await client.stop()
     71   }
     72 
     73   func testDuplicateAndLostSettlementReplyConvergeWithoutAnotherWrite() async throws {
     74     for lostReply in [false, true] {
     75       let fixture = try BackgroundUploadFixture()
     76       defer { fixture.remove() }
     77       let transfer = BackgroundTransferHarness()
     78       let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
     79       try await transfer.seed(request: request, state: .awaitingVerification)
     80       let value = try await transfer.snapshot(for: request.identifier)
     81       let snapshot = try XCTUnwrap(value)
     82       let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: .init(draft: fixture.draft(revision: 3, stage: .verified)))
     83       if lostReply {
     84         await transfer.failNextSettlement(afterWrite: true)
     85       }
     86       for _ in 0 ..< 2 {
     87         try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer)
     88       }
     89       let counts = await transfer.counts
     90       XCTAssertEqual(counts.acceptedSettlement, 1)
     91       XCTAssertEqual(counts.enqueue, 0)
     92       XCTAssertEqual(counts.retry, 0)
     93     }
     94   }
     95 
     96   func testUnconfirmedSettlementRetainsReceiptAndCanResume() async throws {
     97     let fixture = try BackgroundUploadFixture()
     98     defer { fixture.remove() }
     99     let transfer = BackgroundTransferHarness()
    100     let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
    101     try await transfer.seed(request: request, state: .awaitingVerification)
    102     let value = try await transfer.snapshot(for: request.identifier)
    103     let snapshot = try XCTUnwrap(value)
    104     let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: .init(draft: fixture.draft(revision: 3, stage: .verified)))
    105     await transfer.failNextSettlement(afterWrite: false)
    106     do {
    107       try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer)
    108       XCTFail("A lost reply without completed native state cannot succeed")
    109     } catch {}
    110     let state = await transfer.state
    111     XCTAssertEqual(state, .awaitingVerification)
    112     try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer)
    113     let counts = await transfer.counts
    114     XCTAssertEqual(counts.acceptedSettlement, 1)
    115   }
    116 
    117   func testChangedNativeAssociationCannotSettle() async throws {
    118     let fixture = try BackgroundUploadFixture()
    119     defer { fixture.remove() }
    120     let transfer = BackgroundTransferHarness()
    121     let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
    122     try await transfer.seed(request: request, state: .awaitingVerification)
    123     let value = try await transfer.snapshot(for: request.identifier)
    124     let snapshot = try XCTUnwrap(value)
    125     let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: .init(draft: fixture.draft(revision: 3, stage: .verified)))
    126     let changed = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)), remoteURL: "http://127.0.0.1:3000/changed")
    127     try await transfer.seed(request: changed, state: .awaitingVerification)
    128     do {
    129       try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer)
    130       XCTFail("Changed request must retain evidence")
    131     } catch {}
    132     let counts = await transfer.counts
    133     XCTAssertEqual(counts.acceptedSettlement, 0)
    134     XCTAssertEqual(counts.cancel, 0)
    135   }
    136 }