field_ios

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

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 }