apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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 }