using Xunit; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; // AlarmHistorianEvent using ZB.MOM.WW.OtOpcUa.Core.Abstractions; // AlarmSeverity using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping; public sealed class AlarmEventMapperTests { [Fact] public void A_direct_event_has_a_nonzero_wire_id_that_survives_repeated_mapping() { var alarm = new AlarmHistorianEvent("A", "S", "N", "LimitAlarm", AlarmSeverity.High, "Activated", "m", "system", null, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); var first = AlarmEventMapper.ToHistorianEvent(alarm).Id; Assert.True(Guid.TryParse(first, out var id), $"Expected a delivery GUID; received '{first}'."); Assert.NotEqual(Guid.Empty, id); Assert.Equal(first, AlarmEventMapper.ToHistorianEvent(alarm).Id); } [Fact] public void Maps_source_time_type_and_rich_properties() { var a = new AlarmHistorianEvent("A1", "Area/Line/Pump1", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "Temp high", "operator1", "ack note", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); var e = AlarmEventMapper.ToHistorianEvent(a); Assert.Equal("Area/Line/Pump1", e.SourceName); Assert.Equal("LimitAlarm", e.Type); Assert.Equal(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), e.EventTime.ToDateTime()); Assert.Equal(a.DeliveryId.ToString("D"), e.Id); Assert.Equal("A1", e.Properties["AlarmId"]); Assert.Equal("HiHi", e.Properties["AlarmName"]); Assert.Equal("Activated", e.Properties["EventKind"]); // Severity is the AlarmHistorianEvent.Severity XML doc contract: "Mapped to Historian's // numeric priority on the sink side" — NOT the .NET enum name. EventMapper.ParseSeverity on // the read side int.TryParse()s this same property, so a non-numeric string (the old bug: // alarm.Severity.ToString() == "High") silently always read back as the minimum severity (1). Assert.Equal("700", e.Properties["Severity"]); Assert.Equal("operator1", e.Properties["User"]); Assert.Equal("ack note", e.Properties["Comment"]); Assert.Equal("Temp high", e.Properties["Message"]); } /// /// Driver.Historian.Gateway-001 regression: the write side () and /// the read side () must agree on the wire representation of severity — /// a numeric OPC UA Part 9 priority string, matching the convention already used elsewhere in the /// codebase for the same enum (NativeAlarmProjector.MapSeverity: Low=200/Medium=500/ /// High=700/Critical=900). Round-tripping through both mappers must reproduce the numeric severity, /// not collapse to the minimum every time. /// [Theory] [InlineData(AlarmSeverity.Low, (ushort)200)] [InlineData(AlarmSeverity.Medium, (ushort)500)] [InlineData(AlarmSeverity.High, (ushort)700)] [InlineData(AlarmSeverity.Critical, (ushort)900)] public void Severity_round_trips_through_EventMapper_as_numeric_priority(AlarmSeverity severity, ushort expected) { var a = new AlarmHistorianEvent("A1", "Area/Line/Pump1", "HiHi", "LimitAlarm", severity, "Activated", "m", "operator1", null, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); var wire = AlarmEventMapper.ToHistorianEvent(a); var readBack = EventMapper.ToHistoricalEvent(wire); Assert.Equal(expected, readBack.Severity); } [Fact] public void Null_comment_is_omitted_not_null() { var a = new AlarmHistorianEvent("A", "S", "N", "DiscreteAlarm", AlarmSeverity.Low, "Cleared", "m", "system", null, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); Assert.False(AlarmEventMapper.ToHistorianEvent(a).Properties.ContainsKey("Comment")); } [Fact] public void A_blank_condition_id_still_has_delivery_identity_and_adds_no_condition_property() { // Condition identity and event delivery identity serve different purposes. var a = new AlarmHistorianEvent("", "S", "N", "DiscreteAlarm", AlarmSeverity.Low, "Cleared", "m", "system", null, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); var e = AlarmEventMapper.ToHistorianEvent(a); Assert.Equal(a.DeliveryId.ToString("D"), e.Id); Assert.False(e.Properties.ContainsKey("AlarmId")); } [Fact] public void An_explicitly_empty_delivery_id_is_rejected_instead_of_reminted() { var alarm = new AlarmHistorianEvent("A", "S", "N", "LimitAlarm", AlarmSeverity.High, "Activated", "m", "system", null, DateTime.UtcNow) { DeliveryId = Guid.Empty }; Assert.Throws(() => AlarmEventMapper.ToHistorianEvent(alarm)); } }