field_ios

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

TeraStoppedUploadTests.swift (6558B)


      1 import Foundation
      2 import RadrootsKit
      3 @testable import TeraApp
      4 import XCTest
      5 
      6 @MainActor
      7 final class TeraStoppedUploadTests: XCTestCase {
      8   func testStoppedRecoveryReadsExactResponsesWithoutEnqueueRetryOrSettlement() async throws {
      9     let fixture = try BackgroundUploadFixture()
     10     defer { fixture.remove() }
     11     let transfer = BackgroundTransferHarness()
     12     let coordinator = fixture.coordinator(transfer: transfer)
     13     let status = submission(fixture, revision: 7)
     14     let missing = try await coordinator.retainedSubmissionUpload(status, media: fixture.media)
     15     XCTAssertNil(missing)
     16     let job = fixture.job(revision: 2, operation: String(repeating: "2", count: 32))
     17     let request = try fixture.request(job: job)
     18     for state: RadrootsBackgroundTransferState in [.queued, .running, .failed, .cancelled, .interrupted, .expired] {
     19       try await transfer.seed(request: request, state: state)
     20       let receipt = try await coordinator.retainedSubmissionUpload(status, media: fixture.media)
     21       XCTAssertNil(receipt)
     22     }
     23     for state: RadrootsBackgroundTransferState in [.awaitingVerification, .completed] {
     24       try await transfer.seed(request: request, state: state)
     25       let receipt = try await coordinator.retainedSubmissionUpload(status, media: fixture.media)
     26       XCTAssertEqual(receipt?.identifier, job.transferIdentifier)
     27       XCTAssertEqual(receipt?.draftID, fixture.draftID)
     28       XCTAssertEqual(receipt?.expectedRevision, job.draft.revision)
     29       XCTAssertNotEqual(receipt?.expectedRevision, status.revision)
     30       XCTAssertEqual(receipt?.body, Data("{}".utf8))
     31     }
     32     let counts = await transfer.counts
     33     XCTAssertEqual(counts.enqueue, 0)
     34     XCTAssertEqual(counts.retry, 0)
     35     XCTAssertEqual(counts.cancel, 0)
     36     XCTAssertEqual(counts.acceptedSettlement, 0)
     37   }
     38 
     39   func testStoppedRecoveryRejectsWrongDestinationFutureRevisionAndAmbiguousAttempts() async throws {
     40     let fixture = try BackgroundUploadFixture()
     41     defer { fixture.remove() }
     42     let status = submission(fixture)
     43     let job = fixture.job(revision: 2, operation: String(repeating: "2", count: 32))
     44     for scenario in 0 ..< 3 {
     45       let transfer = BackgroundTransferHarness()
     46       let coordinator = fixture.coordinator(transfer: transfer)
     47       let first = scenario == 1 ? fixture.job(revision: 9, operation: String(repeating: "3", count: 32)) : job
     48       try await transfer.seed(request: fixture.request(job: first, remoteURL: scenario == 0 ? "http://127.0.0.1:3001/upload" : nil), state: .awaitingVerification)
     49       if scenario == 2 {
     50         try await transfer.seed(request: fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "4", count: 32))), state: .awaitingVerification)
     51       }
     52       do {
     53         _ = try await coordinator.retainedSubmissionUpload(status, media: fixture.media)
     54         XCTFail("Mismatched or ambiguous native evidence cannot be consumed")
     55       } catch {}
     56       let counts = await transfer.counts
     57       XCTAssertEqual(counts.enqueue + counts.retry + counts.cancel + counts.acceptedSettlement, 0)
     58     }
     59   }
     60 
     61   func testStopCancelsOwnedWaiterBeforeEnqueueAndRetainsAlreadyEnqueuedResponse() async throws {
     62     for boundary in [BackgroundTransferPause.discovery, .snapshot] {
     63       let fixture = try BackgroundUploadFixture()
     64       defer { fixture.remove() }
     65       let transfer = BackgroundTransferHarness(pause: boundary)
     66       let coordinator = fixture.coordinator(transfer: transfer)
     67       let stop = TeraSubmissionStopControl()
     68       let job = fixture.job(revision: 2, operation: String(repeating: "2", count: 32))
     69       let task = Task { try await stop.upload(using: coordinator, transfer: job.transfer, source: fixture.media) }
     70       for _ in 0 ..< 100 {
     71         if await transfer.isPaused {
     72           break
     73         }
     74         try await Task.sleep(for: .milliseconds(10))
     75       }
     76       let paused = await transfer.isPaused
     77       XCTAssertTrue(paused)
     78       stop.request()
     79       await transfer.releasePause()
     80       do { _ = try await task.value; XCTFail("Stopped waiter cannot report a new result") } catch {}
     81       let counts = await transfer.counts
     82       XCTAssertEqual(counts.enqueue, boundary == .discovery ? 0 : 1)
     83       XCTAssertEqual(counts.retry + counts.cancel + counts.acceptedSettlement, 0)
     84       if boundary == .snapshot {
     85         let receipt = try await coordinator.retainedSubmissionUpload(submission(fixture), media: fixture.media)
     86         XCTAssertEqual(receipt?.identifier, job.transferIdentifier)
     87       }
     88     }
     89   }
     90 
     91   private func submission(_ fixture: BackgroundUploadFixture, revision: UInt64 = 2) -> TeraSubmissionStatus {
     92     let draft = fixture.draft(revision: revision, stage: .uploading)
     93     let scope = TeraComposerScope(authorPublicKey: String(repeating: "a", count: 64), localNetworkID: "nearby")
     94     let request = TeraSubmissionRequest(commandID: String(repeating: "3", count: 32), scope: scope,
     95                                         composerID: String(repeating: "4", count: 32), expectedRevision: 1)
     96     return TeraSubmissionStatus(request: request, intentID: fixture.draftID, operationID: String(repeating: "5", count: 32),
     97                                 revision: draft.revision, captured: TeraComposerDraft(scope: scope, id: request.composerID, revision: 1, editSequence: 1,
     98                                                                                       form: TeraComposerForm(editing: draft.form!)), state: .cancelled,
     99                                 committedAtUnixMilliseconds: draft.createdAtUnixMilliseconds, updatedAtUnixMilliseconds: draft.updatedAtUnixMilliseconds,
    100                                 media: [TeraSubmissionMedia(opaqueReference: fixture.media.opaqueReference, progress: draft.media[0])],
    101                                 settlement: TeraOperationSettlement(artifacts: 1, signed: 0, admitted: 0, pending: 0, retryable: 0,
    102                                                                     indeterminate: 0, failedTerminal: 0, cancelled: 1, deliveryPlans: 1, deliverySatisfied: 0, deliveryPending: 0,
    103                                                                     deliveryRetryable: 0, deliveryExhausted: 0, deliveryFailedTerminal: 0, deliveryCancelled: 1),
    104                                 delivery: TeraPublicationEvidence(state: .notIssued, stopRequestedAtUnixMilliseconds: 1_800_000_000_002,
    105                                                                   schedulingRevision: 2, retainedFacts: 0, recordedAttempts: 0, unresolvedClaims: false),
    106                                 targetDetails: .fixture(), retry: .stopped)
    107   }
    108 }