TeraRestoreRecoveryStoreTests.swift (4522B)
1 @testable import TeraApp 2 import XCTest 3 4 @MainActor 5 final class TeraRestoreRecoveryStoreTests: XCTestCase { 6 func testStatusAndReviewNeverResumeWithoutTheSeparateAction() async throws { 7 let backend = try TeraScopeBackend() 8 await backend.restoreStorage.configure(targets: []) 9 let client = try await TeraScopeFixtures.client(backend) 10 let store = TeraRestoreRecoveryStore(client: client) 11 await store.load() 12 XCTAssertEqual(store.status?.phase, .held) 13 XCTAssertNil(store.reviewedInventory) 14 await store.review() 15 XCTAssertEqual(store.status?.phase, .reviewed) 16 XCTAssertEqual(store.reviewedInventory, "reviewed") 17 let before = await backend.restoreStorage.calls 18 XCTAssertEqual(before, ["review"]) 19 await store.resume() 20 XCTAssertEqual(store.status?.phase, .resumed) 21 XCTAssertNil(store.reviewedInventory) 22 let after = await backend.restoreStorage.calls 23 XCTAssertEqual(after, ["review", "resume"]) 24 _ = try await client.stop() 25 } 26 27 func testOneActionChecksOneDestinationAndDoesNotStarveUncheckedWork() async throws { 28 let backend = try TeraScopeBackend() 29 await backend.restoreStorage.configure(targets: [ 30 TeraRestoreTarget(draftID: "offline", eventID: "a", targetFingerprint: "first", observation: .incomplete), 31 TeraRestoreTarget(draftID: "unchecked", eventID: "b", targetFingerprint: "second", observation: nil), 32 ]) 33 let client = try await TeraScopeFixtures.client(backend) 34 let store = TeraRestoreRecoveryStore(client: client) 35 await store.load() 36 await store.checkNext() 37 let first = await backend.restoreStorage.calls 38 XCTAssertEqual(first, ["check:unchecked"]) 39 XCTAssertNil(store.reviewedInventory) 40 await store.checkNext() 41 let second = await backend.restoreStorage.calls 42 XCTAssertEqual(second, ["check:unchecked", "check:offline"]) 43 _ = try await client.stop() 44 } 45 46 func testAccountInvalidationDiscardsLateReviewAndCannotExposeResume() async throws { 47 let backend = try TeraScopeBackend() 48 let pause = ResourceTestPause() 49 await backend.restoreStorage.configure(targets: [], pause: pause) 50 let client = try await TeraScopeFixtures.client(backend) 51 let store = TeraRestoreRecoveryStore(client: client) 52 await store.load() 53 let task = Task { await store.review() } 54 await pause.entered.wait() 55 store.invalidate() 56 await pause.resume.open() 57 await task.value 58 XCTAssertNil(store.status) 59 XCTAssertNil(store.reviewedInventory) 60 await store.resume() 61 let calls = await backend.restoreStorage.calls 62 XCTAssertEqual(calls, ["review"]) 63 _ = try await client.stop() 64 } 65 } 66 67 actor RestoreTestStorage { 68 private var value: TeraRestoreStatus? 69 private var pause: ResourceTestPause? 70 private(set) var calls: [String] = [] 71 func configure(targets: [TeraRestoreTarget], pause: ResourceTestPause? = nil) { 72 value = TeraRestoreStatus(attemptID: "fixture", phase: .held, targets: targets) 73 self.pause = pause 74 } 75 76 func status() -> TeraRestoreStatus? { 77 value 78 } 79 80 func check(_ target: TeraRestoreTarget) { 81 calls.append("check:\(target.draftID)") 82 guard let value else { return } 83 self.value = TeraRestoreStatus(attemptID: value.attemptID, phase: value.phase, targets: value.targets.map { 84 $0 == target ? TeraRestoreTarget(draftID: $0.draftID, eventID: $0.eventID, targetFingerprint: $0.targetFingerprint, observation: .notObserved) : $0 85 }) 86 } 87 88 func review() async throws -> String { 89 calls.append("review") 90 if let pause { 91 await pause.wait() 92 } 93 guard let value else { throw TeraComposerAcknowledgment.unconfirmed } 94 self.value = TeraRestoreStatus(attemptID: value.attemptID, phase: .reviewed, targets: value.targets) 95 return "reviewed" 96 } 97 98 func resume(_ digest: String) throws { 99 guard digest == "reviewed", let value, value.phase == .reviewed else { throw TeraComposerAcknowledgment.unconfirmed } 100 calls.append("resume") 101 self.value = TeraRestoreStatus(attemptID: value.attemptID, phase: .resumed, targets: value.targets) 102 } 103 } 104 105 extension TeraScopeBackend { 106 func restoreStatus() async -> TeraRestoreStatus? { 107 await restoreStorage.status() 108 } 109 110 func reconcileRestoredTarget(_ target: TeraRestoreTarget) async { 111 await restoreStorage.check(target) 112 } 113 114 func reviewRestoredWork() async throws -> String { 115 try await restoreStorage.review() 116 } 117 118 func resumeRestoredWork(reviewedInventory: String) async throws { 119 try await restoreStorage.resume(reviewedInventory) 120 } 121 }