RadrootsAppleErrorSafetyTests.swift (11678B)
1 import Foundation 2 @testable import RadrootsKit 3 import RadrootsKitTesting 4 import Testing 5 6 @Test func publicAppleErrorsUseClosedPathFreeDescriptions() { 7 let errors = publicAppleErrorExamples 8 let forbidden = [ 9 "/Users/example/private.sqlite", 10 "https://secret.example.invalid/token", 11 "nsec1secretcanary", 12 ] 13 14 for error in errors { 15 let renderings = [ 16 String(describing: error), 17 String(reflecting: error), 18 (error as NSError).localizedDescription, 19 ] 20 #expect(renderings.allSatisfy { !$0.isEmpty }) 21 #expect( 22 renderings.allSatisfy { rendering in 23 forbidden.allSatisfy { !rendering.contains($0) } 24 } 25 ) 26 } 27 } 28 29 @Test func dependencyDiagnosticsCannotBecomePublicErrorText() { 30 let canary = NSError( 31 domain: "/Users/example/private.sqlite", 32 code: 7, 33 userInfo: [ 34 NSLocalizedDescriptionKey: 35 "https://secret.example.invalid/token nsec1secretcanary", 36 ] 37 ) 38 39 let captureError = RadrootsAppleMediaPicker.adapt(error: canary) 40 #expect(captureError == .transientFailure) 41 #expect(!String(reflecting: captureError).contains("private.sqlite")) 42 #expect(!(captureError.errorDescription ?? "").contains("secret.example.invalid")) 43 44 #if canImport(LocalAuthentication) 45 let presenceError = RadrootsAppleUserPresenceAdapters.adapt(error: canary) 46 #expect(presenceError == .permanentFailure) 47 #expect(!String(reflecting: presenceError).contains("nsec1secretcanary")) 48 #expect(!(presenceError.errorDescription ?? "").contains("secret.example.invalid")) 49 #endif 50 } 51 52 @Test func throwingAppleAdaptersMapDependencyDiagnosticsToClosedErrors() async throws { 53 try await verifyBackgroundTaskErrorMapping() 54 try await verifyPermissionErrorMapping() 55 try await verifyTransferErrorMapping() 56 } 57 58 @Test func fileSystemDiagnosticsCannotEscapeTheClosedFileErrorBoundary() throws { 59 let root = FileManager.default.temporaryDirectory 60 .appendingPathComponent("radroots-error-safety-\(UUID().uuidString)", isDirectory: true) 61 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) 62 let invalidDataRoot = root.appendingPathComponent("private.sqlite") 63 try Data("not a directory".utf8).write(to: invalidDataRoot) 64 let roots = try RadrootsAppleFileRoots( 65 appIdentifier: "org.radroots.error-safety", 66 dataRoot: invalidDataRoot, 67 cacheRoot: root.appendingPathComponent("cache", isDirectory: true), 68 temporaryRoot: root.appendingPathComponent("temporary", isDirectory: true) 69 ) 70 let access = RadrootsAppleFileAccess(roots: roots) 71 72 do { 73 try access.write( 74 .inline(Data("payload".utf8)), 75 to: RadrootsFileReference(scope: .data, relativePath: "secret.example.invalid") 76 ) 77 Issue.record("expected the invalid filesystem layout to fail") 78 } catch let error as RadrootsAppleFileError { 79 #expect(error == .permanentFailure) 80 #expect(!String(reflecting: error).contains("private.sqlite")) 81 #expect(!(error.errorDescription ?? "").contains("secret.example.invalid")) 82 } catch { 83 Issue.record("unexpected dependency-owned error: \(type(of: error))") 84 } 85 } 86 87 private func dependencyCanaryError() -> NSError { 88 NSError( 89 domain: "/Users/example/private.sqlite", 90 code: 9, 91 userInfo: [NSLocalizedDescriptionKey: "https://secret.example.invalid/token"] 92 ) 93 } 94 95 private actor ThrowingBackgroundTransferStore: RadrootsBackgroundTransferStore { 96 func withAdmission<Result: Sendable>( 97 for _: RadrootsBackgroundTransferIdentifier, 98 operation _: @escaping @Sendable () async throws -> Result 99 ) async throws -> Result { 100 throw dependencyCanaryError() 101 } 102 103 func admissionIsActive(for _: RadrootsBackgroundTransferIdentifier) async throws -> Bool { 104 throw dependencyCanaryError() 105 } 106 107 func compareExchangeSnapshot( 108 expected _: RadrootsBackgroundTransferSnapshot?, desired _: RadrootsBackgroundTransferSnapshot 109 ) async throws -> Bool { 110 throw dependencyCanaryError() 111 } 112 113 func loadSnapshots() async throws -> [RadrootsBackgroundTransferSnapshot] { 114 throw dependencyCanaryError() 115 } 116 117 func saveSnapshot(_: RadrootsBackgroundTransferSnapshot) async throws { 118 throw dependencyCanaryError() 119 } 120 121 func removeSnapshot(for _: RadrootsBackgroundTransferIdentifier) async throws { 122 throw dependencyCanaryError() 123 } 124 125 func removeAllSnapshots() async throws { 126 throw dependencyCanaryError() 127 } 128 } 129 130 private let publicAppleErrorExamples: [any Error] = [ 131 RadrootsCaptureIntakeError.invalidRequest, 132 RadrootsCaptureIntakeError.unavailable, 133 RadrootsCaptureIntakeError.permissionDenied, 134 RadrootsCaptureIntakeError.userCancelled, 135 RadrootsCaptureIntakeError.transientFailure, 136 RadrootsCaptureIntakeError.permanentFailure, 137 RadrootsBackgroundTransferError.invalidRequest, 138 RadrootsBackgroundTransferError.unavailable, 139 RadrootsBackgroundTransferError.transferFailure, 140 RadrootsBackgroundTransferError.persistenceFailure, 141 RadrootsDocumentInterchangeError.invalidRequest, 142 RadrootsDocumentInterchangeError.notFound, 143 RadrootsDocumentInterchangeError.userCancelled, 144 RadrootsDocumentInterchangeError.permissionDenied, 145 RadrootsDocumentInterchangeError.transientFailure, 146 RadrootsDocumentInterchangeError.permanentFailure, 147 RadrootsAppLocalStateResetError.invalidRequest, 148 RadrootsAppLocalStateResetError.fileSystemFailure, 149 RadrootsAppLocalStateResetError.keychainFailure, 150 RadrootsAppleMediaPreparationError.invalidRequest, 151 RadrootsAppleMediaPreparationError.unavailable, 152 RadrootsAppleMediaPreparationError.preparationFailure, 153 RadrootsTelemetryError.invalidRequest, 154 RadrootsExternalActionError.invalidRequest, 155 RadrootsExternalActionError.blockedByPolicy, 156 RadrootsExternalActionError.unavailable, 157 RadrootsExternalActionError.transientFailure, 158 RadrootsExternalActionError.permanentFailure, 159 RadrootsAppleFileError.invalidRequest, 160 RadrootsAppleFileError.notFound, 161 RadrootsAppleFileError.permissionDenied, 162 RadrootsAppleFileError.transientFailure, 163 RadrootsAppleFileError.permanentFailure, 164 RadrootsLocationServicesError.invalidRequest, 165 RadrootsLocationServicesError.permissionDenied, 166 RadrootsLocationServicesError.unavailable, 167 RadrootsLocationServicesError.timeout, 168 RadrootsLocationServicesError.cancelled, 169 RadrootsLocationServicesError.transientFailure, 170 RadrootsLocationServicesError.permanentFailure, 171 RadrootsUserPresenceError.invalidRequest, 172 RadrootsUserPresenceError.userCancelled, 173 RadrootsUserPresenceError.permissionDenied, 174 RadrootsUserPresenceError.unavailable, 175 RadrootsUserPresenceError.timeout, 176 RadrootsUserPresenceError.transientFailure, 177 RadrootsUserPresenceError.permanentFailure, 178 RadrootsBackgroundTaskError.invalidRequest, 179 RadrootsBackgroundTaskError.unavailable, 180 RadrootsBackgroundTaskError.schedulerFailure, 181 RadrootsAppleSecurityError.invalidRequest, 182 RadrootsAppleSecurityError.notFound, 183 RadrootsAppleSecurityError.permissionDenied, 184 RadrootsAppleSecurityError.userCancelled, 185 RadrootsAppleSecurityError.transientFailure, 186 RadrootsAppleSecurityError.unavailable, 187 RadrootsAppleSecurityError.permanentFailure, 188 RadrootsAppleSecurityError.keychainFailure, 189 RadrootsAppleMobileStoreError.invalidPublicKey, 190 RadrootsAppleMobileStoreError.protectedDataUnavailable, 191 RadrootsAppleMobileStoreError.invalidDirectoryLayout, 192 RadrootsAppleMobileStoreError.fileSystemFailure, 193 RadrootsVerifiedArtifactAccessError.invalidDescriptor, 194 RadrootsVerifiedArtifactAccessError.protectedDataUnavailable, 195 RadrootsVerifiedArtifactAccessError.artifactUnavailable, 196 RadrootsVerifiedArtifactAccessError.artifactCorrupt, 197 RadrootsVerifiedArtifactAccessError.fileSystemFailure, 198 ] 199 200 private func verifyBackgroundTaskErrorMapping() async throws { 201 let backgroundTasks = RadrootsAppleBackgroundTaskScheduler( 202 adapters: RadrootsAppleBackgroundTaskSchedulerAdapters( 203 now: { Date(timeIntervalSince1970: 1) }, 204 register: { _ in throw dependencyCanaryError() }, 205 submit: { _ in throw dependencyCanaryError() }, 206 cancel: { _ in }, 207 cancelAll: {}, 208 pendingTasks: { throw dependencyCanaryError() } 209 ) 210 ) 211 let registration = try RadrootsAppleBackgroundTaskRegistration( 212 identifier: RadrootsBackgroundTaskIdentifier("org.radroots.error-safety.refresh"), 213 kind: .appRefresh, 214 handler: { true } 215 ) 216 await #expect(throws: RadrootsBackgroundTaskError.schedulerFailure) { 217 _ = try await backgroundTasks.register(registration) 218 } 219 await #expect(throws: RadrootsBackgroundTaskError.schedulerFailure) { 220 _ = try await backgroundTasks.pendingTasks() 221 } 222 } 223 224 private func verifyPermissionErrorMapping() async throws { 225 let location = RadrootsAppleLocationServices( 226 adapters: RadrootsAppleLocationServicesAdapters( 227 now: { Date(timeIntervalSince1970: 1) }, 228 locationServicesEnabled: { true }, 229 authorizationStatus: { .notDetermined }, 230 requestWhenInUseAuthorization: { _ in throw dependencyCanaryError() }, 231 requestCurrentLocation: { _ in throw dependencyCanaryError() } 232 ) 233 ) 234 await #expect(throws: RadrootsLocationServicesError.permanentFailure) { 235 _ = try await location.requestWhenInUseAuthorization() 236 } 237 238 let presence = RadrootsAppleUserPresence( 239 adapters: RadrootsAppleUserPresenceAdapters( 240 currentStatus: { throw dependencyCanaryError() }, 241 verify: { _ in throw dependencyCanaryError() } 242 ) 243 ) 244 await #expect(throws: RadrootsUserPresenceError.permanentFailure) { 245 _ = try await presence.currentStatus() 246 } 247 let request = try RadrootsUserPresenceRequest(reason: "Verify local identity") 248 await #expect(throws: RadrootsUserPresenceError.permanentFailure) { 249 _ = try await presence.verify(request) 250 } 251 } 252 253 private func verifyTransferErrorMapping() async throws { 254 let transferRequest = try RadrootsBackgroundTransferRequest( 255 identifier: RadrootsBackgroundTransferIdentifier("error-safety.transfer"), 256 remoteURL: #require(URL(string: "https://radroots.org/error-safety")), 257 method: .get, 258 operation: .download( 259 destination: .file( 260 RadrootsFileReference(scope: .cache, relativePath: "error-safety.bin") 261 ) 262 ) 263 ) 264 let storageFailure = RadrootsAppleBackgroundTransfer( 265 store: ThrowingBackgroundTransferStore(), 266 adapters: .unavailable 267 ) 268 await #expect(throws: RadrootsBackgroundTransferError.persistenceFailure) { 269 _ = try await storageFailure.enqueue(transferRequest) 270 } 271 272 let adapterFailure = RadrootsAppleBackgroundTransfer( 273 store: RadrootsInMemoryBackgroundTransferStore(), 274 adapters: RadrootsAppleBackgroundTransferAdapters( 275 now: { Date(timeIntervalSince1970: 1) }, 276 enqueue: { _, _ in throw dependencyCanaryError() }, 277 cancel: { _ in }, 278 activeTransferIdentifiers: { [] }, 279 handleBackgroundEvents: { _, completion in completion() } 280 ) 281 ) 282 await #expect(throws: RadrootsBackgroundTransferError.transferFailure) { 283 _ = try await adapterFailure.enqueue(transferRequest) 284 } 285 }