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 }