apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

RadrootsTelemetryTests.swift (4375B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import RadrootsKit
      5 
      6 @Test func telemetryEventNormalizesSafeIdentifiersAndFields() throws {
      7     let event = try RadrootsTelemetryEvent(
      8         name: "field_ios.startup.begin",
      9         category: "field_ios",
     10         level: .notice,
     11         message: " Startup began ",
     12         fields: [
     13             .integer("configured_relay_count", 3),
     14             .bool("has_identity", true),
     15             .string("relay_light", "green"),
     16         ],
     17         occurredAt: Date(timeIntervalSince1970: 42)
     18     )
     19 
     20     #expect(event.name == "field_ios.startup.begin")
     21     #expect(event.category == "field_ios")
     22     #expect(event.level == .notice)
     23     #expect(event.message == "Startup began")
     24     #expect(event.fields.map(\.key) == ["configured_relay_count", "has_identity", "relay_light"])
     25     #expect(event.occurredAt == Date(timeIntervalSince1970: 42))
     26 }
     27 
     28 @Test func telemetryEventRejectsUnsafeShape() throws {
     29     #expect(throws: RadrootsTelemetryError.invalidRequest) {
     30         _ = try RadrootsTelemetryEvent(name: "FieldIos.Startup")
     31     }
     32     #expect(throws: RadrootsTelemetryError.invalidRequest) {
     33         _ = try RadrootsTelemetryField.string("Relay Light", "green")
     34     }
     35     #expect(throws: RadrootsTelemetryError.invalidRequest) {
     36         _ = try RadrootsTelemetryField.double("elapsed_seconds", .infinity)
     37     }
     38     #expect(throws: RadrootsTelemetryError.invalidRequest) {
     39         _ = try RadrootsTelemetryEvent(
     40             name: "field_ios.relay.status",
     41             fields: [
     42                 .integer("connected_count", 1),
     43                 .integer("connected_count", 2),
     44             ]
     45         )
     46     }
     47 }
     48 
     49 @Test func telemetryRedactionPolicyRedactsSecretsPathsAndUnsafeKeys() throws {
     50     let policy = RadrootsTelemetryRedactionPolicy.default
     51     let secretHex = String(repeating: "a", count: 64)
     52     let event = try RadrootsTelemetryEvent(
     53         name: "field_ios.nsec_startup",
     54         category: "field_ios",
     55         level: .error,
     56         message: "failed with nsec1secretvalue",
     57         fields: [
     58             .string("relay_error", "path /Users/person/container"),
     59             .string("selected_secret_key_name", "field identity"),
     60             .string("public_reason", "event id \(secretHex)"),
     61             .integer("absolute_path_count", 1),
     62             .stringList("relay_urls", ["wss://radroots.org", "nsec1relay"]),
     63         ]
     64     )
     65 
     66     let redacted = policy.redacted(event)
     67 
     68     #expect(redacted.name == "redacted")
     69     #expect(redacted.category == "field_ios")
     70     #expect(redacted.message == "[redacted]")
     71     #expect(redacted.fields[0].value == .string("[redacted]"))
     72     #expect(redacted.fields[1].value == .string("[redacted]"))
     73     #expect(redacted.fields[2].value == .string("[redacted]"))
     74     #expect(redacted.fields[3].value == .string("[redacted]"))
     75     #expect(redacted.fields[4].value == .stringList(["wss://radroots.org", "[redacted]"]))
     76 }
     77 
     78 @Test func redactingTelemetryRecordsOnlyRedactedEvents() async throws {
     79     let recorder = RadrootsTelemetryProbe()
     80     let telemetry = RadrootsRedactingTelemetry(sink: recorder)
     81     let event = try RadrootsTelemetryEvent(
     82         name: "field_ios.identity.import",
     83         message: "imported nsec1secret",
     84         fields: [
     85             .string("identity_state", "imported")
     86         ]
     87     )
     88 
     89     await telemetry.record(event)
     90 
     91     let events = await recorder.recordedEvents
     92     #expect(events.count == 1)
     93     #expect(events[0].message == "[redacted]")
     94     #expect(events[0].fields[0].value == .string("imported"))
     95 }
     96 
     97 @Test func multiplexTelemetryForwardsToAllSinks() async throws {
     98     let first = RadrootsTelemetryProbe()
     99     let second = RadrootsTelemetryProbe()
    100     let telemetry = RadrootsMultiplexTelemetry([first, second])
    101     let event = try RadrootsTelemetryEvent(name: "field_ios.startup.success")
    102 
    103     await telemetry.record(event)
    104 
    105     #expect(await first.recordedEventNames == ["field_ios.startup.success"])
    106     #expect(await second.recordedEventNames == ["field_ios.startup.success"])
    107 }
    108 
    109 private actor RadrootsTelemetryProbe: RadrootsTelemetry {
    110     private var eventsValue: [RadrootsTelemetryEvent] = []
    111 
    112     func record(_ event: RadrootsTelemetryEvent) async {
    113         eventsValue.append(event)
    114     }
    115 
    116     var recordedEvents: [RadrootsTelemetryEvent] {
    117         eventsValue
    118     }
    119 
    120     var recordedEventNames: [String] {
    121         eventsValue.map(\.name)
    122     }
    123 }