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));
}
}