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

RadrootsBackgroundTransferReceiptTests.swift (13427B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import RadrootsKitTesting
      5 import Testing
      6 
      7 @Test func appleBackgroundTransferCoordinatorPersistsBoundedDescriptorResponse() async throws {
      8     let roots = try appleTransferRoots()
      9     let store = RadrootsInMemoryBackgroundTransferStore()
     10     let coordinator = RadrootsTransferCoordinator(
     11         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
     12         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots),
     13         now: { Date(timeIntervalSince1970: 710) }
     14     )
     15     let body = Data(#"{"url":"https://cdn.radroots.org/a.png"}"#.utf8)
     16     let request = try appleUploadRequest(
     17         identifier: "field.transfer.upload.descriptor",
     18         responsePolicy: .boundedJSON(maximumBodyBytes: 1024)
     19     )
     20     try await store.saveSnapshot(
     21         RadrootsBackgroundTransferSnapshot(request: request, state: .running)
     22     )
     23 
     24     await coordinator.complete(
     25         identifier: request.identifier,
     26         completion: RadrootsTransferCompletion(
     27             platformError: nil,
     28             stagedDownloadResult: nil,
     29             httpResult: RadrootsBackgroundHTTPResult(
     30                 statusCode: 200, mediaType: "application/json", body: body, bodyExceeded: false
     31             ),
     32             bytesTransferred: 10,
     33             totalBytesExpected: 10
     34         )
     35     )
     36 
     37     let snapshot = try #require(try await store.loadSnapshots().first)
     38     #expect(snapshot.state == .awaitingVerification)
     39     #expect(snapshot.response?.body == body)
     40     #expect(snapshot.response?.mediaType == "application/json")
     41     #expect(!snapshot.possibleRemoteOrphan)
     42 
     43     await coordinator.complete(
     44         identifier: request.identifier,
     45         completion: RadrootsTransferCompletion(
     46             platformError: nil,
     47             stagedDownloadResult: nil,
     48             httpResult: RadrootsBackgroundHTTPResult(
     49                 statusCode: 500, mediaType: nil, body: nil, bodyExceeded: false
     50             ),
     51             bytesTransferred: 10,
     52             totalBytesExpected: 10
     53         )
     54     )
     55     #expect(try await store.loadSnapshots().first?.state == .awaitingVerification)
     56 }
     57 
     58 @Test func appleBackgroundTransferCoordinatorRejectsEncodedDescriptorResponse() 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.upload.encoded",
     67         responsePolicy: .boundedJSON(maximumBodyBytes: 1024)
     68     )
     69     try await store.saveSnapshot(
     70         RadrootsBackgroundTransferSnapshot(request: request, state: .running)
     71     )
     72 
     73     await coordinator.complete(
     74         identifier: request.identifier,
     75         completion: RadrootsTransferCompletion(
     76             platformError: nil,
     77             stagedDownloadResult: nil,
     78             httpResult: RadrootsBackgroundHTTPResult(
     79                 statusCode: 200, mediaType: "application/json", body: Data("{}".utf8),
     80                 contentEncoding: "gzip", bodyExceeded: false
     81             ),
     82             bytesTransferred: 10,
     83             totalBytesExpected: 10
     84         )
     85     )
     86 
     87     let snapshot = try #require(try await store.loadSnapshots().first)
     88     #expect(snapshot.state == .failed)
     89     #expect(snapshot.failure == .responseContentEncoding)
     90     #expect(snapshot.possibleRemoteOrphan)
     91 }
     92 
     93 @Test func appleBackgroundTransferCoordinatorRejectsStatusAndOversizedResponse() async throws {
     94     let roots = try appleTransferRoots()
     95     let store = RadrootsInMemoryBackgroundTransferStore()
     96     let coordinator = RadrootsTransferCoordinator(
     97         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
     98         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots),
     99         now: { Date(timeIntervalSince1970: 720) }
    100     )
    101     let statusRequest = try appleUploadRequest(identifier: "field.transfer.upload.status")
    102     try await store.saveSnapshot(
    103         RadrootsBackgroundTransferSnapshot(request: statusRequest, state: .running)
    104     )
    105     await coordinator.complete(
    106         identifier: statusRequest.identifier,
    107         completion: RadrootsTransferCompletion(
    108             platformError: nil,
    109             stagedDownloadResult: nil,
    110             httpResult: RadrootsBackgroundHTTPResult(
    111                 statusCode: 503, mediaType: nil, body: nil, bodyExceeded: false
    112             ),
    113             bytesTransferred: 10,
    114             totalBytesExpected: 10
    115         )
    116     )
    117     #expect(try await store.loadSnapshots().first?.failure == .httpStatus)
    118 
    119     let bodyRequest = try appleUploadRequest(
    120         identifier: "field.transfer.upload.oversized",
    121         responsePolicy: .boundedJSON(maximumBodyBytes: 32)
    122     )
    123     try await store.saveSnapshot(
    124         RadrootsBackgroundTransferSnapshot(request: bodyRequest, state: .running)
    125     )
    126     await coordinator.complete(
    127         identifier: bodyRequest.identifier,
    128         completion: RadrootsTransferCompletion(
    129             platformError: CancellationError(),
    130             stagedDownloadResult: nil,
    131             httpResult: RadrootsBackgroundHTTPResult(
    132                 statusCode: 200, mediaType: "application/json", body: nil, bodyExceeded: true
    133             ),
    134             bytesTransferred: 10,
    135             totalBytesExpected: 10
    136         )
    137     )
    138     let oversized = try #require(
    139         try await store.loadSnapshots().first { $0.identifier == bodyRequest.identifier }
    140     )
    141     #expect(oversized.state == .failed)
    142     #expect(oversized.failure == .responseTooLarge)
    143     #expect(oversized.possibleRemoteOrphan)
    144 }
    145 
    146 @Test func appleBackgroundTransferCoordinatorDoesNotResurrectCancelledTransfer() async throws {
    147     let roots = try appleTransferRoots()
    148     let store = RadrootsInMemoryBackgroundTransferStore()
    149     let coordinator = RadrootsTransferCoordinator(
    150         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    151         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    152     )
    153     let request = try appleUploadRequest(identifier: "field.transfer.upload.cancelled")
    154     try await store.saveSnapshot(
    155         RadrootsBackgroundTransferSnapshot(request: request, state: .cancelled)
    156     )
    157 
    158     await coordinator.complete(
    159         identifier: request.identifier,
    160         completion: RadrootsTransferCompletion(
    161             platformError: nil,
    162             stagedDownloadResult: nil,
    163             httpResult: successfulHTTPResult(),
    164             bytesTransferred: 10,
    165             totalBytesExpected: 10
    166         )
    167     )
    168 
    169     #expect(try await store.loadSnapshots().first?.state == .cancelled)
    170 }
    171 
    172 @Test func appleBackgroundTransferRetainsReceiptWhileProtectedDataIsLocked() async throws {
    173     let roots = try appleTransferRoots()
    174     let protectedData = RadrootsProtectedDataProbe(state: .available)
    175     let store = RadrootsAppleBackgroundTransferStore(
    176         roots: roots, protectedData: RadrootsProtectedDataProvider { protectedData.state }
    177     )
    178     let request = try appleUploadRequest(identifier: "field.transfer.upload.locked")
    179     try await store.saveSnapshot(
    180         RadrootsBackgroundTransferSnapshot(request: request, state: .running)
    181     )
    182     let coordinator = RadrootsTransferCoordinator(
    183         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    184         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    185     )
    186 
    187     protectedData.state = .locked
    188     let pending = Task {
    189         await coordinator.complete(
    190             identifier: request.identifier,
    191             completion: RadrootsTransferCompletion(platformError: nil, stagedDownloadResult: nil,
    192                                                    httpResult: successfulHTTPResult(), bytesTransferred: 10,
    193                                                    totalBytesExpected: 10)
    194         )
    195     }
    196     for _ in 0 ..< 100 {
    197         if await coordinator.hasPendingReceipts {
    198             break
    199         }
    200         try await Task.sleep(for: .milliseconds(10))
    201     }
    202     #expect(await coordinator.hasPendingReceipts)
    203     let completion = RadrootsCompletionProbe()
    204     await coordinator.handleBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer") {
    205         completion.markCompleted()
    206     }
    207     await coordinator.finishBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer")
    208     #expect(!completion.completed)
    209     protectedData.state = .available
    210     await pending.value
    211     #expect(completion.completed)
    212     let reopened = RadrootsAppleBackgroundTransferStore(roots: roots)
    213     let recovered = try #require(try await reopened.loadSnapshots().first)
    214     #expect(recovered.state == .awaitingVerification)
    215     #expect(recovered.response?.statusCode == 200)
    216     #expect(!recovered.possibleRemoteOrphan)
    217 }
    218 
    219 @Test func appleBackgroundTransferCoordinatorInvokesStoredCompletionHandlerAfterFinishedEvents()
    220     async throws {
    221     let roots = try appleTransferRoots()
    222     let store = RadrootsInMemoryBackgroundTransferStore()
    223     let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    224     let coordinator = RadrootsTransferCoordinator(
    225         sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store,
    226         fileResolver: resolver
    227     )
    228     let completion = RadrootsCompletionProbe()
    229     let secondCompletion = RadrootsCompletionProbe()
    230     let unrelated = RadrootsCompletionProbe()
    231 
    232     await coordinator.handleBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer") {
    233         completion.markCompleted()
    234     }
    235     #expect(!completion.completed)
    236     await coordinator.handleBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer") {
    237         secondCompletion.markCompleted()
    238     }
    239 
    240     await coordinator.handleBackgroundEvents(identifier: "other.session") {
    241         unrelated.markCompleted()
    242     }
    243     #expect(unrelated.completed)
    244 
    245     await coordinator.finishBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer")
    246     #expect(completion.completed)
    247     #expect(secondCompletion.completed)
    248     await coordinator.finishBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer")
    249     #expect(completion.completionCount == 1)
    250     #expect(secondCompletion.completionCount == 1)
    251 }
    252 
    253 @Test func appleBackgroundTransferCoordinatorClaimsFinishBeforeHandlerExactlyOnce() async throws {
    254     let roots = try appleTransferRoots()
    255     let coordinator = RadrootsTransferCoordinator(
    256         sessionIdentifier: "org.radroots.field-ios.background.transfer",
    257         store: RadrootsInMemoryBackgroundTransferStore(),
    258         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots)
    259     )
    260     let completion = RadrootsCompletionProbe()
    261 
    262     await coordinator.finishBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer")
    263     await coordinator.handleBackgroundEvents(identifier: "org.radroots.field-ios.background.transfer") {
    264         completion.markCompleted()
    265     }
    266 
    267     #expect(completion.completionCount == 1)
    268 }
    269 
    270 @Test func appleBackgroundTransferRequiresExplicitVerificationSettlement() async throws {
    271     let request = try appleUploadRequest(identifier: "field.transfer.settlement")
    272     let response = try RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: nil, body: nil)
    273     let store = try RadrootsInMemoryBackgroundTransferStore(
    274         snapshots: [
    275             RadrootsBackgroundTransferSnapshot(
    276                 request: request,
    277                 state: .awaitingVerification,
    278                 response: response
    279             )
    280         ]
    281     )
    282     let transfer = RadrootsAppleBackgroundTransfer(
    283         store: store,
    284         adapters: RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 800)).adapters()
    285     )
    286 
    287     try await transfer.settle(request.identifier, verification: .accepted)
    288 
    289     #expect(try await store.loadSnapshots().first?.state == .completed)
    290     #expect(try await store.loadSnapshots().first?.response == response)
    291     await #expect(
    292         throws: RadrootsBackgroundTransferError.invalidRequest
    293     ) {
    294         try await transfer.settle(request.identifier, verification: .accepted)
    295     }
    296 }
    297 
    298 @Test func appleBackgroundTransferRejectedVerificationRemainsRecoverableWithoutClaimingSuccess()
    299     async throws {
    300     let request = try appleUploadRequest(identifier: "field.transfer.rejected")
    301     let store = try RadrootsInMemoryBackgroundTransferStore(
    302         snapshots: [
    303             RadrootsBackgroundTransferSnapshot(
    304                 request: request,
    305                 state: .awaitingVerification,
    306                 response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: nil, body: nil)
    307             )
    308         ]
    309     )
    310     let transfer = RadrootsAppleBackgroundTransfer(
    311         store: store,
    312         adapters: RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 850)).adapters()
    313     )
    314 
    315     try await transfer.settle(
    316         request.identifier,
    317         verification: .rejected(failure: .verificationRejected)
    318     )
    319 
    320     let snapshot = try #require(try await store.loadSnapshots().first)
    321     #expect(snapshot.state == .failed)
    322     #expect(snapshot.failure == .verificationRejected)
    323     #expect(snapshot.possibleRemoteOrphan)
    324     #expect(snapshot.response == nil)
    325 }