field_ios

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

TeraNativeRepairTests.swift (6603B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import TeraKitBindings
      5 import XCTest
      6 
      7 final class TeraNativeRepairTests: XCTestCase {
      8   func testMissingAndMismatchedTransfersPersistIndependentlyOfValidSettlement() async throws {
      9     let fixture = try BackgroundUploadFixture()
     10     let storage = try MediaOwnershipFixture()
     11     defer { fixture.remove(); storage.remove() }
     12     let runtime = try await storage.runtime()
     13     let backend = TeraGeneratedRuntimeBackend(runtime: runtime)
     14     let transfer = BackgroundTransferHarness()
     15     let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)))
     16     let missing = try RadrootsBackgroundTransferIdentifier("radroots.add.\(String(repeating: "0", count: 31))1.2.\(String(repeating: "a", count: 32))")
     17     try await transfer.seed(request: TeraBackgroundUploadRequest.replacingIdentifier(in: request, with: missing), state: .awaitingVerification)
     18     try await transfer.seed(request: request, state: .awaitingVerification)
     19     let mismatch = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "b", count: 32)), remoteURL: "http://127.0.0.1:3000/changed")
     20     try await transfer.seed(request: mismatch, state: .awaitingVerification)
     21     let owner = TeraNativeUploadRecoveryOwner(draft: fixture.draft(revision: 3, stage: .verified))
     22     let run = { @Sendable in
     23       try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil, complete: { snapshot, owner in
     24         let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: owner)
     25         try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer)
     26       }, report: { snapshot, reason in
     27         let key = TeraNativeRecoveryIssue.key(snapshot.identifier.rawValue)
     28         let status = try? await backend.reportNativeRecoveryStatus(key: key, reason: reason)
     29         return reason == .resolved ? nil : .init(key: key, reason: reason, status: status)
     30       }, lookup: { key in key == fixture.draftID ? owner : nil })
     31     }
     32     let first = try await run()
     33     XCTAssertEqual(first.progress.visited, 3)
     34     XCTAssertEqual(first.progress.issues.count, 2)
     35     XCTAssertTrue(first.progress.issues.contains { $0.reason == .missingParent })
     36     XCTAssertTrue(first.progress.issues.contains { $0.reason == .associationMismatch })
     37     XCTAssertTrue(first.progress.issues.allSatisfy { $0.status?.revision == 1 })
     38     let replay = try await run()
     39     XCTAssertEqual(replay.progress.visited, 3, "Completed positions are visited without repeating settlement")
     40     XCTAssertEqual(replay.progress.issues, first.progress.issues)
     41     let counts = await transfer.counts
     42     XCTAssertEqual(counts.acceptedSettlement, 1)
     43     XCTAssertEqual(counts.cancel, 0)
     44     XCTAssertEqual(counts.enqueue, 0)
     45     for issue in first.progress.issues {
     46       let saved = try await backend.nativeRecoveryStatus(key: issue.key)
     47       XCTAssertEqual(saved, issue.status)
     48     }
     49     _ = try await runtime.shutdown()
     50   }
     51 
     52   func testProtectedDataPausesAtSameCursorWithoutCorruptionOrSettlement() async throws {
     53     let fixture = try BackgroundUploadFixture()
     54     defer { fixture.remove() }
     55     let transfer = BackgroundTransferHarness()
     56     try await transfer.seed(request: fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))), state: .awaitingVerification)
     57     let failure = TeraRuntimeFailure.local(operation: "test.recovery", code: "protected_data_unavailable", safeMessage: "Locked.")
     58     let result = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil) { _ in throw failure }
     59     XCTAssertEqual(result.progress.pause, .protectedData)
     60     XCTAssertEqual(result.progress.visited, 0)
     61     XCTAssertEqual(result.progress.remaining, 1)
     62     XCTAssertFalse(result.progress.needsAttention)
     63     XCTAssertTrue(result.progress.issues.isEmpty)
     64     XCTAssertNil(result.cursor)
     65     let state = await transfer.state
     66     XCTAssertEqual(state, .awaitingVerification)
     67   }
     68 
     69   @MainActor
     70   func testRunningNativeRecoveryCannotBlockUnrelatedSaveAndSubmit() async throws {
     71     let backend = AddBackend(advanceOffline: true)
     72     let client = try await TeraAddStoreTests.startedClient(backend)
     73     let fixture = try BackgroundUploadFixture()
     74     defer { fixture.remove() }
     75     let transfer = BackgroundTransferHarness()
     76     let pause = ResourceTestPause()
     77     await transfer.pauseDiscovery(pause)
     78     let store = TeraAddStore(runtimeClient: client, media: fixture.coordinator(transfer: transfer))
     79     await store.configure(snapshot: backend.snapshot())
     80     let startup = Task { await store.start() }
     81     await pause.entered.wait()
     82     XCTAssertEqual(store.state, .ready)
     83     store.updateForm(\.content, "An unrelated saved update")
     84     await store.save()
     85     XCTAssertEqual(store.composerState, .saved)
     86     await store.submit()
     87     XCTAssertEqual(store.submissions.status?.state, .queued)
     88     XCTAssertTrue(store.recovery.transfers.isRunning)
     89     await pause.resume.open()
     90     await startup.value
     91     XCTAssertEqual(store.state, .ready)
     92     store.stop()
     93     _ = try await client.stop()
     94   }
     95 
     96   @MainActor
     97   func testFailedStatusPersistenceIsNotReportedAsDurable() async throws {
     98     let backend = try TeraScopeBackend()
     99     let client = try await TeraScopeFixtures.client(backend)
    100     let issue = await TeraNativeRecoveryClassification.report("unknown-native-record", reason: .missingParent, client: client)
    101     XCTAssertEqual(issue?.reason, .missingParent)
    102     XCTAssertNil(issue?.status)
    103     XCTAssertEqual(TeraNativeRecoveryClassification.pause(RadrootsBackgroundTransferError.persistenceFailure), .storageUnavailable)
    104     _ = try await client.stop()
    105   }
    106 
    107   func testGeneratedStatusRejectsUnknownVersionAndNoncanonicalIdentity() async throws {
    108     let fixture = try MediaOwnershipFixture()
    109     defer { fixture.remove() }
    110     let runtime = try await fixture.runtime()
    111     for (version, key) in [(UInt16(2), String(repeating: "a", count: 64)), (1, String(repeating: "A", count: 64))] {
    112       do {
    113         _ = try await runtime.reportNativeRecoveryStatus(schemaVersion: version, transferKey: key, reason: .missingParent)
    114         XCTFail("Invalid status input must fail before persistence")
    115       } catch let TeraAppError.Failure(report) {
    116         XCTAssertEqual(report.code, "invalid_native_recovery_status")
    117       }
    118     }
    119     let absent = try await runtime.nativeRecoveryStatus(schemaVersion: 1, transferKey: String(repeating: "a", count: 64))
    120     XCTAssertNil(absent)
    121     _ = try await runtime.shutdown()
    122   }
    123 }