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 }