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

RadrootsTransferCapacityTests.swift (8252B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import Testing
      5 
      6 @Test func transferCapacityRetainsPriorOrAmbiguousReceiptWithoutSuccess() async throws {
      7     for phase in [RadrootsAtomicFile.Phase.beforeInstall, .beforeDirectorySync] {
      8         let fixture = try CapacityFixture()
      9         defer { fixture.remove() }
     10         let live = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     11         let request = try appleUploadRequest(identifier: "original")
     12         let original = try RadrootsBackgroundTransferSnapshot(request: request, state: .running, executionID: UUID())
     13         try await live.saveSnapshot(original)
     14         let receipt = try RadrootsBackgroundTransferSnapshot(request: request, state: .awaitingVerification,
     15             response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: nil, body: nil),
     16             executionID: original.executionID)
     17         let faulted = RadrootsAppleBackgroundTransferStore(roots: fixture.roots, persistence: capacityPersistence(phase))
     18         await #expect(throws: RadrootsBackgroundTransferError.spaceInsufficient) {
     19             _ = try await faulted.compareExchangeSnapshot(expected: original, desired: receipt)
     20         }
     21         let reopened = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     22         let observed = try #require(try await reopened.loadSnapshots().first)
     23         #expect(observed == (phase == .beforeDirectorySync ? receipt : original))
     24         if observed == original {
     25             #expect(try await reopened.compareExchangeSnapshot(expected: original, desired: receipt))
     26         } else {
     27             #expect(try await !reopened.compareExchangeSnapshot(expected: original, desired: receipt))
     28         }
     29         #expect(try await reopened.loadSnapshots() == [receipt])
     30     }
     31 }
     32 
     33 @Test func transferCapacityFailureDoesNotEnqueueOrReidentifyOriginalRequest() async throws {
     34     let fixture = try CapacityFixture()
     35     defer { fixture.remove() }
     36     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots, persistence: capacityPersistence(.beforeInstall))
     37     let probe = RadrootsAppleBackgroundTransferProbe()
     38     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters())
     39     let request = try appleUploadRequest(identifier: "original_request")
     40     await #expect(throws: RadrootsBackgroundTransferError.spaceInsufficient) { _ = try await transfer.enqueue(request) }
     41     #expect(await probe.enqueuedRequests.isEmpty)
     42     #expect(try await store.loadSnapshots().isEmpty)
     43     let live = RadrootsAppleBackgroundTransfer(store: RadrootsAppleBackgroundTransferStore(roots: fixture.roots),
     44                                              adapters: probe.adapters())
     45     let handle = try await live.enqueue(request)
     46     #expect(handle.identifier == request.identifier)
     47     #expect(await probe.enqueuedRequests == [request])
     48 }
     49 
     50 @Test func transferCapacityFromUploadLeaseReachesCallerAndRetainsAttempt() async throws {
     51     let fixture = try CapacityFixture()
     52     defer { fixture.remove() }
     53     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     54     let adapters = RadrootsAppleBackgroundTransferAdapters(enqueue: { _, _ in
     55         throw RadrootsAppleFileError.spaceInsufficient
     56     }, cancel: { _ in }, activeTransferIdentifiers: { [] }, handleBackgroundEvents: { _, done in done() })
     57     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: adapters)
     58     let request = try appleUploadRequest(identifier: "lease_failure")
     59     await #expect(throws: RadrootsBackgroundTransferError.spaceInsufficient) { _ = try await transfer.enqueue(request) }
     60     let retained = try #require(try await store.loadSnapshots().first)
     61     #expect(retained.identifier == request.identifier)
     62     #expect(retained.executionID != nil)
     63     #expect(retained.state == .failed)
     64     #expect(retained.possibleRemoteOrphan)
     65 }
     66 
     67 @Test func transferReceiptQuotaRetainsEveryPriorReceiptAndIsNotDiskCapacity() async throws {
     68     let fixture = try CapacityFixture()
     69     defer { fixture.remove() }
     70     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     71     let body = try JSONSerialization.data(withJSONObject: ["padding": String(repeating: "a", count: 65000)])
     72     var retained: [RadrootsBackgroundTransferSnapshot] = []
     73     var refused = false
     74     for index in 0 ..< 16 {
     75         let request = try appleUploadRequest(identifier: "receipt_\(index)", responsePolicy: .boundedJSON(maximumBodyBytes: 65536))
     76         let receipt = try RadrootsBackgroundTransferSnapshot(request: request, state: .awaitingVerification,
     77             response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: "application/json", body: body),
     78             executionID: UUID())
     79         do {
     80             try await store.saveSnapshot(receipt)
     81             retained.append(receipt)
     82         } catch let error as RadrootsBackgroundTransferError {
     83             #expect(error == .receiptCapacityExceeded)
     84             refused = true
     85             break
     86         }
     87     }
     88     #expect(refused)
     89     #expect(!retained.isEmpty)
     90     let reopened = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     91     #expect(try await reopened.loadSnapshots() == retained.sorted { $0.identifier < $1.identifier })
     92     // Cache purge cannot authorize deleting unresolved receipts or claim the
     93     // envelope quota has become available.
     94     try RadrootsAppleFileAccess(roots: fixture.roots).reset(scope: .cache)
     95     #expect(try await reopened.loadSnapshots() == retained.sorted { $0.identifier < $1.identifier })
     96 }
     97 
     98 @Test func transferCapacityRetainsPendingCallbackAndDefersAcknowledgement() async throws {
     99     let fixture = try CapacityFixture()
    100     defer { fixture.remove() }
    101     let gate = CapacityFaultGate()
    102     defer { gate.enabled = false }
    103     let persistence = RadrootsFilePersistence(install: { bytes, url, mode, readOnly in
    104         try RadrootsAtomicFile.installForTesting(bytes, at: url, mode: mode, readOnly: readOnly) { phase in
    105             if phase == .beforeInstall, gate.enabled { throw RadrootsAppleFileError.spaceInsufficient }
    106         }
    107     }, synchronize: { try RadrootsAtomicFile.synchronizeExisting(at: $0) })
    108     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots, persistence: persistence)
    109     let request = try appleUploadRequest(identifier: "pending_receipt")
    110     let original = try RadrootsBackgroundTransferSnapshot(request: request, state: .running)
    111     try await store.saveSnapshot(original)
    112     gate.enabled = true
    113     let coordinator = RadrootsTransferCoordinator(sessionIdentifier: "capacity.test", store: store,
    114         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: fixture.roots))
    115     let pending = Task {
    116         await coordinator.complete(identifier: request.identifier,
    117             completion: RadrootsTransferCompletion(platformError: nil, stagedDownloadResult: nil,
    118                 httpResult: successfulHTTPResult(), bytesTransferred: 10, totalBytesExpected: 10))
    119     }
    120     for _ in 0 ..< 100 {
    121         if await coordinator.hasPendingReceipts { break }
    122         try await Task.sleep(for: .milliseconds(10))
    123     }
    124     #expect(await coordinator.hasPendingReceipts)
    125     let completion = RadrootsCompletionProbe()
    126     await coordinator.handleBackgroundEvents(identifier: "capacity.test") { completion.markCompleted() }
    127     await coordinator.finishBackgroundEvents(identifier: "capacity.test")
    128     #expect(!completion.completed)
    129     #expect(try await store.loadSnapshots() == [original])
    130     gate.enabled = false
    131     await pending.value
    132     #expect(completion.completionCount == 1)
    133     let reopened = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
    134     let receipt = try #require(try await reopened.loadSnapshots().first)
    135     #expect(receipt.identifier == request.identifier)
    136     #expect(receipt.state == .awaitingVerification)
    137     #expect(receipt.response?.statusCode == 200)
    138 }
    139 
    140 /// The test mutates one Boolean across actor calls; every access holds this lock.
    141 private final class CapacityFaultGate: @unchecked Sendable {
    142     private let lock = NSLock()
    143     private var value = false
    144     var enabled: Bool {
    145         get { lock.lock(); defer { lock.unlock() }; return value }
    146         set { lock.lock(); value = newValue; lock.unlock() }
    147     }
    148 }