RadrootsAppleBackgroundTransferTests.swift (12401B)
1 import Darwin 2 import Foundation 3 @testable import RadrootsKit 4 import RadrootsKitTesting 5 import Testing 6 7 @Test func appleBackgroundTransferPersistsRunningSnapshotAfterEnqueue() async throws { 8 let store = RadrootsInMemoryBackgroundTransferStore() 9 let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 100)) 10 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 11 let request = try appleTransferRequest(identifier: "field.transfer.enqueue") 12 13 let handle = try await transfer.enqueue(request) 14 15 #expect(handle.identifier == request.identifier) 16 #expect(await probe.enqueuedRequests == [request]) 17 let snapshot = try await transfer.snapshot(for: request.identifier) 18 #expect(snapshot?.state == .running) 19 #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 100)) 20 } 21 22 @Test func appleBackgroundTransferRejectsDuplicateIdentifiersBeforeAdapterMutation() async throws { 23 let store = RadrootsInMemoryBackgroundTransferStore() 24 let probe = RadrootsAppleBackgroundTransferProbe() 25 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 26 let request = try appleTransferRequest(identifier: "field.transfer.duplicate") 27 28 _ = try await transfer.enqueue(request) 29 await #expect( 30 throws: RadrootsBackgroundTransferError.invalidRequest 31 ) { 32 _ = try await transfer.enqueue(request) 33 } 34 35 #expect(await probe.enqueuedRequests == [request]) 36 } 37 38 @Test func appleBackgroundTransferRecordsFailedSnapshotWhenAdapterRejectsEnqueue() async throws { 39 let store = RadrootsInMemoryBackgroundTransferStore() 40 let probe = RadrootsAppleBackgroundTransferProbe( 41 now: Date(timeIntervalSince1970: 200), 42 enqueueOutcome: .failure(.transferFailure) 43 ) 44 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 45 let request = try appleTransferRequest(identifier: "field.transfer.failed") 46 47 await #expect( 48 throws: RadrootsBackgroundTransferError.transferFailure 49 ) { 50 _ = try await transfer.enqueue(request) 51 } 52 53 let snapshot = try await transfer.snapshot(for: request.identifier) 54 #expect(snapshot?.state == .failed) 55 #expect(snapshot?.failure == .enqueueFailed) 56 #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 200)) 57 } 58 59 @Test func appleBackgroundTransferCancelsThroughAdapterAndUpdatesStore() async throws { 60 let store = RadrootsInMemoryBackgroundTransferStore() 61 let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 300)) 62 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 63 let request = try appleTransferRequest(identifier: "field.transfer.cancel") 64 65 _ = try await transfer.enqueue(request) 66 try await transfer.cancel(request.identifier) 67 68 #expect(await probe.cancelledIdentifiers == [request.identifier]) 69 #expect(try await transfer.snapshot(for: request.identifier)?.state == .cancelled) 70 #expect( 71 try await transfer.snapshot(for: request.identifier)?.updatedAt 72 == Date(timeIntervalSince1970: 300) 73 ) 74 } 75 76 @Test func appleBackgroundTransferReconcilesQueuedSnapshotsWithActiveRecoveredTasks() async throws { 77 let request = try appleTransferRequest(identifier: "field.transfer.recovered") 78 let queued = try RadrootsBackgroundTransferSnapshot( 79 request: request, state: .queued, updatedAt: Date(timeIntervalSince1970: 1) 80 ) 81 let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [queued]) 82 let probe = RadrootsAppleBackgroundTransferProbe( 83 now: Date(timeIntervalSince1970: 400), activeIdentifiers: [request.identifier] 84 ) 85 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 86 87 let snapshots = try await transfer.snapshots() 88 89 #expect(snapshots.count == 1) 90 #expect(snapshots.first?.state == .running) 91 #expect(snapshots.first?.updatedAt == Date(timeIntervalSince1970: 400)) 92 #expect(try await store.loadSnapshots().first?.state == .running) 93 } 94 95 @Test func appleBackgroundTransferMarksMissingRecoveredUploadInterrupted() async throws { 96 let request = try appleUploadRequest(identifier: "field.transfer.interrupted") 97 let running = try RadrootsBackgroundTransferSnapshot( 98 request: request, state: .running, 99 progress: RadrootsBackgroundTransferProgress(bytesTransferred: 5, totalBytesExpected: 10), 100 updatedAt: Date(timeIntervalSince1970: 1) 101 ) 102 let store = RadrootsInMemoryBackgroundTransferStore(snapshots: [running]) 103 let probe = RadrootsAppleBackgroundTransferProbe(now: Date(timeIntervalSince1970: 401)) 104 let transfer = RadrootsAppleBackgroundTransfer(store: store, adapters: probe.adapters()) 105 106 let snapshot = try #require(try await transfer.snapshots().first) 107 108 #expect(snapshot.state == .interrupted) 109 #expect(snapshot.failure == .interrupted) 110 #expect(snapshot.possibleRemoteOrphan) 111 #expect(snapshot.updatedAt == Date(timeIntervalSince1970: 401)) 112 } 113 114 @Test func appleBackgroundTransferForwardsBackgroundCompletionHandlers() async { 115 let probe = RadrootsAppleBackgroundTransferProbe() 116 let transfer = RadrootsAppleBackgroundTransfer( 117 store: RadrootsInMemoryBackgroundTransferStore(), adapters: probe.adapters() 118 ) 119 let completion = RadrootsCompletionProbe() 120 121 await transfer.handleEventsForBackgroundURLSession( 122 identifier: "org.radroots.field-ios.background.transfer" 123 ) { 124 completion.markCompleted() 125 } 126 127 #expect( 128 await probe.handledBackgroundEventIdentifiers == ["org.radroots.field-ios.background.transfer"] 129 ) 130 #expect(completion.completed) 131 } 132 133 @Test func appleBackgroundTransferCoordinatorMovesCompletedDownloadToDestination() async throws { 134 let roots = try appleTransferRoots() 135 let store = RadrootsInMemoryBackgroundTransferStore() 136 let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots) 137 let coordinator = RadrootsTransferCoordinator( 138 sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store, 139 fileResolver: resolver, 140 now: { Date(timeIntervalSince1970: 500) } 141 ) 142 let request = try appleTransferRequest(identifier: "field.transfer.completed") 143 let running = try RadrootsBackgroundTransferSnapshot( 144 request: request, state: .running, updatedAt: Date(timeIntervalSince1970: 1) 145 ) 146 try await store.saveSnapshot(running) 147 let stagingRoot = roots.temporaryRoot.appendingPathComponent( 148 "background-transfer-tests", isDirectory: true 149 ) 150 try FileManager.default.createDirectory(at: stagingRoot, withIntermediateDirectories: true) 151 let stagedFile = stagingRoot.appendingPathComponent("download.bin") 152 let payload = Data("downloaded".utf8) 153 try payload.write(to: stagedFile) 154 155 await coordinator.complete( 156 identifier: request.identifier, 157 completion: RadrootsTransferCompletion( 158 platformError: nil, 159 stagedDownloadResult: .file(stagedFile), 160 httpResult: RadrootsBackgroundHTTPResult( 161 statusCode: 200, mediaType: "image/png", body: nil, bodyExceeded: false 162 ), 163 bytesTransferred: 0, 164 totalBytesExpected: nil 165 ) 166 ) 167 168 let snapshot = try await store.loadSnapshots().first 169 let destination = try resolver.resolve( 170 .file(RadrootsFileReference(scope: .cache, relativePath: "field.transfer.completed.json")) 171 ) 172 #expect(snapshot?.state == .awaitingVerification) 173 #expect(snapshot?.progress.bytesTransferred == Int64(payload.count)) 174 #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 500)) 175 #expect(snapshot?.response?.statusCode == 200) 176 #expect(snapshot?.downloadedArtifact?.byteSize == UInt64(payload.count)) 177 #expect(snapshot?.downloadedArtifact?.mediaType == "image/png") 178 let downloadedDigest = try RadrootsAppleFileDigest.sha256(at: destination) 179 #expect(snapshot?.downloadedArtifact?.sha256 == downloadedDigest) 180 #expect(try Data(contentsOf: destination) == payload) 181 #expect(!FileManager.default.fileExists(atPath: stagedFile.path)) 182 } 183 184 @Test func appleBackgroundTransferCoordinatorRejectsMalformedResponseMediaType() async throws { 185 let roots = try appleTransferRoots() 186 let store = RadrootsInMemoryBackgroundTransferStore() 187 let coordinator = RadrootsTransferCoordinator( 188 sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store, 189 fileResolver: RadrootsAppleBackgroundTransferFileResolver(roots: roots) 190 ) 191 let request = try appleUploadRequest(identifier: "field.transfer.malformed-media-type") 192 try await store.saveSnapshot( 193 RadrootsBackgroundTransferSnapshot(request: request, state: .running) 194 ) 195 196 await coordinator.complete( 197 identifier: request.identifier, 198 completion: RadrootsTransferCompletion( 199 platformError: nil, 200 stagedDownloadResult: nil, 201 httpResult: RadrootsBackgroundHTTPResult( 202 statusCode: 200, mediaType: nil, body: nil, bodyExceeded: false, 203 mediaTypeWasMalformed: true 204 ), 205 bytesTransferred: 10, 206 totalBytesExpected: 10 207 ) 208 ) 209 210 let snapshot = try #require(try await store.loadSnapshots().first) 211 #expect(snapshot.state == .failed) 212 #expect(snapshot.failure == .responseMediaType) 213 #expect(snapshot.possibleRemoteOrphan) 214 } 215 216 @Test func appleBackgroundTransferCoordinatorRecordsFailedDownloadSnapshot() async throws { 217 let roots = try appleTransferRoots() 218 let store = RadrootsInMemoryBackgroundTransferStore() 219 let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots) 220 let coordinator = RadrootsTransferCoordinator( 221 sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store, 222 fileResolver: resolver, 223 now: { Date(timeIntervalSince1970: 600) } 224 ) 225 let request = try appleTransferRequest(identifier: "field.transfer.download.failed") 226 try await store.saveSnapshot( 227 RadrootsBackgroundTransferSnapshot( 228 request: request, state: .running, updatedAt: Date(timeIntervalSince1970: 1) 229 ) 230 ) 231 232 await coordinator.complete( 233 identifier: request.identifier, 234 completion: RadrootsTransferCompletion( 235 platformError: nil, 236 stagedDownloadResult: .failure, 237 httpResult: successfulHTTPResult(), 238 bytesTransferred: 0, 239 totalBytesExpected: nil 240 ) 241 ) 242 243 let snapshot = try await store.loadSnapshots().first 244 #expect(snapshot?.state == .failed) 245 #expect(snapshot?.failure == .downloadStagingFailure) 246 #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 600)) 247 } 248 249 @Test func appleBackgroundTransferCoordinatorCompletesUploadWithProgress() async throws { 250 let roots = try appleTransferRoots() 251 let store = RadrootsInMemoryBackgroundTransferStore() 252 let resolver = RadrootsAppleBackgroundTransferFileResolver(roots: roots) 253 let coordinator = RadrootsTransferCoordinator( 254 sessionIdentifier: "org.radroots.field-ios.background.transfer", store: store, 255 fileResolver: resolver, 256 now: { Date(timeIntervalSince1970: 700) } 257 ) 258 let request = try appleUploadRequest(identifier: "field.transfer.upload.completed") 259 try await store.saveSnapshot( 260 RadrootsBackgroundTransferSnapshot( 261 request: request, state: .running, updatedAt: Date(timeIntervalSince1970: 1) 262 ) 263 ) 264 265 await coordinator.updateProgress( 266 identifier: request.identifier, bytesTransferred: 4, totalBytesExpected: 10 267 ) 268 await coordinator.complete( 269 identifier: request.identifier, 270 completion: RadrootsTransferCompletion( 271 platformError: nil, 272 stagedDownloadResult: nil, 273 httpResult: successfulHTTPResult(), 274 bytesTransferred: 10, 275 totalBytesExpected: 10 276 ) 277 ) 278 279 let snapshot = try await store.loadSnapshots().first 280 #expect(snapshot?.state == .awaitingVerification) 281 #expect(snapshot?.progress.bytesTransferred == 10) 282 #expect(snapshot?.progress.totalBytesExpected == 10) 283 #expect(snapshot?.updatedAt == Date(timeIntervalSince1970: 700)) 284 #expect(snapshot?.response?.statusCode == 200) 285 }