fca978de07
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
298 lines
11 KiB
C#
298 lines
11 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the pure projection/formatting helpers behind the
|
|
/// dashboard Browse and Alarms tabs.
|
|
/// </summary>
|
|
public sealed class DashboardBrowseAndAlarmModelTests
|
|
{
|
|
/// <summary>Verifies that the tree builder links children to parents and promotes orphans to roots.</summary>
|
|
[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<DashboardBrowseNode> 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);
|
|
}
|
|
|
|
/// <summary>Verifies that the tree builder sorts areas before non-area objects.</summary>
|
|
[Fact]
|
|
public void BuildTree_SortsAreasBeforeObjects()
|
|
{
|
|
GalaxyObject instance = new() { GobjectId = 1, BrowseName = "Zeta", IsArea = false };
|
|
GalaxyObject areaB = new() { GobjectId = 2, BrowseName = "Beta", IsArea = true };
|
|
|
|
IReadOnlyList<DashboardBrowseNode> 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);
|
|
}
|
|
|
|
/// <summary>Verifies that the formatter renders boolean values correctly.</summary>
|
|
/// <param name="input">The boolean input value.</param>
|
|
/// <param name="expected">The expected formatted output.</param>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>Verifies that the formatter renders numbers and strings correctly.</summary>
|
|
[Fact]
|
|
public void FormatValue_FormatsNumbersAndStrings()
|
|
{
|
|
Assert.Equal("42", DashboardMxValueFormatter.FormatValue(new MxValue { Int32Value = 42 }));
|
|
Assert.Equal("hello", DashboardMxValueFormatter.FormatValue(new MxValue { StringValue = "hello" }));
|
|
}
|
|
|
|
/// <summary>Verifies that the formatter handles null payloads and null references.</summary>
|
|
[Fact]
|
|
public void FormatValue_HandlesNullPayloadAndNullReference()
|
|
{
|
|
Assert.Equal("-", DashboardMxValueFormatter.FormatValue(null));
|
|
Assert.Equal("(null)", DashboardMxValueFormatter.FormatValue(new MxValue { IsNull = true }));
|
|
}
|
|
|
|
/// <summary>Verifies that tag values from successful reads mark good quality.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that tag values from failed reads carry the error message.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that active alarms parse provider and acknowledgement state from snapshots.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that a healthy alarmmgr provider status maps to a green badge.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that a degraded subtag provider status maps to an amber warning badge.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An explicitly-degraded status whose mode is still Alarmmgr (the
|
|
/// <c>Degraded || Mode==Subtag</c> guard's second, independent branch) must still
|
|
/// map to the degraded amber badge.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The <c>SinceUtc</c> field must carry the protobuf <c>Since</c>
|
|
/// timestamp converted to a <see cref="DateTimeOffset" />.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="DashboardAlarmProviderStatus.FromFeed" /> — the entry the
|
|
/// dashboard SignalR snapshot path actually calls — projects a provider-status
|
|
/// feed message into the badge model.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="DashboardAlarmProviderStatus.FromFeed" /> throws
|
|
/// <see cref="ArgumentException" /> when the feed message does not carry a
|
|
/// provider-status payload.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FromFeed_NonProviderStatusPayload_Throws()
|
|
{
|
|
AlarmFeedMessage message = new()
|
|
{
|
|
SnapshotComplete = true,
|
|
};
|
|
|
|
Assert.Throws<ArgumentException>(() => DashboardAlarmProviderStatus.FromFeed(message));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a configured forced-subtag provider status renders the
|
|
/// distinct "forced" badge (cyan/info), not the amber failover-degraded one.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that the formatter renders array elements and element type correctly.</summary>
|
|
[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));
|
|
}
|
|
}
|