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 }