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

RadrootsNativeAdmissionTests.swift (14736B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import RadrootsKitTesting
      5 import Testing
      6 
      7 @Test func nativeAdmissionReservesBeforeAwaitAndPreservesQueuedOwnership() async throws {
      8     let fixture = try NativeAdmissionFixture()
      9     defer { fixture.remove() }
     10     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     11     let gate = NativeAdmissionGate()
     12     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: gate.adapters())
     13     let request = try nativeAdmissionRequest()
     14     let first = Task { try await transfer.enqueue(request) }
     15     await gate.waitUntilEntered()
     16     await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) {
     17         _ = try await transfer.enqueue(request)
     18     }
     19     let conflicting = try nativeAdmissionRequest(remote: "https://example.org/other")
     20     await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) {
     21         _ = try await transfer.enqueue(conflicting)
     22     }
     23     let secondStore = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
     24     let secondOwner = RadrootsAppleBackgroundTransfer(store: secondStore, adapters: gate.adapters())
     25     #expect(try await secondOwner.snapshots().first?.state == .queued)
     26     await #expect(throws: RadrootsBackgroundTransferError.invalidRequest) {
     27         _ = try await secondOwner.enqueue(request)
     28     }
     29     let queued = try #require(try await transfer.snapshots().first)
     30     #expect(queued.state == .queued)
     31     #expect(queued.request.headers.isEmpty && queued.request.metadata.isEmpty)
     32     #expect(queued.executionID != nil)
     33     await gate.finish()
     34     #expect(try await first.value.identifier == request.identifier)
     35     #expect(await gate.admittedCount == 1)
     36     #expect(try await !secondStore.admissionIsActive(for: request.identifier))
     37     #expect(try await transfer.snapshot(for: request.identifier)?.state == .running)
     38 }
     39 
     40 @Test func nativeAdmissionDifferentIdentifiersCanProceedWhileAnotherAwaitIsHeld() async throws {
     41     let store = RadrootsInMemoryBackgroundTransferStore()
     42     let firstGate = NativeAdmissionGate()
     43     let secondGate = NativeAdmissionGate()
     44     let first = RadrootsAppleBackgroundTransfer(store: store, adapters: firstGate.adapters())
     45     let second = RadrootsAppleBackgroundTransfer(store: store, adapters: secondGate.adapters())
     46     let original = try nativeAdmissionRequest()
     47     let other = try RadrootsBackgroundTransferRequest(
     48         identifier: RadrootsBackgroundTransferIdentifier("native.admission.other"),
     49         remoteURL: original.remoteURL, method: original.method, operation: original.operation
     50     )
     51     let firstTask = Task { try await first.enqueue(original) }
     52     await firstGate.waitUntilEntered()
     53     let secondTask = Task { try await second.enqueue(other) }
     54     await secondGate.waitUntilEntered()
     55     #expect(try await store.loadSnapshots().count == 2)
     56     await secondGate.finish()
     57     #expect(try await secondTask.value.identifier == other.identifier)
     58     await firstGate.finish()
     59     #expect(try await firstTask.value.identifier == original.identifier)
     60     #expect(await firstGate.admittedCount == 1)
     61     #expect(await secondGate.admittedCount == 1)
     62 }
     63 
     64 @Test func nativeAdmissionCancellationWhileEnqueueSuspendsCancelsLateTask() async throws {
     65     let store = RadrootsInMemoryBackgroundTransferStore()
     66     let gate = NativeAdmissionGate()
     67     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: gate.adapters())
     68     let request = try nativeAdmissionRequest()
     69     let pending = Task { try await transfer.enqueue(request) }
     70     await gate.waitUntilEntered()
     71     try await transfer.cancel(request.identifier)
     72     await gate.finish()
     73     _ = try await pending.value
     74     #expect(await gate.active.isEmpty)
     75     let snapshot = try #require(try await store.loadSnapshots().first)
     76     #expect(snapshot.state == .cancelled && snapshot.possibleRemoteOrphan)
     77 }
     78 
     79 @Test func nativeAdmissionErrorAfterEffectPreservesUnknownOutcomeAndExistingReceipt() async throws {
     80     let store = RadrootsInMemoryBackgroundTransferStore()
     81     let request = try nativeAdmissionRequest()
     82     let adapters = RadrootsAppleBackgroundTransferAdapters(
     83         enqueue: { _, executionID in
     84             let existing = try #require(try await store.loadSnapshots().first)
     85             #expect(existing.executionID == executionID)
     86             let completed = try RadrootsBackgroundTransferSnapshot(
     87                 request: existing.request, state: .awaitingVerification,
     88                 response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: nil, body: nil),
     89                 executionID: executionID
     90             )
     91             #expect(try await store.compareExchangeSnapshot(expected: existing, desired: completed))
     92             throw RadrootsBackgroundTransferError.transferFailure
     93         }, cancel: { _ in }, activeTransferIdentifiers: { [] },
     94         handleBackgroundEvents: { _, completion in completion() }
     95     )
     96     let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: adapters)
     97     await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { _ = try await transfer.enqueue(request) }
     98     #expect(try await store.loadSnapshots().first?.state == .awaitingVerification)
     99     let lost = try RadrootsBackgroundTransferSnapshot(request: request, state: .queued, executionID: UUID())
    100     let recoveredStore = RadrootsInMemoryBackgroundTransferStore(snapshots: [lost])
    101     let recovered = RadrootsAppleBackgroundTransfer(store: recoveredStore, adapters: adapters)
    102     let interrupted = try #require(try await recovered.snapshots().first)
    103     #expect(interrupted.state == .interrupted && interrupted.possibleRemoteOrphan)
    104 }
    105 
    106 @Test func nativeAdmissionStaleAndDuplicateCallbacksCannotOverwriteNewAttempt() async throws {
    107     let fixture = try NativeAdmissionFixture()
    108     defer { fixture.remove() }
    109     let request = try nativeAdmissionRequest()
    110     let generation = UUID()
    111     let running = try RadrootsBackgroundTransferSnapshot(request: request, state: .running, executionID: generation)
    112     let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [running])
    113     let coordinator = RadrootsTransferCoordinator(
    114         sessionIdentifier: "tests", store: store,
    115         fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: fixture.roots)
    116     )
    117     let response = RadrootsBackgroundHTTPResult(statusCode: 200, mediaType: nil, body: nil, bodyExceeded: false)
    118     await coordinator.complete(
    119         identifier: request.identifier,
    120         completion: RadrootsTransferCompletion(platformError: nil, stagedDownloadResult: nil, httpResult: response,
    121                                                bytesTransferred: 3, totalBytesExpected: 3),
    122         executionID: UUID()
    123     )
    124     #expect(try await store.loadSnapshots().first == running)
    125     await coordinator.complete(
    126         identifier: request.identifier,
    127         completion: RadrootsTransferCompletion(platformError: nil, stagedDownloadResult: nil, httpResult: response,
    128                                                bytesTransferred: 3, totalBytesExpected: 3),
    129         executionID: generation
    130     )
    131     let completed = try #require(try await store.loadSnapshots().first)
    132     #expect(completed.state == .awaitingVerification && completed.executionID == generation)
    133     await coordinator.updateProgress(identifier: request.identifier, bytesTransferred: 1, totalBytesExpected: 3,
    134                                      executionID: generation)
    135     await coordinator.complete(
    136         identifier: request.identifier,
    137         completion: RadrootsTransferCompletion(platformError: RadrootsBackgroundTransferError.transferFailure,
    138                                                stagedDownloadResult: nil, httpResult: response, bytesTransferred: 1,
    139                                                totalBytesExpected: 3),
    140         executionID: generation
    141     )
    142     #expect(try await store.loadSnapshots().first == completed)
    143 }
    144 
    145 @Test func nativeAdmissionPersistsExactLeaseAndGenerationAcrossRestart() async throws {
    146     let fixture = try NativeAdmissionFixture()
    147     defer { fixture.remove() }
    148     let file = RadrootsFileReference(scope: .cache, relativePath: "body")
    149     let bytes = Data("exact upload bytes".utf8)
    150     let fileOwner = RadrootsAppleFileAccess(roots: fixture.roots)
    151     try fileOwner.write(.inline(bytes), to: file)
    152     let request = try nativeAdmissionRequest(expectedSHA256: RadrootsAppleFileDigest.sha256(bytes))
    153     let generation = UUID()
    154     let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: fixture.roots)
    155     let lease = try resolver.prepareUploadLease(for: request, executionID: generation, existing: nil)
    156     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
    157     let queued = try RadrootsBackgroundTransferSnapshot(
    158         request: request,
    159         executionID: generation,
    160         uploadLease: lease.blob
    161     )
    162     #expect(try await store.compareExchangeSnapshot(expected: nil, desired: queued))
    163     try fileOwner.reset(scope: .cache)
    164     let reopened = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
    165     let saved = try #require(try await reopened.loadSnapshots().first)
    166     #expect(saved.request.headers.isEmpty && saved.request.metadata.isEmpty)
    167     #expect(saved.executionID == generation && saved.uploadLease == lease.blob)
    168     let recovered = try resolver.prepareUploadLease(
    169         for: saved.request,
    170         executionID: generation,
    171         existing: saved.uploadLease
    172     )
    173     #expect(recovered == lease)
    174     #expect(try Data(contentsOf: recovered.fileURL) == bytes)
    175     let descriptor = RadrootsBackgroundURLTaskDescriptor(request: request, executionID: generation)
    176     #expect(RadrootsBackgroundURLTaskDescriptor(taskDescription: descriptor.taskDescription) == descriptor)
    177     #expect(RadrootsBackgroundURLTaskDescriptor(taskDescription: "radroots-transfer-v2|bad|0|0|invalid") == nil)
    178     try resolver.releaseUploadLease(executionID: generation)
    179     #expect(!FileManager.default.fileExists(atPath: lease.fileURL.path))
    180 }
    181 
    182 @Test func nativeAdmissionStoreClaimsOnceAndRejectsStaleTransition() async throws {
    183     let fixture = try NativeAdmissionFixture()
    184     defer { fixture.remove() }
    185     let request = try nativeAdmissionRequest()
    186     let results = await withTaskGroup(of: Bool.self) { group in
    187         for _ in 0 ..< 8 {
    188             group.addTask {
    189                 let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
    190                 let desired = try? RadrootsBackgroundTransferSnapshot(request: request, executionID: UUID())
    191                 guard let desired else { return false }
    192                 return await (try? store.compareExchangeSnapshot(expected: nil, desired: desired)) ?? false
    193             }
    194         }
    195         var results: [Bool] = []
    196         for await result in group {
    197             results.append(result)
    198         }
    199         return results
    200     }
    201     #expect(results.filter(\.self).count == 1)
    202     let store = RadrootsAppleBackgroundTransferStore(roots: fixture.roots)
    203     let original = try #require(try await store.loadSnapshots().first)
    204     let cancelled = try original.transitioned(to: .cancelled, at: Date(), possibleRemoteOrphan: true)
    205     #expect(try await store.compareExchangeSnapshot(expected: original, desired: cancelled))
    206     #expect(try await !store.compareExchangeSnapshot(expected: original,
    207                                                      desired: original.transitioned(to: .running, at: Date())))
    208     #expect(try await store.loadSnapshots().first == cancelled)
    209 }
    210 
    211 private actor NativeAdmissionGate {
    212     private var entered = false
    213     private var observer: CheckedContinuation<Void, Never>?
    214     private var pending: CheckedContinuation<Void, Never>?
    215     private(set) var active: Set<RadrootsBackgroundTransferIdentifier> = []
    216     private(set) var admittedCount = 0
    217 
    218     nonisolated func adapters() -> RadrootsAppleBackgroundTransferAdapters {
    219         RadrootsAppleBackgroundTransferAdapters(enqueue: { request, _ in await self.enqueue(request) },
    220                                                 cancel: { identifier in await self.cancel(identifier) },
    221                                                 activeTransferIdentifiers: { await self.active },
    222                                                 handleBackgroundEvents: { _, completion in completion() })
    223     }
    224 
    225     func waitUntilEntered() async {
    226         if entered {
    227             return
    228         }
    229         await withCheckedContinuation { observer = $0 }
    230     }
    231 
    232     func finish() {
    233         pending?.resume(); pending = nil
    234     }
    235 
    236     func cancel(_ identifier: RadrootsBackgroundTransferIdentifier) {
    237         active.remove(identifier)
    238     }
    239 
    240     private func enqueue(_ request: RadrootsBackgroundTransferRequest) async {
    241         admittedCount += 1
    242         entered = true
    243         observer?.resume()
    244         observer = nil
    245         await withCheckedContinuation { pending = $0 }
    246         active.insert(request.identifier)
    247     }
    248 }
    249 
    250 private func nativeAdmissionRequest(
    251     remote: String = "https://example.org/upload", expectedSHA256: String? = nil
    252 ) throws -> RadrootsBackgroundTransferRequest {
    253     try RadrootsBackgroundTransferRequest(identifier: RadrootsBackgroundTransferIdentifier("native.admission"),
    254                                           remoteURL: #require(URL(string: remote)), method: .put,
    255                                           operation: .upload(source: .file(RadrootsFileReference(
    256                                               scope: .cache,
    257                                               relativePath: "body"
    258                                           ))),
    259                                           headers: ["Authorization": "test-only-authority"],
    260                                           metadata: ["label": "test-only-private-metadata"],
    261                                           expectedSourceSHA256: expectedSHA256)
    262 }
    263 
    264 private struct NativeAdmissionFixture {
    265     let base: URL
    266     let roots: RadrootsAppleFileRoots
    267     init() throws {
    268         let raw = FileManager.default.temporaryDirectory.appendingPathComponent("native-admission-\(UUID().uuidString)")
    269         try FileManager.default.createDirectory(at: raw, withIntermediateDirectories: true)
    270         let pointer = try #require(raw.path.withCString { Darwin.realpath($0, nil) })
    271         defer { Darwin.free(pointer) }
    272         base = URL(fileURLWithPath: String(cString: pointer), isDirectory: true)
    273         roots = try RadrootsAppleFileRoots(
    274             appIdentifier: "org.radroots.tests",
    275             dataRoot: base.appendingPathComponent("data"),
    276             cacheRoot: base.appendingPathComponent("cache"),
    277             temporaryRoot: base.appendingPathComponent("tmp")
    278         )
    279     }
    280 
    281     func remove() {
    282         try? FileManager.default.removeItem(at: base)
    283     }
    284 }