TeraLocalRestoreTests.swift (8876B)
1 import CryptoKit 2 import Darwin 3 import Foundation 4 import RadrootsKit 5 @testable import TeraApp 6 import TeraKitBindings 7 import XCTest 8 9 final class TeraLocalRestoreTests: XCTestCase { 10 func testActualColdRestoreAndGuardedReopenRequireExplicitResume() async throws { 11 let fixture = try RestoreFileFixture() 12 defer { fixture.remove() } 13 let backup = try TeraLocalBackupHost(roots: fixture.roots, publicKey: fixture.backup.publicKey, 14 generation: fixture.backup.sourceGeneration, protectedData: { true }) 15 try await backup.prepare(request: fixture.backup) 16 let initial = try await fixture.runtime() 17 _ = try await backup.capture(runtime: initial, request: fixture.backup) 18 _ = try await initial.shutdown() 19 let host = try fixture.host() 20 do { 21 _ = try await host.restore(store: fixture.store, request: fixture.request) 22 XCTFail("A live process marker must block even after runtime shutdown") 23 } catch { XCTAssertEqual(TeraGeneratedRuntimeFailure.from(error).code, "restore_busy") } 24 // Simulate a fresh process in this test fixture only, after explicit close. 25 // Production never removes a live PID marker to force recovery admission. 26 try RadrootsAppleFileAccess(roots: fixture.roots).delete(RadrootsFileReference( 27 scope: .data, relativePath: "\(TeraMediaProcessUse.directory)/\(getpid())" 28 )) 29 let guardRecord = try await host.restore(store: fixture.store, request: fixture.request) 30 XCTAssertEqual(try TeraRestoreFiles(roots: fixture.roots, publicKey: fixture.backup.publicKey).readGuard(), guardRecord.bytes) 31 do { _ = try await fixture.runtime(); XCTFail("Ordinary startup must refuse the guard") } catch {} 32 let startup = try await TeraGeneratedRuntimeBackend.start(configuration: fixture.configuration, localBackups: true) 33 let settings = try await startup.backend.mobileSettings() 34 XCTAssertEqual(settings.revision, 1, "Guarded startup must not adopt conflicting launch settings") 35 XCTAssertTrue(settings.identity.identities.isEmpty) 36 let held = try await startup.backend.restoreStatus() 37 XCTAssertEqual(held?.phase, .held) 38 _ = try await startup.backend.shutdown() 39 let restored = try await TeraRuntime.withHostSignerAndRestoreGuard( 40 store: fixture.store, hostSigner: TeraGeneratedHostSigner(signer: TestRuntimeSigner()), guard: guardRecord.bytes, localBackups: true 41 ) 42 let status = try await restored.applicationRestoreStatus() 43 XCTAssertEqual(status?.phase, .held) 44 XCTAssertEqual(status?.attemptId, fixture.request.attemptId) 45 do { 46 try await restored.resumeRestoredWork(reviewedInventory: String(repeating: "00", count: 32)) 47 XCTFail("Restore alone cannot authorize work") 48 } catch { XCTAssertEqual(TeraGeneratedRuntimeFailure.from(error).code, "restore_reconciliation_required") } 49 let digest = try await restored.reviewRestoredWork() 50 try await restored.resumeRestoredWork(reviewedInventory: digest) 51 let resumed = try await restored.applicationRestoreStatus() 52 XCTAssertEqual(resumed?.phase, .resumed) 53 _ = try await restored.shutdown() 54 } 55 56 func testUnknownOrActiveNativeTasksAndProtectedDataRefuseBeforeGuard() async throws { 57 let fixture = try RestoreFileFixture() 58 defer { fixture.remove() } 59 for active in [false, true] { 60 let host = try TeraLocalRestoreHost(roots: fixture.roots, activeTransfers: { 61 if active { 62 return try [RadrootsBackgroundTransferIdentifier("test.active")] 63 } 64 throw RadrootsBackgroundTransferError.unavailable 65 }, protectedData: { true }) 66 do { 67 _ = try await host.restore(store: fixture.store, request: fixture.request) 68 XCTFail("Unproven native inactivity cannot restore") 69 } catch { XCTAssertEqual(TeraGeneratedRuntimeFailure.from(error).code, active ? "restore_busy" : "restore_recovery_required") } 70 XCTAssertNil(try TeraRestoreFiles(roots: fixture.roots, publicKey: fixture.backup.publicKey).readGuard()) 71 } 72 let locked = try TeraLocalRestoreHost(roots: fixture.roots, activeTransfers: { [] }, protectedData: { false }) 73 do { _ = try await locked.restore(store: fixture.store, request: fixture.request); XCTFail("Protected data must be available") } catch {} 74 XCTAssertNil(try TeraRestoreFiles(roots: fixture.roots, publicKey: fixture.backup.publicKey).readGuard()) 75 } 76 77 func testImmutableLeaseRestoresMissingMediaAndRefusesTamperedBytes() throws { 78 let fixture = try RestoreFileFixture() 79 defer { fixture.remove() } 80 let roots = try TeraDurableMediaRoots.selectingStaging(in: fixture.roots) 81 let bytes = Data("restore exact media".utf8) 82 let hash = SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined() 83 let item = FfiBackupMedia(sha256: hash, byteLength: UInt64(bytes.count), 84 leaseIdentifier: "tera_backup_\(fixture.backup.backupId)_\(hash)") 85 let blob = try RadrootsStagedBlobReference(blobID: hash, sizeBytes: bytes.count) 86 let access = RadrootsAppleFileAccess(roots: roots) 87 try access.installStagedBlob(bytes, reference: blob) 88 let backup = TeraBackupFiles(roots: roots, publicKey: fixture.backup.publicKey) 89 let lease = try backup.mediaAccess().leaseStagedBlob(blob, expectedSHA256: hash, identifier: item.leaseIdentifier) 90 try access.releaseStagedBlob(blob) 91 let files = TeraRestoreFiles(roots: roots, publicKey: fixture.backup.publicKey) 92 let manifest = FfiBackupManifest(request: fixture.backup, media: [item], manifest: Data()) 93 try files.restoreMedia(manifest) 94 XCTAssertEqual(try access.readStagedBlob(blob), bytes) 95 try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: lease.fileURL.path) 96 try Data(repeating: 0, count: bytes.count).write(to: lease.fileURL) 97 XCTAssertThrowsError(try files.restoreMedia(manifest)) 98 XCTAssertEqual(try access.readStagedBlob(blob), bytes) 99 } 100 } 101 102 private struct RestoreFileFixture { 103 let root: URL 104 let roots: RadrootsAppleFileRoots 105 let backup = FfiBackupRequest(schemaVersion: 1, backupId: String(repeating: "01", count: 16), 106 publicKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 107 sourceGeneration: String(repeating: "07", count: 32), requestedAtUnixMs: 200, maximumBytes: 128 * 1024 * 1024) 108 init() throws { 109 root = FileManager.default.temporaryDirectory.resolvingSymlinksInPath().appendingPathComponent("tera-restore-\(UUID().uuidString)") 110 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) 111 roots = try RadrootsAppleFileRoots(appIdentifier: "test.tera.restore", dataRoot: root.appendingPathComponent("data"), 112 cacheRoot: root.appendingPathComponent("cache"), temporaryRoot: root.appendingPathComponent("temporary")) 113 _ = try RadrootsAppleMobileStore.prepare(roots: roots, publicKeyHex: backup.publicKey, protectedDataAvailability: .available) 114 try TeraBackupFiles(roots: roots, publicKey: backup.publicKey).prepare() 115 } 116 117 var store: FfiRestoreStore { 118 FfiRestoreStore(applicationSupportDirectory: roots.dataRoot.path, publicKey: backup.publicKey, 119 sourceGeneration: backup.sourceGeneration, sourceGenerationCreatedAtMs: 100, protectedData: .available) 120 } 121 122 var request: FfiRestoreRequest { 123 FfiRestoreRequest(attemptId: String(repeating: "02", count: 16), backup: backup, requestedAtMs: 300) 124 } 125 126 var configuration: TeraRuntimeLaunchConfiguration { 127 TeraRuntimeLaunchConfiguration( 128 applicationSupportDirectory: roots.dataRoot.path, publicKeyHex: backup.publicKey, 129 sourceGenerationHex: backup.sourceGeneration, sourceGenerationCreatedAtUnixMilliseconds: 100, 130 protectedData: .available, networkProfile: .simulator, writableRelays: ["ws://127.0.0.1:19999"], blossom: nil, 131 app: TeraRuntimeAppMetadata(bundleIdentifier: "test.restore", version: "1", buildNumber: "1", buildSHA: nil), 132 signerGeneration: "restore", signer: TestRuntimeSigner(), adoptBootstrapSettings: true 133 ) 134 } 135 136 func host() throws -> TeraLocalRestoreHost { 137 try TeraLocalRestoreHost(roots: roots, activeTransfers: { [] }, protectedData: { true }) 138 } 139 140 func runtime() async throws -> TeraRuntime { 141 try await TeraRuntime.withHostSignerAndLocalBackups(applicationSupportDirectory: roots.dataRoot.path, 142 publicKeyHex: backup.publicKey, sourceGenerationHex: backup.sourceGeneration, 143 sourceGenerationCreatedAtUnixMs: 100, protectedData: .available, 144 hostSigner: TeraGeneratedHostSigner(signer: TestRuntimeSigner())) 145 } 146 147 func remove() { 148 try? FileManager.default.removeItem(at: root) 149 } 150 }