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

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 }