using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.GalaxyRepository.Grpc; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Server.Dashboard; namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard; /// /// Unit tests for the pure projection/formatting helpers behind the /// dashboard Browse and Alarms tabs. /// public sealed class DashboardBrowseAndAlarmModelTests { /// Verifies that the tree builder links children to parents and promotes orphans to roots. [Fact] public void BuildTree_LinksChildrenToParents_AndPromotesOrphansToRoots() { GalaxyObject area = new() { GobjectId = 1, BrowseName = "AreaA", IsArea = true, ParentGobjectId = 0 }; GalaxyObject child = new() { GobjectId = 2, BrowseName = "Pump01", ParentGobjectId = 1 }; GalaxyObject orphan = new() { GobjectId = 3, BrowseName = "Lost", ParentGobjectId = 99 }; IReadOnlyList roots = DashboardBrowseTreeBuilder.Build([area, child, orphan]); // The area and the orphan (its parent id is absent) are both roots. Assert.Equal(2, roots.Count); DashboardBrowseNode areaNode = Assert.Single(roots, node => node.Object.GobjectId == 1); Assert.Single(areaNode.Children); Assert.Equal(2, areaNode.Children[0].Object.GobjectId); Assert.Contains(roots, node => node.Object.GobjectId == 3); } /// Verifies that the tree builder sorts areas before non-area objects. [Fact] public void BuildTree_SortsAreasBeforeObjects() { GalaxyObject instance = new() { GobjectId = 1, BrowseName = "Zeta", IsArea = false }; GalaxyObject areaB = new() { GobjectId = 2, BrowseName = "Beta", IsArea = true }; IReadOnlyList roots = DashboardBrowseTreeBuilder.Build([instance, areaB]); Assert.Equal(2, roots.Count); Assert.True(roots[0].IsArea); Assert.Equal("Beta", roots[0].DisplayName); Assert.False(roots[1].IsArea); } /// Verifies that the formatter renders boolean values correctly. /// The boolean input value. /// The expected formatted output. [Theory] [InlineData(true, "true")] [InlineData(false, "false")] public void FormatValue_FormatsBooleans(bool input, string expected) { MxValue value = new() { DataType = MxDataType.Boolean, BoolValue = input }; Assert.Equal(expected, DashboardMxValueFormatter.FormatValue(value)); } /// Verifies that the formatter renders numbers and strings correctly. [Fact] public void FormatValue_FormatsNumbersAndStrings() { Assert.Equal("42", DashboardMxValueFormatter.FormatValue(new MxValue { Int32Value = 42 })); Assert.Equal("hello", DashboardMxValueFormatter.FormatValue(new MxValue { StringValue = "hello" })); } /// Verifies that the formatter handles null payloads and null references. [Fact] public void FormatValue_HandlesNullPayloadAndNullReference() { Assert.Equal("-", DashboardMxValueFormatter.FormatValue(null)); Assert.Equal("(null)", DashboardMxValueFormatter.FormatValue(new MxValue { IsNull = true })); } /// Verifies that tag values from successful reads mark good quality. [Fact] public void TagValue_FromSuccessfulReadResult_MarksGoodQuality() { BulkReadResult result = new() { TagAddress = "Galaxy!Area.Tag", WasSuccessful = true, Quality = 192, Value = new MxValue { DataType = MxDataType.Double, DoubleValue = 1.5 }, }; DashboardTagValue value = DashboardTagValue.FromBulkReadResult(result); Assert.True(value.Ok); Assert.True(value.QualityGood); Assert.Equal("1.5", value.ValueText); Assert.Null(value.Error); } /// Verifies that tag values from failed reads carry the error message. [Fact] public void TagValue_FromFailedReadResult_CarriesError() { BulkReadResult result = new() { TagAddress = "Galaxy!Area.Bad", WasSuccessful = false, Quality = 0, ErrorMessage = "invalid handle", }; DashboardTagValue value = DashboardTagValue.FromBulkReadResult(result); Assert.False(value.Ok); Assert.False(value.QualityGood); Assert.Equal("invalid handle", value.Error); } /// Verifies that active alarms parse provider and acknowledgement state from snapshots. [Fact] public void ActiveAlarm_FromSnapshot_ParsesProviderAndAcknowledgementState() { ActiveAlarmSnapshot unacked = new() { AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001", Category = "TestArea", CurrentState = AlarmConditionState.Active, Severity = 500, }; ActiveAlarmSnapshot acked = new() { AlarmFullReference = "Galaxy!TestArea.TestMachine_002.TestAlarm001", CurrentState = AlarmConditionState.ActiveAcked, }; DashboardActiveAlarm unackedRow = DashboardActiveAlarm.FromSnapshot(unacked); DashboardActiveAlarm ackedRow = DashboardActiveAlarm.FromSnapshot(acked); Assert.Equal("Galaxy", unackedRow.Provider); Assert.Equal("TestArea", unackedRow.Area); Assert.Equal(500, unackedRow.Severity); Assert.True(unackedRow.IsUnacknowledged); Assert.False(ackedRow.IsUnacknowledged); } /// Verifies that a healthy alarmmgr provider status maps to a green badge. [Fact] public void FromProviderStatus_Alarmmgr_NotDegraded_GreenBadge() { AlarmProviderStatus status = new() { Mode = AlarmProviderMode.Alarmmgr, Degraded = false, }; DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); Assert.False(model.IsDegraded); Assert.Contains("bg-success", model.BadgeCssClass, StringComparison.Ordinal); Assert.Equal(DashboardAlarmProviderStatus.AlarmManagerLabel, model.Label); } /// Verifies that a degraded subtag provider status maps to an amber warning badge. [Fact] public void FromProviderStatus_Subtag_Degraded_WarningBadge() { AlarmProviderStatus status = new() { Mode = AlarmProviderMode.Subtag, Degraded = true, Reason = "x", }; DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); Assert.True(model.IsDegraded); Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal); Assert.Equal("x", model.Reason); Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label); } /// /// An explicitly-degraded status whose mode is still Alarmmgr (the /// Degraded || Mode==Subtag guard's second, independent branch) must still /// map to the degraded amber badge. /// [Fact] public void FromProviderStatus_Alarmmgr_DegradedFlagSet_WarningBadge() { AlarmProviderStatus status = new() { Mode = AlarmProviderMode.Alarmmgr, Degraded = true, Reason = "independently degraded", }; DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); Assert.True(model.IsDegraded); Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label); Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal); } /// /// The SinceUtc field must carry the protobuf Since /// timestamp converted to a . /// [Fact] public void FromProviderStatus_WithSinceTimestamp_MapsSinceUtc() { DateTimeOffset since = new(2026, 6, 15, 12, 30, 0, TimeSpan.Zero); AlarmProviderStatus status = new() { Mode = AlarmProviderMode.Subtag, Degraded = true, Reason = "x", Since = Timestamp.FromDateTimeOffset(since), }; DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); Assert.Equal(since, model.SinceUtc); } /// /// — the entry the /// dashboard SignalR snapshot path actually calls — projects a provider-status /// feed message into the badge model. /// [Fact] public void FromFeed_ProviderStatusPayload_ProjectsBadge() { AlarmFeedMessage message = new() { ProviderStatus = new AlarmProviderStatus { Mode = AlarmProviderMode.Subtag, Degraded = true, Reason = "alarmmgr failed", }, }; DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromFeed(message); Assert.Equal(AlarmProviderMode.Subtag, model.Mode); Assert.True(model.IsDegraded); Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label); Assert.Equal("alarmmgr failed", model.Reason); } /// /// throws /// when the feed message does not carry a /// provider-status payload. /// [Fact] public void FromFeed_NonProviderStatusPayload_Throws() { AlarmFeedMessage message = new() { SnapshotComplete = true, }; Assert.Throws(() => DashboardAlarmProviderStatus.FromFeed(message)); } /// /// Verifies that a configured forced-subtag provider status renders the /// distinct "forced" badge (cyan/info), not the amber failover-degraded one. /// [Fact] public void FromProviderStatus_Subtag_ForcedReason_ForcedBadge() { AlarmProviderStatus status = new() { Mode = AlarmProviderMode.Subtag, Degraded = true, Reason = ZB.MOM.WW.MxGateway.Server.Alarms.AlarmProviderReasons.ForcedSubtag, }; DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status); Assert.True(model.IsDegraded); Assert.Equal(DashboardAlarmProviderStatus.ForcedSubtagLabel, model.Label); Assert.Contains("bg-info", model.BadgeCssClass, StringComparison.Ordinal); Assert.DoesNotContain("bg-warning", model.BadgeCssClass, StringComparison.Ordinal); } /// Verifies that the formatter renders array elements and element type correctly. [Fact] public void FormatValue_AndDataType_RenderArrayElementsAndElementType() { MxArray array = new() { ElementDataType = MxDataType.Double }; array.Dimensions.Add(3u); array.DoubleValues = new DoubleArray(); array.DoubleValues.Values.Add(new[] { 1.5, 2.25, 3.0 }); MxValue value = new() { ArrayValue = array }; Assert.Equal("[1.5, 2.25, 3]", DashboardMxValueFormatter.FormatValue(value)); Assert.Equal("Double[3]", DashboardMxValueFormatter.FormatDataType(value)); } }