using Google.Protobuf.WellKnownTypes; using Shouldly; using ZB.MOM.WW.OtOpcUa.Commons.Protos; using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1; using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry; using Xunit; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Telemetry; /// /// Unit tests for — the central-side proto→domain /// projection. Each kind asserts every field round-trips, including the null-vs-absent presence /// handling for nullable Timestamps, optional strings, and the tri-state optional bool. A coverage /// guard proves every value has a converter. /// public sealed class TelemetryProtoMapCentralTests { private static readonly DateTime SampleUtc = new(2026, 7, 23, 10, 30, 45, DateTimeKind.Utc); private static readonly DateTime OtherUtc = new(2026, 7, 23, 9, 15, 0, DateTimeKind.Utc); [Fact] public void ToAlarm_transcribes_every_field_with_nullables_present() { var proto = new AlarmTransition { AlarmId = "Plant/Modbus/dev1/Speed", EquipmentPath = "Area/Line/Equip", AlarmName = "HighSpeed", TransitionKind = "Activated", Severity = 700, Message = "Speed high", User = "operator1", TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "LimitAlarm", Comment = "ack comment", HistorizeToAveva = false, }; proto.ReferencingEquipmentPaths.AddRange(["Area/Line/E1", "Area/Line/E2"]); var e = TelemetryProtoMapCentral.ToAlarm(proto); e.AlarmId.ShouldBe("Plant/Modbus/dev1/Speed"); e.EquipmentPath.ShouldBe("Area/Line/Equip"); e.AlarmName.ShouldBe("HighSpeed"); e.TransitionKind.ShouldBe("Activated"); e.Severity.ShouldBe(700); e.Message.ShouldBe("Speed high"); e.User.ShouldBe("operator1"); e.TimestampUtc.ShouldBe(SampleUtc); e.TimestampUtc.Kind.ShouldBe(DateTimeKind.Utc); e.AlarmTypeName.ShouldBe("LimitAlarm"); e.Comment.ShouldBe("ack comment"); e.HistorizeToAveva.ShouldBe(false); e.ReferencingEquipmentPaths.ShouldBe(["Area/Line/E1", "Area/Line/E2"]); } [Fact] public void ToAlarm_absent_optionals_map_to_null_and_empty_repeated_to_empty_list() { var proto = new AlarmTransition { AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Cleared", Severity = 1, Message = "m", User = "system", TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition", // Comment, HistorizeToAveva unset; no referencing paths added. }; var e = TelemetryProtoMapCentral.ToAlarm(proto); e.Comment.ShouldBeNull(); e.HistorizeToAveva.ShouldBeNull(); e.ReferencingEquipmentPaths.ShouldNotBeNull(); e.ReferencingEquipmentPaths.ShouldBeEmpty(); } [Fact] public void ToAlarm_historize_present_true_round_trips() { var proto = new AlarmTransition { AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated", Severity = 1, Message = "m", User = "system", TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition", HistorizeToAveva = true, }; TelemetryProtoMapCentral.ToAlarm(proto).HistorizeToAveva.ShouldBe(true); } [Fact] public void ToScript_transcribes_every_field_with_optionals_present() { var proto = new ScriptLog { ScriptId = "script-1", Level = "Information", Message = "hello", TimestampUtc = Timestamp.FromDateTime(SampleUtc), VirtualTagId = "vt-1", AlarmId = "al-1", EquipmentId = "eq-1", }; var e = TelemetryProtoMapCentral.ToScript(proto); e.ScriptId.ShouldBe("script-1"); e.Level.ShouldBe("Information"); e.Message.ShouldBe("hello"); e.TimestampUtc.ShouldBe(SampleUtc); e.TimestampUtc.Kind.ShouldBe(DateTimeKind.Utc); e.VirtualTagId.ShouldBe("vt-1"); e.AlarmId.ShouldBe("al-1"); e.EquipmentId.ShouldBe("eq-1"); } [Fact] public void ToScript_absent_optionals_map_to_null() { var proto = new ScriptLog { ScriptId = "script-1", Level = "Error", Message = "boom", TimestampUtc = Timestamp.FromDateTime(SampleUtc), // VirtualTagId, AlarmId, EquipmentId unset. }; var e = TelemetryProtoMapCentral.ToScript(proto); e.VirtualTagId.ShouldBeNull(); e.AlarmId.ShouldBeNull(); e.EquipmentId.ShouldBeNull(); } [Fact] public void ToHealth_transcribes_every_field_with_nullables_present() { var proto = new DriverHealth { ClusterId = "c1", DriverInstanceId = "d1", State = "Faulted", LastSuccessfulReadUtc = Timestamp.FromDateTime(OtherUtc), LastError = "timeout", ErrorCount5Min = 3, PublishedUtc = Timestamp.FromDateTime(SampleUtc), }; var e = TelemetryProtoMapCentral.ToHealth(proto); e.ClusterId.ShouldBe("c1"); e.DriverInstanceId.ShouldBe("d1"); e.State.ShouldBe("Faulted"); e.LastSuccessfulReadUtc.ShouldBe(OtherUtc); e.LastSuccessfulReadUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc); e.LastError.ShouldBe("timeout"); e.ErrorCount5Min.ShouldBe(3); e.PublishedUtc.ShouldBe(SampleUtc); e.PublishedUtc.Kind.ShouldBe(DateTimeKind.Utc); } [Fact] public void ToHealth_absent_nullable_timestamp_and_optional_string_map_to_null() { var proto = new DriverHealth { ClusterId = "c1", DriverInstanceId = "d1", State = "Healthy", ErrorCount5Min = 0, PublishedUtc = Timestamp.FromDateTime(SampleUtc), // LastSuccessfulReadUtc, LastError unset. }; var e = TelemetryProtoMapCentral.ToHealth(proto); e.LastSuccessfulReadUtc.ShouldBeNull(); e.LastError.ShouldBeNull(); } [Fact] public void ToResilience_transcribes_every_field_with_nullable_present() { var proto = new DriverResilienceStatus { DriverInstanceId = "d1", HostName = "host-a", BreakerOpen = true, ConsecutiveFailures = 5, CurrentInFlight = 2, LastBreakerOpenUtc = Timestamp.FromDateTime(OtherUtc), LastSampledUtc = Timestamp.FromDateTime(SampleUtc), PublishedUtc = Timestamp.FromDateTime(SampleUtc), }; var e = TelemetryProtoMapCentral.ToResilience(proto); e.DriverInstanceId.ShouldBe("d1"); e.HostName.ShouldBe("host-a"); e.BreakerOpen.ShouldBeTrue(); e.ConsecutiveFailures.ShouldBe(5); e.CurrentInFlight.ShouldBe(2); e.LastBreakerOpenUtc.ShouldBe(OtherUtc); e.LastBreakerOpenUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc); e.LastSampledUtc.ShouldBe(SampleUtc); e.PublishedUtc.ShouldBe(SampleUtc); } [Fact] public void ToResilience_absent_nullable_timestamp_maps_to_null() { var proto = new DriverResilienceStatus { DriverInstanceId = "d1", HostName = "host-a", BreakerOpen = false, ConsecutiveFailures = 0, CurrentInFlight = 0, LastSampledUtc = Timestamp.FromDateTime(SampleUtc), PublishedUtc = Timestamp.FromDateTime(SampleUtc), // LastBreakerOpenUtc unset. }; TelemetryProtoMapCentral.ToResilience(proto).LastBreakerOpenUtc.ShouldBeNull(); } [Fact] public void MapEvent_routes_each_case_to_its_record_type() { var alarm = new TelemetryEvent { AlarmTransition = MinimalAlarm() }; var script = new TelemetryEvent { ScriptLog = MinimalScript() }; var health = new TelemetryEvent { DriverHealth = MinimalHealth() }; var resilience = new TelemetryEvent { DriverResilience = MinimalResilience() }; TelemetryProtoMapCentral.MapEvent(alarm).ShouldBeOfType(); TelemetryProtoMapCentral.MapEvent(script).ShouldBeOfType(); TelemetryProtoMapCentral.MapEvent(health).ShouldBeOfType(); TelemetryProtoMapCentral.MapEvent(resilience).ShouldBeOfType(); } [Fact] public void MapEvent_unset_case_throws_NotSupported() { Should.Throw(() => TelemetryProtoMapCentral.MapEvent(new TelemetryEvent())); } /// /// Coverage guard: every value must be routed /// by to a non-null domain record. Adding a /// fifth oneof case (and thus a fifth HandledCases entry) without a converter arm fails here. /// [Fact] public void MapEvent_covers_every_handled_case() { foreach (var handled in TelemetryProtoContract.HandledCases) { var evt = BuildFor(handled); evt.EventCase.ShouldBe(handled); TelemetryProtoMapCentral.MapEvent(evt).ShouldNotBeNull(); } } private static TelemetryEvent BuildFor(TelemetryEvent.EventOneofCase handled) => handled switch { TelemetryEvent.EventOneofCase.AlarmTransition => new TelemetryEvent { AlarmTransition = MinimalAlarm() }, TelemetryEvent.EventOneofCase.ScriptLog => new TelemetryEvent { ScriptLog = MinimalScript() }, TelemetryEvent.EventOneofCase.DriverHealth => new TelemetryEvent { DriverHealth = MinimalHealth() }, TelemetryEvent.EventOneofCase.DriverResilience => new TelemetryEvent { DriverResilience = MinimalResilience() }, _ => throw new InvalidOperationException($"Test does not know how to build case {handled}."), }; private static AlarmTransition MinimalAlarm() => new() { AlarmId = "a", EquipmentPath = "p", AlarmName = "n", TransitionKind = "Activated", Severity = 1, Message = "m", User = "system", TimestampUtc = Timestamp.FromDateTime(SampleUtc), AlarmTypeName = "AlarmCondition", }; private static ScriptLog MinimalScript() => new() { ScriptId = "s", Level = "Information", Message = "m", TimestampUtc = Timestamp.FromDateTime(SampleUtc), }; private static DriverHealth MinimalHealth() => new() { ClusterId = "c", DriverInstanceId = "d", State = "Healthy", ErrorCount5Min = 0, PublishedUtc = Timestamp.FromDateTime(SampleUtc), }; private static DriverResilienceStatus MinimalResilience() => new() { DriverInstanceId = "d", HostName = "h", BreakerOpen = false, ConsecutiveFailures = 0, CurrentInFlight = 0, LastSampledUtc = Timestamp.FromDateTime(SampleUtc), PublishedUtc = Timestamp.FromDateTime(SampleUtc), }; }