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 }