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

RadrootsAppleBackgroundTransferTests.swift (12401B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import RadrootsKitTesting
      5 import Testing
      6 
      7 @Test func appleBackgroundTransferPersistsRunningSnapshotAfterEnqueue() async throws {
      8     let store = RadrootsInMemoryBackgroundTransferStore()
      9     let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 100))
     10     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
     11     let request = try appleTransferRequest(identifier: "field.transfer.enqueue")
     12 
     13     let handle = try await transfer.enqueue(request)
     14 
     15     #expect(handle.identifier == request.identifier)
     16     #expect(await probe.enqueuedRequests == [request])
     17     let snapshot = try await transfer.snapshot(for: request.identifier)
     18     #expect(snapshot?.state == .running)
     19     #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 100))
     20 }
     21 
     22 @Test func appleBackgroundTransferRejectsDuplicateIdentifiersBeforeAdapterMutation() async throws {
     23     let store = RadrootsInMemoryBackgroundTransferStore()
     24     let probe = RadrootsAppleBackgroundTransferProbe()
     25     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
     26     let request = try appleTransferRequest(identifier: "field.transfer.duplicate")
     27 
     28     _ = try await transfer.enqueue(request)
     29     await #expect(
     30         throws: RadrootsBackgroundTransferError.invalidRequest
     31     ) {
     32         _ = try await transfer.enqueue(request)
     33     }
     34 
     35     #expect(await probe.enqueuedRequests == [request])
     36 }
     37 
     38 @Test func appleBackgroundTransferRecordsFailedSnapshotWhenAdapterRejectsEnqueue() async throws {
     39     let store = RadrootsInMemoryBackgroundTransferStore()
     40     let probe = RadrootsAppleBackgroundTransferProbe(
     41         now: Date(timeIntervalSince1970: 200),
     42         enqueueOutcome: .failure(.transferFailure)
     43     )
     44     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
     45     let request = try appleTransferRequest(identifier: "field.transfer.failed")
     46 
     47     await #expect(
     48         throws: RadrootsBackgroundTransferError.transferFailure
     49     ) {
     50         _ = try await transfer.enqueue(request)
     51     }
     52 
     53     let snapshot = try await transfer.snapshot(for: request.identifier)
     54     #expect(snapshot?.state == .failed)
     55     #expect(snapshot?.failure == .enqueueFailed)
     56     #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 200))
     57 }
     58 
     59 @Test func appleBackgroundTransferCancelsThroughAdapterAndUpdatesStore() async throws {
     60     let store = RadrootsInMemoryBackgroundTransferStore()
     61     let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 300))
     62     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
     63     let request = try appleTransferRequest(identifier: "field.transfer.cancel")
     64 
     65     _ = try await transfer.enqueue(request)
     66     try await transfer.cancel(request.identifier)
     67 
     68     #expect(await probe.cancelledIdentifiers == [request.identifier])
     69     #expect(try await transfer.snapshot(for: request.identifier)?.state == .cancelled)
     70     #expect(
     71         try await transfer.snapshot(for: request.identifier)?.updatedAt
     72             == Date(timeIntervalSince1970: 300)
     73     )
     74 }
     75 
     76 @Test func appleBackgroundTransferReconcilesQueuedSnapshotsWithActiveRecoveredTasks() async throws {
     77     let request = try appleTransferRequest(identifier: "field.transfer.recovered")
     78     let queued = try RadrootsBackgroundTransferSnapshot(
     79         request: request, state: .queued, updatedAt: Date(timeIntervalSince1970: 1)
     80     )
     81     let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [queued])
     82     let probe = RadrootsAppleBackgroundTransferProbe(
     83         now: Date(timeIntervalSince1970: 400), activeIdentifiers: [request.identifier]
     84     )
     85     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
     86 
     87     let snapshots = try await transfer.snapshots()
     88 
     89     #expect(snapshots.count == 1)
     90     #expect(snapshots.first?.state == .running)
     91     #expect(snapshots.first?.updatedAt == Date(timeIntervalSince1970: 400))
     92     #expect(try await store.loadSnapshots().first?.state == .running)
     93 }
     94 
     95 @Test func appleBackgroundTransferMarksMissingRecoveredUploadInterrupted() async throws {
     96     let request = try appleUploadRequest(identifier: "field.transfer.interrupted")
     97     let running = try RadrootsBackgroundTransferSnapshot(
     98         request: request, state: .running,
     99         progress: RadrootsBackgroundTransferProgress(bytesTransferred: 5, totalBytesExpected: 10),
    100         updatedAt: Date(timeIntervalSince1970: 1)
    101     )
    102     let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [running])
    103     let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 401))
    104     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
    105 
    106     let snapshot = try #require(try await transfer.snapshots().first)
    107 
    108     #expect(snapshot.state == .interrupted)
    109     #expect(snapshot.failure == .interrupted)
    110     #expect(snapshot.possibleRemoteOrphan)
    111     #expect(snapshot.updatedAt == Date(timeIntervalSince1970: 401))
    112 }
    113 
    114 @Test func appleBackgroundTransferForwardsBackgroundCompletionHandlers() async {
    115     let probe = RadrootsAppleBackgroundTransferProbe()
    116     let transfer = RadrootsAppleBackgroundTransfer(
    117         store: RadrootsInMemoryBackgroundTransferStore(), adapters: probe.adapters()
    118     )
    119     let completion = RadrootsCompletionProbe()
    120 
    121     await transfer.handleEventsForBackgroundURLSession(
    122         identifier: "org.radroots.field-ios.background.transfer"
    123     ) {
    124         completion.markCompleted()
    125     }
    126 
    127     #expect(
    128         await probe.handledBackgroundEventIdentifiers == ["org.radroots.field-ios.background.transfer"]
    129     )
    130     #expect(completion.completed)
    131 }
    132 
    133 @Test func appleBackgroundTransferCoordinatorMovesCompletedDownloadToDestination() async throws {
    134     let roots = try appleTransferRoots()
    135     let store = RadrootsInMemoryBackgroundTransferStore()
    136     let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    137     let coordinator = RadrootsTransferCoordinator(
    138         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    139         fileResolver: resolver,
    140         now: { Date(timeIntervalSince1970: 500) }
    141     )
    142     let request = try appleTransferRequest(identifier: "field.transfer.completed")
    143     let running = try RadrootsBackgroundTransferSnapshot(
    144         request: request, state: .running, updatedAt: Date(timeIntervalSince1970: 1)
    145     )
    146     try await store.saveSnapshot(running)
    147     let stagingRoot = roots.temporaryRoot.appendingPathComponent(
    148         "background-transfer-tests", isDirectory: true
    149     )
    150     try FileManager.default.createDirectory(at: stagingRoot, withIntermediateDirectories: true)
    151     let stagedFile = stagingRoot.appendingPathComponent("download.bin")
    152     let payload = Data("downloaded".utf8)
    153     try payload.write(to: stagedFile)
    154 
    155     await coordinator.complete(
    156         identifier: request.identifier,
    157         completion: RadrootsTransferCompletion(
    158             platformError: nil,
    159             stagedDownloadResult: .file(stagedFile),
    160             httpResult: RadrootsBackgroundHTTPResult(
    161                 statusCode: 200, mediaType: "image/png", body: nil, bodyExceeded: false
    162             ),
    163             bytesTransferred: 0,
    164             totalBytesExpected: nil
    165         )
    166     )
    167 
    168     let snapshot = try await store.loadSnapshots().first
    169     let destination = try resolver.resolve(
    170         .file(RadrootsFileReference(scope: .cache, relativePath: "field.transfer.completed.json"))
    171     )
    172     #expect(snapshot?.state == .awaitingVerification)
    173     #expect(snapshot?.progress.bytesTransferred == Int64(payload.count))
    174     #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 500))
    175     #expect(snapshot?.response?.statusCode == 200)
    176     #expect(snapshot?.downloadedArtifact?.byteSize == UInt64(payload.count))
    177     #expect(snapshot?.downloadedArtifact?.mediaType == "image/png")
    178     let downloadedDigest = try RadrootsAppleFileDigest.sha256(at: destination)
    179     #expect(snapshot?.downloadedArtifact?.sha256 == downloadedDigest)
    180     #expect(try Data(contentsOf: destination) == payload)
    181     #expect(!FileManager.default.fileExists(atPath: stagedFile.path))
    182 }
    183 
    184 @Test func appleBackgroundTransferCoordinatorRejectsMalformedResponseMediaType() async throws {
    185     let roots = try appleTransferRoots()
    186     let store = RadrootsInMemoryBackgroundTransferStore()
    187     let coordinator = RadrootsTransferCoordinator(
    188         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    189         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    190     )
    191     let request = try appleUploadRequest(identifier: "field.transfer.malformed-media-type")
    192     try await store.saveSnapshot(
    193         RadrootsBackgroundTransferSnapshot(request: request, state: .running)
    194     )
    195 
    196     await coordinator.complete(
    197         identifier: request.identifier,
    198         completion: RadrootsTransferCompletion(
    199             platformError: nil,
    200             stagedDownloadResult: nil,
    201             httpResult: RadrootsBackgroundHTTPResult(
    202                 statusCode: 200, mediaType: nil, body: nil, bodyExceeded: false,
    203                 mediaTypeWasMalformed: true
    204             ),
    205             bytesTransferred: 10,
    206             totalBytesExpected: 10
    207         )
    208     )
    209 
    210     let snapshot = try #require(try await store.loadSnapshots().first)
    211     #expect(snapshot.state == .failed)
    212     #expect(snapshot.failure == .responseMediaType)
    213     #expect(snapshot.possibleRemoteOrphan)
    214 }
    215 
    216 @Test func appleBackgroundTransferCoordinatorRecordsFailedDownloadSnapshot() async throws {
    217     let roots = try appleTransferRoots()
    218     let store = RadrootsInMemoryBackgroundTransferStore()
    219     let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    220     let coordinator = RadrootsTransferCoordinator(
    221         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    222         fileResolver: resolver,
    223         now: { Date(timeIntervalSince1970: 600) }
    224     )
    225     let request = try appleTransferRequest(identifier: "field.transfer.download.failed")
    226     try await store.saveSnapshot(
    227         RadrootsBackgroundTransferSnapshot(
    228             request: request, state: .running, updatedAt: Date(timeIntervalSince1970: 1)
    229         )
    230     )
    231 
    232     await coordinator.complete(
    233         identifier: request.identifier,
    234         completion: RadrootsTransferCompletion(
    235             platformError: nil,
    236             stagedDownloadResult: .failure,
    237             httpResult: successfulHTTPResult(),
    238             bytesTransferred: 0,
    239             totalBytesExpected: nil
    240         )
    241     )
    242 
    243     let snapshot = try await store.loadSnapshots().first
    244     #expect(snapshot?.state == .failed)
    245     #expect(snapshot?.failure == .downloadStagingFailure)
    246     #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 600))
    247 }
    248 
    249 @Test func appleBackgroundTransferCoordinatorCompletesUploadWithProgress() async throws {
    250     let roots = try appleTransferRoots()
    251     let store = RadrootsInMemoryBackgroundTransferStore()
    252     let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    253     let coordinator = RadrootsTransferCoordinator(
    254         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    255         fileResolver: resolver,
    256         now: { Date(timeIntervalSince1970: 700) }
    257     )
    258     let request = try appleUploadRequest(identifier: "field.transfer.upload.completed")
    259     try await store.saveSnapshot(
    260         RadrootsBackgroundTransferSnapshot(
    261             request: request, state: .running, updatedAt: Date(timeIntervalSince1970: 1)
    262         )
    263     )
    264 
    265     await coordinator.updateProgress(
    266         identifier: request.identifier, bytesTransferred: 4, totalBytesExpected: 10
    267     )
    268     await coordinator.complete(
    269         identifier: request.identifier,
    270         completion: RadrootsTransferCompletion(
    271             platformError: nil,
    272             stagedDownloadResult: nil,
    273             httpResult: successfulHTTPResult(),
    274             bytesTransferred: 10,
    275             totalBytesExpected: 10
    276         )
    277     )
    278 
    279     let snapshot = try await store.loadSnapshots().first
    280     #expect(snapshot?.state == .awaitingVerification)
    281     #expect(snapshot?.progress.bytesTransferred == 10)
    282     #expect(snapshot?.progress.totalBytesExpected == 10)
    283     #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 700))
    284     #expect(snapshot?.response?.statusCode == 200)
    285 }