using System; using MxGateway.Contracts.Proto; using MxGateway.Worker.MxAccess; namespace MxGateway.Worker.Tests.MxAccess; /// /// Pins the pure helpers used to translate AVEVA's wnwrapConsumer XML /// payloads into proto-friendly fields. The COM-side I/O on /// needs an AVEVA install and is /// covered by the Skip-gated probe (WnWrapConsumerProbeTests); /// these unit tests cover everything that doesn't touch the live COM /// surface. /// public sealed class AlarmRecordTransitionMapperTests { [Fact] public void ComposeFullReference_WithProviderAndGroup_UsesProviderBangGroupDotNameFormat() { string reference = AlarmRecordTransitionMapper.ComposeFullReference( providerName: "GalaxyAlarmProvider", groupName: "Tank01", alarmName: "Level.HiHi"); Assert.Equal("GalaxyAlarmProvider!Tank01.Level.HiHi", reference); } [Fact] public void ComposeFullReference_WithEmptyProvider_DropsProvider() { string reference = AlarmRecordTransitionMapper.ComposeFullReference( providerName: null, groupName: "Tank01", alarmName: "Level.HiHi"); Assert.Equal("Tank01.Level.HiHi", reference); } [Fact] public void ComposeFullReference_WithEmptyGroup_DropsGroup() { string reference = AlarmRecordTransitionMapper.ComposeFullReference( providerName: "GalaxyAlarmProvider", groupName: null, alarmName: "GlobalAlarm"); Assert.Equal("GalaxyAlarmProvider!GlobalAlarm", reference); } [Fact] public void ComposeFullReference_WithEmptyProviderAndGroup_ReturnsAlarmName() { string reference = AlarmRecordTransitionMapper.ComposeFullReference( providerName: null, groupName: null, alarmName: "Bare"); Assert.Equal("Bare", reference); } [Theory] [InlineData("UNACK_ALM", MxAlarmStateKind.UnackAlm)] [InlineData("ACK_ALM", MxAlarmStateKind.AckAlm)] [InlineData("UNACK_RTN", MxAlarmStateKind.UnackRtn)] [InlineData("ACK_RTN", MxAlarmStateKind.AckRtn)] [InlineData("unack_alm", MxAlarmStateKind.UnackAlm)] // case-insensitive [InlineData(" ACK_ALM ", MxAlarmStateKind.AckAlm)] // trim [InlineData("UNKNOWN", MxAlarmStateKind.Unspecified)] [InlineData("", MxAlarmStateKind.Unspecified)] [InlineData(null, MxAlarmStateKind.Unspecified)] public void ParseStateKind_ForEachStateString_DecodesStateKind(string? input, MxAlarmStateKind expected) { Assert.Equal(expected, AlarmRecordTransitionMapper.ParseStateKind(input)); } [Theory] // First sighting: new alarm in *_ALM → Raise. [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Raise)] // First sighting in *_RTN → Clear (unusual; missed the original raise). [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackRtn, AlarmTransitionKind.Clear)] [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.AckRtn, AlarmTransitionKind.Clear)] // Active → Cleared. [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.UnackRtn, AlarmTransitionKind.Clear)] [InlineData(MxAlarmStateKind.AckAlm, MxAlarmStateKind.AckRtn, AlarmTransitionKind.Clear)] // Cleared → Active (re-trigger). [InlineData(MxAlarmStateKind.UnackRtn, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] [InlineData(MxAlarmStateKind.AckRtn, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] // Unacked → Acked (operator ack). [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Acknowledge)] [InlineData(MxAlarmStateKind.UnackRtn, MxAlarmStateKind.AckRtn, AlarmTransitionKind.Acknowledge)] // No-op (state unchanged) — caller is supposed to filter these out. [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Unspecified)] // Current=Unspecified → Unspecified. [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.Unspecified, AlarmTransitionKind.Unspecified)] public void MapTransition_ForEachStatePair_DecidesProtoKind( MxAlarmStateKind previous, MxAlarmStateKind current, AlarmTransitionKind expected) { Assert.Equal(expected, AlarmRecordTransitionMapper.MapTransition(previous, current)); } [Fact] public void ParseTransitionTimestampUtc_WithValidXmlFields_AssemblesUtc() { // Captured payload from probe (2026-05-01): EDT producer, GMTOFFSET=240, DSTADJUST=0. // Local 13:26:14.709 + 240 minutes (4h) = 17:26:14.709 UTC. DateTime utc = AlarmRecordTransitionMapper.ParseTransitionTimestampUtc( "2026/5/1", "13:26:14.709", gmtOffsetMinutes: 240, dstAdjustMinutes: 0); Assert.Equal(DateTimeKind.Utc, utc.Kind); Assert.Equal(2026, utc.Year); Assert.Equal(5, utc.Month); Assert.Equal(1, utc.Day); Assert.Equal(17, utc.Hour); Assert.Equal(26, utc.Minute); Assert.Equal(14, utc.Second); Assert.Equal(709, utc.Millisecond); } [Fact] public void ParseTransitionTimestampUtc_WithUnparseableInputs_ReturnsMinValue() { Assert.Equal(DateTime.MinValue, AlarmRecordTransitionMapper.ParseTransitionTimestampUtc(null, null, 0, 0)); Assert.Equal(DateTime.MinValue, AlarmRecordTransitionMapper.ParseTransitionTimestampUtc("not a date", "13:00:00", 0, 0)); Assert.Equal(DateTime.MinValue, AlarmRecordTransitionMapper.ParseTransitionTimestampUtc("2026/5/1", "not a time", 0, 0)); } }