TeraLifecycleTests.swift (7526B)
1 import Foundation 2 import RadrootsKit 3 @testable import TeraApp 4 import XCTest 5 6 final class TeraLifecycleTests: XCTestCase { 7 func testDiagnosticsRejectPreEpochAndSignedOverflowTimestamps() async throws { 8 let buffer = TeraDiagnosticsBuffer() 9 for timestamp in [ 10 TimeInterval(-1), 11 TimeInterval(Int64.max) / 1000 + 1, 12 ] { 13 try await buffer.record( 14 RadrootsTelemetryEvent( 15 name: "ios.lifecycle.invalid_clock", 16 occurredAt: Date(timeIntervalSince1970: timestamp) 17 ) 18 ) 19 } 20 21 let records = await buffer.records() 22 XCTAssertTrue(records.isEmpty) 23 } 24 25 func testBackgroundEventsCompleteExactlyOnceBeforeAndAfterAttachment() async { 26 let router = TeraBackgroundEventRouter() 27 let first = CompletionProbe() 28 29 await router.handle( 30 identifier: "org.radroots.tests.background.transfer", 31 completion: TeraCompletionOnce { first.increment() } 32 ) 33 XCTAssertEqual(first.value(), 0) 34 35 await router.attach(identifier: "org.radroots.tests.background.transfer") { _, completion in 36 completion() 37 completion() 38 } 39 XCTAssertEqual(first.value(), 1) 40 41 let mismatched = CompletionProbe() 42 await router.handle( 43 identifier: "org.radroots.tests.background.other", 44 completion: TeraCompletionOnce { mismatched.increment() } 45 ) 46 XCTAssertEqual(mismatched.value(), 1) 47 } 48 49 func testBackgroundEventQueueIsBoundedAndShutdownCompletesPendingEvents() async { 50 let router = TeraBackgroundEventRouter() 51 let probes = (0 ..< 12).map { _ in CompletionProbe() } 52 53 for probe in probes { 54 await router.handle( 55 identifier: "org.radroots.tests.background.transfer", 56 completion: TeraCompletionOnce { probe.increment() } 57 ) 58 } 59 60 XCTAssertEqual(probes.filter { $0.value() == 1 }.count, 4) 61 await router.detachAndCompletePending() 62 XCTAssertTrue(probes.allSatisfy { $0.value() == 1 }) 63 } 64 65 func testLifecycleBridgeRunsRegisteredShutdownOnlyOnce() async { 66 let bridge = TeraLifecycleBridge() 67 let shutdowns = CompletionProbe() 68 await bridge.register { shutdowns.increment(); return true } 69 70 await bridge.requestShutdown() 71 await bridge.requestShutdown() 72 73 XCTAssertEqual(shutdowns.value(), 1) 74 } 75 76 func testProtectedDataMonitorReflectsTransitions() { 77 let monitor = TeraProtectedDataMonitor(available: false) 78 XCTAssertFalse(monitor.isAvailable()) 79 80 monitor.update(available: true) 81 82 XCTAssertTrue(monitor.isAvailable()) 83 } 84 85 func testDiagnosticsAreBoundedAndExcludeSensitiveRuntimeValues() async throws { 86 let roots = try makeRoots() 87 defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } 88 let coordinator = TeraLifecycleCoordinator.testing(roots: roots, capacity: 16) 89 let secret = "nsec1diagnosticcanary" 90 let privateKey = String(repeating: "ab", count: 32) 91 let privatePath = "/Users/private/radroots/private.json" 92 93 for index in 0 ..< 40 { 94 await coordinator.record( 95 "ios.lifecycle.test_event", 96 fields: [ 97 "attempt": "\(index)", 98 "content": secret, 99 "private_key": privateKey, 100 "source": privatePath, 101 ] 102 ) 103 } 104 let export = try await coordinator.prepareDiagnostics( 105 snapshot: makeSnapshot(privateKey: privateKey), 106 appVersion: "0.1.0-alpha", 107 appBuild: "1", 108 phase: "running" 109 ) 110 let data = try Data(contentsOf: export.fileURL) 111 let text = try XCTUnwrap(String(data: data, encoding: .utf8)) 112 let object = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) 113 let records = try XCTUnwrap(object["records"] as? [[String: Any]]) 114 115 XCTAssertEqual(object["schema"] as? String, "radroots.ios.diagnostics.v1") 116 XCTAssertEqual(records.count, 16) 117 XCTAssertLessThanOrEqual(data.count, 256 * 1024) 118 XCTAssertTrue(text.contains("ios.diagnostics.redacted")) 119 XCTAssertTrue(records.allSatisfy { ($0["fields"] as? [String: String])?.isEmpty == true }) 120 XCTAssertFalse(text.contains(secret)) 121 XCTAssertFalse(text.contains(privateKey)) 122 XCTAssertFalse(text.contains(privatePath)) 123 XCTAssertFalse(text.contains("password@example.test")) 124 125 await coordinator.releaseDiagnostics(export) 126 XCTAssertFalse(FileManager.default.fileExists(atPath: export.fileURL.path)) 127 } 128 129 func testActualExportExcludesUntrustedMetadataAndOrdinaryPrivateContent() async throws { 130 let roots = try makeRoots() 131 defer { try? FileManager.default.removeItem(at: roots.dataRoot.deletingLastPathComponent()) } 132 let coordinator = TeraLifecycleCoordinator.testing(roots: roots) 133 let canary = "plain post at 49.123456,-123.987654 Bearer CANARY /Users/private/file" 134 await coordinator.record("private_event", fields: ["value": canary, "phase": canary]) 135 let export = try await coordinator.prepareDiagnostics( 136 snapshot: makeSnapshot(privateKey: String(repeating: "ab", count: 32), metadata: canary), 137 appVersion: canary, appBuild: canary, phase: canary 138 ) 139 let data = try Data(contentsOf: export.fileURL) 140 let text = try XCTUnwrap(String(data: data, encoding: .utf8)) 141 XCTAssertFalse(text.contains(canary)) 142 let document = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) 143 for key in ["appVersion", "appBuild", "runtimeCrate", "runtimeVersion", "runtimePhase", "relayProfile", "relayState"] { 144 XCTAssertEqual(document[key] as? String, "unavailable", key) 145 } 146 XCTAssertLessThanOrEqual(data.count, 256 * 1024) 147 await coordinator.releaseDiagnostics(export) 148 XCTAssertFalse(FileManager.default.fileExists(atPath: export.fileURL.path)) 149 } 150 151 private func makeRoots() throws -> RadrootsAppleFileRoots { 152 let base = FileManager.default.temporaryDirectory 153 .appendingPathComponent( 154 "radroots-ios-lifecycle-tests-\(UUID().uuidString)", isDirectory: true 155 ) 156 return try RadrootsAppleFileRoots( 157 appIdentifier: "org.radroots.tests", 158 dataRoot: base.appendingPathComponent("data", isDirectory: true), 159 cacheRoot: base.appendingPathComponent("cache", isDirectory: true), 160 temporaryRoot: base.appendingPathComponent("temporary", isDirectory: true) 161 ) 162 } 163 164 private func makeSnapshot(privateKey: String, metadata: String? = nil) -> TeraRuntimeSnapshot { 165 TeraRuntimeSnapshot( 166 identity: TeraRuntimeIdentity( 167 publicKeyHex: privateKey, 168 hostSignerConfigured: true 169 ), 170 relay: TeraRelayStatus( 171 profile: metadata ?? "simulator", 172 state: metadata ?? "configured", 173 readAvailability: "available", 174 writeAvailability: "available", 175 relays: [ 176 TeraRelayEndpointStatus( 177 url: "wss://user:password@example.test", 178 access: .readWrite, 179 readState: "available", 180 writeState: "available", 181 readLastAttemptUnixMilliseconds: nil, 182 writeLastAttemptUnixMilliseconds: nil, 183 readNextAttemptUnixMilliseconds: nil, 184 writeNextAttemptUnixMilliseconds: nil 185 ), 186 ] 187 ), 188 blossomConfiguration: nil, 189 blossomEvidence: nil, 190 crateName: metadata ?? "tera_ffi", 191 crateVersion: metadata ?? "0.1.0-alpha", 192 isClosed: false 193 ) 194 } 195 } 196 197 private final class CompletionProbe: @unchecked Sendable { 198 private let lock = NSLock() 199 private var count = 0 200 201 func increment() { 202 lock.withLock { count += 1 } 203 } 204 205 func value() -> Int { 206 lock.withLock { count } 207 } 208 }