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 }