RadrootsBackgroundTransferTestSupport.swift (6050B)
1 import Darwin 2 import Foundation 3 @testable import RadrootsKit 4 import RadrootsKitTesting 5 import Testing 6 7 actor RadrootsAppleBackgroundTransferProbe { 8 private let nowValue: Date 9 private let enqueueOutcome: Result<Void, RadrootsBackgroundTransferError> 10 private var activeIdentifiersValue: Set<RadrootsBackgroundTransferIdentifier> 11 private var enqueuedRequestsValue: [RadrootsBackgroundTransferRequest] 12 private var cancelledIdentifiersValue: [RadrootsBackgroundTransferIdentifier] 13 private var handledBackgroundEventIdentifiersValue: [String] 14 15 init( 16 now: Date = Date(timeIntervalSince1970: 0), 17 enqueueOutcome: Result<Void, RadrootsBackgroundTransferError> = .success(()), 18 activeIdentifiers: Set<RadrootsBackgroundTransferIdentifier> = [] 19 ) { 20 nowValue = now 21 self.enqueueOutcome = enqueueOutcome 22 activeIdentifiersValue = activeIdentifiers 23 enqueuedRequestsValue = [] 24 cancelledIdentifiersValue = [] 25 handledBackgroundEventIdentifiersValue = [] 26 } 27 28 nonisolated func adapters() -> RadrootsAppleBackgroundTransferAdapters { 29 RadrootsAppleBackgroundTransferAdapters( 30 now: { self.nowValue }, enqueue: { request, _ in try await self.enqueue(request) }, 31 cancel: { identifier in await self.cancel(identifier) }, 32 activeTransferIdentifiers: { await self.activeIdentifiers() }, 33 handleBackgroundEvents: { identifier, completionHandler in 34 await self.handleBackgroundEvents( 35 identifier: identifier, completionHandler: completionHandler 36 ) 37 } 38 ) 39 } 40 41 private func enqueue(_ request: RadrootsBackgroundTransferRequest) throws { 42 enqueuedRequestsValue.append(request) 43 switch enqueueOutcome { 44 case .success: activeIdentifiersValue.insert(request.identifier) 45 case let .failure(error): throw error 46 } 47 } 48 49 private func cancel(_ identifier: RadrootsBackgroundTransferIdentifier) { 50 cancelledIdentifiersValue.append(identifier) 51 activeIdentifiersValue.remove(identifier) 52 } 53 54 private func activeIdentifiers() -> Set<RadrootsBackgroundTransferIdentifier> { 55 activeIdentifiersValue 56 } 57 58 private func handleBackgroundEvents( 59 identifier: String, completionHandler: @escaping @Sendable () -> Void 60 ) { 61 handledBackgroundEventIdentifiersValue.append(identifier) 62 completionHandler() 63 } 64 65 var enqueuedRequests: [RadrootsBackgroundTransferRequest] { 66 enqueuedRequestsValue 67 } 68 69 var cancelledIdentifiers: [RadrootsBackgroundTransferIdentifier] { 70 cancelledIdentifiersValue 71 } 72 73 var handledBackgroundEventIdentifiers: [String] { 74 handledBackgroundEventIdentifiersValue 75 } 76 } 77 78 final class RadrootsCompletionProbe: @unchecked Sendable { 79 private let lock = NSLock() 80 private var completionCountValue = 0 81 82 func markCompleted() { 83 lock.lock() 84 completionCountValue += 1 85 lock.unlock() 86 } 87 88 var completed: Bool { 89 completionCount > 0 90 } 91 92 var completionCount: Int { 93 lock.lock() 94 defer { lock.unlock() } 95 return completionCountValue 96 } 97 } 98 99 final class RadrootsProtectedDataProbe: @unchecked Sendable { 100 private let lock = NSLock() 101 private var stateValue: RadrootsProtectedDataState 102 103 init(state: RadrootsProtectedDataState) { 104 stateValue = state 105 } 106 107 var state: RadrootsProtectedDataState { 108 get { 109 lock.lock() 110 defer { lock.unlock() } 111 return stateValue 112 } 113 set { 114 lock.lock() 115 stateValue = newValue 116 lock.unlock() 117 } 118 } 119 } 120 121 func appleTransferRequest(identifier: String) throws -> RadrootsBackgroundTransferRequest { 122 try RadrootsBackgroundTransferRequest( 123 identifier: RadrootsBackgroundTransferIdentifier(identifier), 124 remoteURL: URL(string: "https://radroots.org/\(identifier).json")!, 125 method: .get, 126 operation: .download( 127 destination: .file(RadrootsFileReference(scope: .cache, relativePath: "\(identifier).json")) 128 ) 129 ) 130 } 131 132 func appleUploadRequest( 133 identifier: String, 134 responsePolicy: RadrootsBackgroundTransferResponsePolicy = .discard, 135 maximumTransferBytes: UInt64 = RadrootsBackgroundTransferRequest.defaultMaximumTransferBytes 136 ) throws 137 -> RadrootsBackgroundTransferRequest { 138 try RadrootsBackgroundTransferRequest( 139 identifier: RadrootsBackgroundTransferIdentifier(identifier), 140 remoteURL: URL(string: "https://radroots.org/\(identifier).json")!, 141 method: .put, 142 operation: .upload( 143 source: .file(RadrootsFileReference(scope: .cache, relativePath: "\(identifier).json")) 144 ), 145 responsePolicy: responsePolicy, maximumTransferBytes: maximumTransferBytes 146 ) 147 } 148 149 func successfulHTTPResult() -> RadrootsBackgroundHTTPResult { 150 RadrootsBackgroundHTTPResult(statusCode: 200, mediaType: nil, body: nil, bodyExceeded: false) 151 } 152 153 func appleTransferRoots() throws -> RadrootsAppleFileRoots { 154 let unresolvedRoot = FileManager.default.temporaryDirectory.appendingPathComponent( 155 "radroots-apple-background-transfer-\(UUID().uuidString)", isDirectory: true 156 ) 157 try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: false) 158 guard let resolvedPointer = unresolvedRoot.path.withCString({ Darwin.realpath($0, nil) }) else { 159 throw RadrootsBackgroundTransferError.persistenceFailure 160 } 161 defer { Darwin.free(resolvedPointer) } 162 let root = URL(fileURLWithPath: String(cString: resolvedPointer), isDirectory: true) 163 return try RadrootsAppleFileRoots( 164 appIdentifier: "org.radroots.tests", 165 dataRoot: root.appendingPathComponent("data", isDirectory: true), 166 cacheRoot: root.appendingPathComponent("cache", isDirectory: true), 167 temporaryRoot: root.appendingPathComponent("tmp", isDirectory: true) 168 ) 169 }