apple_kit

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

RadrootsTelemetryTests.swift (4640B)


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