RadrootsBackgroundTransferRecoveryTests.swift (7920B)
1 import Darwin 2 import Foundation 3 @testable import RadrootsKit 4 import RadrootsKitTesting 5 import Testing 6 7 @Test func appleBackgroundTransferExpirationAndRetryPreserveStableIdentity() async throws { 8 let request = try appleUploadRequest(identifier: "field.transfer.expire-retry") 9 let store = RadrootsInMemoryBackgroundTransferStore() 10 let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 900)) 11 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 12 13 _ = try await transfer.enqueue(request) 14 try await transfer.expire(request.identifier) 15 #expect(try await store.loadSnapshots().first?.state == .expired) 16 #expect(await probe.cancelledIdentifiers == [request.identifier]) 17 18 let retried = try await transfer.retry(request) 19 #expect(retried.identifier == request.identifier) 20 #expect(try await store.loadSnapshots().first?.state == .running) 21 #expect(await probe.enqueuedRequests == [request, request]) 22 } 23 24 @Test func appleBackgroundTransferRefusesRetryWhilePlatformTaskIsStillActive() async throws { 25 let request = try appleUploadRequest(identifier: "field.transfer.active-retry") 26 let store = try RadrootsInMemoryBackgroundTransferStore( 27 snapshots: [RadrootsBackgroundTransferSnapshot(request: request, state: .interrupted)] 28 ) 29 let probe = RadrootsAppleBackgroundTransferProbe(activeIdentifiers: [request.identifier]) 30 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 31 32 await #expect( 33 throws: RadrootsBackgroundTransferError.invalidRequest 34 ) { 35 _ = try await transfer.retry(request) 36 } 37 #expect(await probe.enqueuedRequests.isEmpty) 38 #expect(try await store.loadSnapshots().first?.state == .interrupted) 39 } 40 41 @Test func appleBackgroundTransferTerminalCancellationIsIdempotent() async throws { 42 let request = try appleUploadRequest(identifier: "field.transfer.terminal-cancel") 43 let store = try RadrootsInMemoryBackgroundTransferStore( 44 snapshots: [ 45 RadrootsBackgroundTransferSnapshot(request: request, state: .awaitingVerification) 46 ] 47 ) 48 let probe = RadrootsAppleBackgroundTransferProbe() 49 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 50 51 try await transfer.cancel(request.identifier) 52 try await transfer.expire(request.identifier) 53 54 #expect(try await store.loadSnapshots().first?.state == .awaitingVerification) 55 #expect(await probe.cancelledIdentifiers.isEmpty) 56 } 57 58 @Test func appleBackgroundTransferCoordinatorRejectsTransfersBeyondImmutableBound() async throws { 59 let roots = try appleTransferRoots() 60 let store = RadrootsInMemoryBackgroundTransferStore() 61 let coordinator = RadrootsTransferCoordinator( 62 sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store, 63 fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots) 64 ) 65 let request = try appleUploadRequest( 66 identifier: "field.transfer.too-large", maximumTransferBytes: 5 67 ) 68 try await store.saveSnapshot( 69 RadrootsBackgroundTransferSnapshot(request: request, state: .running) 70 ) 71 72 await coordinator.complete( 73 identifier: request.identifier, 74 completion: RadrootsTransferCompletion( 75 platformError: nil, 76 stagedDownloadResult: nil, 77 httpResult: successfulHTTPResult(), 78 bytesTransferred: 6, 79 totalBytesExpected: 6 80 ) 81 ) 82 83 let snapshot = try #require(try await store.loadSnapshots().first) 84 #expect(snapshot.state == .failed) 85 #expect(snapshot.failure == .transferTooLarge) 86 #expect(snapshot.possibleRemoteOrphan) 87 } 88 89 @Test func appleBackgroundURLTaskDescriptorRetainsRelaunchBoundsAndMigratesLegacyIdentity() throws { 90 let request = try appleUploadRequest( 91 identifier: "field.transfer.task-descriptor", 92 responsePolicy: .boundedJSON(maximumBodyBytes: 1024), 93 maximumTransferBytes: 4096 94 ) 95 96 let descriptor = RadrootsBackgroundURLTaskDescriptor(request: request) 97 #expect( 98 RadrootsBackgroundURLTaskDescriptor(taskDescription: descriptor.taskDescription) == descriptor 99 ) 100 101 let legacy = try #require( 102 RadrootsBackgroundURLTaskDescriptor(taskDescription: request.identifier.rawValue) 103 ) 104 #expect(legacy.identifier == request.identifier) 105 #expect( 106 legacy.maximumTransferBytes == RadrootsBackgroundTransferRequest.defaultMaximumTransferBytes 107 ) 108 #expect(legacy.maximumResponseBodyBytes == 65536) 109 #expect( 110 RadrootsBackgroundURLTaskDescriptor(taskDescription: "radroots-transfer-v1|unsafe|0|0") == nil 111 ) 112 } 113 114 #if os(iOS) && targetEnvironment(simulator) 115 @Test func appleBackgroundTransferLiveURLSessionDownloadsIntoRustVerificationBoundary() 116 async throws { 117 guard let origin = ProcessInfo.processInfo.environment["RADROOTS_URLSESSION_TEST_ORIGIN"], 118 let remoteURL = URL(string: origin)?.appendingPathComponent("Package.swift") 119 else { return } 120 let roots = try appleTransferRoots() 121 defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } 122 let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots) 123 let transfer = try RadrootsAppleBackgroundTransfer( 124 roots: roots, 125 sessionIdentifier: "org.radroots.tests.background-transfer.\(UUID().uuidString.lowercased())" 126 ) 127 let destination = RadrootsFileReference( 128 scope: .cache, relativePath: "live-urlsession/package.swift" 129 ) 130 let request = try RadrootsBackgroundTransferRequest( 131 identifier: RadrootsBackgroundTransferIdentifier("field.transfer.live-urlsession"), 132 remoteURL: remoteURL, 133 method: .get, 134 operation: .download(destination: .file(destination)), 135 networkPolicy: .simulatorLoopbackHTTP, 136 maximumTransferBytes: 64 * 1024 137 ) 138 139 _ = try await transfer.enqueue(request) 140 let deadline = Date().addingTimeInterval(15) 141 var snapshot = try await transfer.snapshot(for: request.identifier) 142 while snapshot?.state == .queued || snapshot?.state == .running { 143 guard Date() < deadline else { 144 throw RadrootsBackgroundTransferError.transferFailure 145 } 146 try await Task.sleep(nanoseconds: 50_000_000) 147 snapshot = try await transfer.snapshot(for: request.identifier) 148 } 149 150 let completedTransfer = try #require(snapshot) 151 #expect(completedTransfer.state == .awaitingVerification) 152 let downloadedArtifact = try #require(completedTransfer.downloadedArtifact) 153 let destinationURL = try resolver.resolve(.file(destination)) 154 let downloadedBytes = try Data(contentsOf: destinationURL) 155 #expect(!downloadedBytes.isEmpty) 156 #expect(downloadedArtifact.byteSize == UInt64(downloadedBytes.count)) 157 #expect(try downloadedArtifact.sha256 == (RadrootsAppleFileDigest.sha256(at: destinationURL))) 158 #expect(downloadedArtifact.mediaType == "application/octet-stream") 159 160 try await transfer.settle(request.identifier, verification: .accepted) 161 #expect(try await transfer.snapshot(for: request.identifier)?.state == .completed) 162 } 163 #endif 164 165 @Test func appleBackgroundTransferCancelsRecoveredPlatformOrphans() async throws { 166 let identifier = try RadrootsBackgroundTransferIdentifier("field.transfer.platform-orphan") 167 let probe = RadrootsAppleBackgroundTransferProbe(activeIdentifiers: [identifier]) 168 let transfer = RadrootsAppleBackgroundTransfer( 169 store: RadrootsInMemoryBackgroundTransferStore(), adapters: probe.adapters() 170 ) 171 172 #expect(try await transfer.snapshots().isEmpty) 173 #expect(await probe.cancelledIdentifiers == [identifier]) 174 }