field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraPermissionRecoveryTests.swift (6191B)


      1 import Foundation
      2 @testable import TeraApp
      3 import XCTest
      4 
      5 @MainActor
      6 final class TeraPermissionRecoveryTests: XCTestCase {
      7   func testSharedCoordinatorKeepsLibraryIndependentAndNeverCapturesDeniedRestrictedOrUnavailable() async throws {
      8     let fixture = try OfflineMediaFixture()
      9     defer { fixture.remove() }
     10     for access in TeraCameraAccess.allCases {
     11       let coordinator = fixture.coordinator(cameraAccess: { access })
     12       let support = try await coordinator.support()
     13       XCTAssertTrue(support.library)
     14       XCTAssertEqual(support.cameraAccess, access)
     15       XCTAssertEqual(support.camera, access == .authorized || access == .notDetermined)
     16       let imported = try await coordinator.importImages(limit: 1)
     17       XCTAssertEqual(imported.count, 1)
     18       if support.camera {
     19         let photo = try await coordinator.captureImage()
     20         XCTAssertNil(photo.remoteURL)
     21       } else {
     22         do {
     23           _ = try await coordinator.captureImage()
     24           XCTFail("Denied capability cannot call the picker")
     25         } catch { XCTAssertTrue(error is TeraRuntimeFailure) }
     26       }
     27     }
     28     let captures = await fixture.picker.captureRequests
     29     let imports = await fixture.picker.importRequests
     30     let uploads = await fixture.transfer.enqueueCount
     31     XCTAssertEqual(captures.count, 2)
     32     XCTAssertEqual(imports.count, 5)
     33     XCTAssertEqual(uploads, 0)
     34   }
     35 
     36   func testDeniedCameraPreservesEditingAndSaveUntilExplicitSettingsRecheck() async throws {
     37     let fixture = try OfflineMediaFixture()
     38     defer { fixture.remove() }
     39     let permission = CameraPermissionProbe(.denied)
     40     let backend = try TeraScopeBackend()
     41     let client = try await TeraScopeFixtures.client(backend)
     42     let store = TeraAddStore(runtimeClient: client, media: fixture.coordinator(cameraAccess: permission.read))
     43     store.configure(snapshot: TeraScopeFixtures.snapshot())
     44     await store.start()
     45     store.selectType(.createPhotoUpdate)
     46     store.updateForm(\.content, "Keep this unfinished description")
     47     let original = store.form
     48     await store.capturePhoto()
     49     await store.capturePhoto()
     50     XCTAssertEqual(store.form, original)
     51     XCTAssertEqual(store.mediaSupport.cameraAccess, .denied)
     52     XCTAssertFalse(store.mediaSupport.camera)
     53     XCTAssertTrue(store.mediaSupport.library)
     54     await store.save()
     55     XCTAssertEqual(store.savedComposer?.form.content, original.content)
     56     let deniedCaptures = await fixture.picker.captureRequests
     57     XCTAssertTrue(deniedCaptures.isEmpty)
     58     permission.set(.authorized)
     59     await store.recheckMediaAccess()
     60     XCTAssertTrue(store.mediaSupport.camera)
     61     XCTAssertEqual(store.form, original)
     62     await store.capturePhoto()
     63     XCTAssertEqual(store.form.media.count, 1)
     64     XCTAssertEqual(store.form.content, original.content)
     65     XCTAssertNil(store.form.media.first?.remoteURL)
     66     let captures = await fixture.picker.captureRequests
     67     XCTAssertEqual(captures.count, 1)
     68     store.stop()
     69     _ = try await client.stop()
     70   }
     71 
     72   func testLatePermissionRecheckCannotOverwriteNewerReadOrChangedAccount() async throws {
     73     let backend = try TeraScopeBackend()
     74     let client = try await TeraScopeFixtures.client(backend)
     75     let media = PausedPermissionMedia()
     76     let store = TeraAddStore(runtimeClient: client, media: media)
     77     store.configure(snapshot: TeraScopeFixtures.snapshot())
     78     let first = ResourceTestPause()
     79     await media.hold(first, access: .denied)
     80     let old = Task { await store.recheckMediaAccess() }
     81     await first.entered.wait()
     82     await store.recheckMediaAccess()
     83     XCTAssertTrue(store.mediaSupport.camera)
     84     await first.resume.open()
     85     await old.value
     86     XCTAssertTrue(store.mediaSupport.camera)
     87     let second = ResourceTestPause()
     88     await media.hold(second, access: .authorized)
     89     let stale = Task { await store.recheckMediaAccess() }
     90     await second.entered.wait()
     91     store.configure(snapshot: TeraScopeFixtures.snapshot(account: "b"))
     92     await second.resume.open()
     93     await stale.value
     94     XCTAssertEqual(store.mediaSupport, .unavailable)
     95     XCTAssertNil(store.message)
     96     store.stop()
     97     _ = try await client.stop()
     98   }
     99 
    100   func testLocalStateRecheckUsesRetryWithoutSubstitutingCustodyActions() {
    101     let identity = TeraAppIdentity(state: .locked, identityHandle: nil, publicKeyHex: nil,
    102                                    label: nil, signerGeneration: nil, recoveryCode: nil)
    103     for phase: TeraSessionPhase in [.protectedDataUnavailable(identity), .corruptIdentity(identity), .stopped] {
    104       XCTAssertTrue(status(phase).canRecheckLocalState)
    105     }
    106     for phase: TeraSessionPhase in [.starting, .identityRequired, .identityLocked(identity), .recoveryRequired(identity)] {
    107       XCTAssertFalse(status(phase).canRecheckLocalState)
    108     }
    109   }
    110 
    111   private func status(_ phase: TeraSessionPhase) -> RuntimeStatusView {
    112     RuntimeStatusView(phase: phase, retry: {}, createIdentity: {}, importIdentity: { _ in },
    113                       unlockIdentity: {}, recoverIdentity: {}, applyConfigurationReconfiguration: {})
    114   }
    115 }
    116 
    117 private final class CameraPermissionProbe: @unchecked Sendable {
    118   private let lock = NSLock()
    119   private var value: TeraCameraAccess
    120   init(_ value: TeraCameraAccess) {
    121     self.value = value
    122   }
    123 
    124   func read() -> TeraCameraAccess {
    125     lock.withLock { value }
    126   }
    127 
    128   func set(_ value: TeraCameraAccess) {
    129     lock.withLock { self.value = value }
    130   }
    131 }
    132 
    133 private actor PausedPermissionMedia: TeraAddMediaHandling {
    134   private var pending: (ResourceTestPause, TeraCameraAccess)?
    135   func hold(_ pause: ResourceTestPause, access: TeraCameraAccess) {
    136     pending = (pause, access)
    137   }
    138 
    139   func support() async -> TeraAddMediaSupport {
    140     guard let request = pending else { return .init(library: true, camera: true) }
    141     pending = nil
    142     await request.0.wait()
    143     return .init(library: true, camera: true, cameraAccess: request.1)
    144   }
    145 
    146   func importImages(limit _: Int) throws -> [TeraPreparedMedia] {
    147     throw TeraComposerAcknowledgment.unconfirmed
    148   }
    149 
    150   func captureImage() throws -> TeraPreparedMedia {
    151     throw TeraComposerAcknowledgment.unconfirmed
    152   }
    153 
    154   func open(_: [TeraPreparedMedia]) throws -> TeraOpenedMedia {
    155     throw TeraComposerAcknowledgment.unconfirmed
    156   }
    157 }