Files
lmxopcua/tests/Client/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/AlarmsViewModelTests.cs
T

388 lines
13 KiB
C#

using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
using ZB.MOM.WW.OtOpcUa.Client.UI.Tests.Fakes;
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests;
public class AlarmsViewModelTests
{
private readonly FakeOpcUaClientService _service;
private readonly AlarmsViewModel _vm;
/// <summary>Initializes a new test instance.</summary>
public AlarmsViewModelTests()
{
_service = new FakeOpcUaClientService();
var dispatcher = new SynchronousUiDispatcher();
_vm = new AlarmsViewModel(_service, dispatcher);
}
/// <summary>Verifies that SubscribeCommand cannot execute when disconnected.</summary>
[Fact]
public void SubscribeCommand_CannotExecute_WhenDisconnected()
{
_vm.IsConnected = false;
_vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that SubscribeCommand cannot execute when already subscribed.</summary>
[Fact]
public void SubscribeCommand_CannotExecute_WhenAlreadySubscribed()
{
_vm.IsConnected = true;
_vm.IsSubscribed = true;
_vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that SubscribeCommand can execute when connected and not subscribed.</summary>
[Fact]
public void SubscribeCommand_CanExecute_WhenConnectedAndNotSubscribed()
{
_vm.IsConnected = true;
_vm.IsSubscribed = false;
_vm.SubscribeCommand.CanExecute(null).ShouldBeTrue();
}
/// <summary>Verifies that SubscribeCommand sets IsSubscribed flag.</summary>
[Fact]
public async Task SubscribeCommand_SetsIsSubscribed()
{
_vm.IsConnected = true;
await _vm.SubscribeCommand.ExecuteAsync(null);
_vm.IsSubscribed.ShouldBeTrue();
_service.SubscribeAlarmsCallCount.ShouldBe(1);
}
/// <summary>Verifies that UnsubscribeCommand cannot execute when not subscribed.</summary>
[Fact]
public void UnsubscribeCommand_CannotExecute_WhenNotSubscribed()
{
_vm.IsConnected = true;
_vm.IsSubscribed = false;
_vm.UnsubscribeCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that UnsubscribeCommand clears IsSubscribed flag.</summary>
[Fact]
public async Task UnsubscribeCommand_ClearsIsSubscribed()
{
_vm.IsConnected = true;
await _vm.SubscribeCommand.ExecuteAsync(null);
await _vm.UnsubscribeCommand.ExecuteAsync(null);
_vm.IsSubscribed.ShouldBeFalse();
_service.UnsubscribeAlarmsCallCount.ShouldBe(1);
}
/// <summary>Verifies that RefreshCommand calls the service.</summary>
[Fact]
public async Task RefreshCommand_CallsService()
{
_vm.IsConnected = true;
_vm.IsSubscribed = true;
await _vm.RefreshCommand.ExecuteAsync(null);
_service.RequestConditionRefreshCallCount.ShouldBe(1);
}
/// <summary>Verifies that RefreshCommand cannot execute when not subscribed.</summary>
[Fact]
public void RefreshCommand_CannotExecute_WhenNotSubscribed()
{
_vm.IsConnected = true;
_vm.IsSubscribed = false;
_vm.RefreshCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that alarm events are added to the collection.</summary>
[Fact]
public void AlarmEvent_AddsToCollection()
{
var alarm = new AlarmEventArgs(
"Source1", "HighAlarm", 500, "Temperature high",
true, true, false, DateTime.UtcNow);
_service.RaiseAlarmEvent(alarm);
_vm.AlarmEvents.Count.ShouldBe(1);
_vm.AlarmEvents[0].SourceName.ShouldBe("Source1");
_vm.AlarmEvents[0].ConditionName.ShouldBe("HighAlarm");
_vm.AlarmEvents[0].Severity.ShouldBe((ushort)500);
_vm.AlarmEvents[0].Message.ShouldBe("Temperature high");
}
/// <summary>Verifies that Clear resets the view model state.</summary>
[Fact]
public void Clear_ResetsState()
{
_vm.IsSubscribed = true;
_vm.AlarmEvents.Add(new AlarmEventViewModel("Src", "Cond", 100, "Msg", true, true, false, DateTime.UtcNow));
_vm.Clear();
_vm.AlarmEvents.ShouldBeEmpty();
_vm.IsSubscribed.ShouldBeFalse();
}
/// <summary>Verifies that Teardown unregisters the event handler.</summary>
[Fact]
public void Teardown_UnhooksEventHandler()
{
_vm.Teardown();
var alarm = new AlarmEventArgs(
"Source1", "HighAlarm", 500, "Test",
true, true, false, DateTime.UtcNow);
_service.RaiseAlarmEvent(alarm);
_vm.AlarmEvents.ShouldBeEmpty();
}
/// <summary>Verifies that the default polling interval is 1000ms.</summary>
[Fact]
public void DefaultInterval_Is1000()
{
_vm.Interval.ShouldBe(1000);
}
/// <summary>
/// Regression test for Client.UI-006 — when SubscribeAlarmsAsync throws, the failure must be
/// surfaced to the operator via the view model's StatusMessage rather than silently swallowed.
/// </summary>
[Fact]
public async Task Subscribe_OnFailure_SurfacesStatusMessage()
{
_vm.IsConnected = true;
_service.SubscribeAlarmsException = new Exception("Server returned BadSubscriptionIdInvalid");
await _vm.SubscribeCommand.ExecuteAsync(null);
_vm.IsSubscribed.ShouldBeFalse();
_vm.StatusMessage.ShouldNotBeNullOrWhiteSpace();
_vm.StatusMessage.ShouldContain("BadSubscriptionIdInvalid");
}
/// <summary>Builds a representative alarm row for shelve/confirm tests.</summary>
/// <param name="ackedState">Whether the alarm is already acknowledged.</param>
/// <param name="hasEventId">Whether the row carries an EventId; <c>false</c> models a missing EventId.</param>
/// <param name="conditionNodeId">The condition node id; pass <c>null</c> to model a missing ConditionId.</param>
private static AlarmEventViewModel BuildAlarm(
bool ackedState = false,
bool hasEventId = true,
string? conditionNodeId = "ns=2;s=Cond1")
{
return new AlarmEventViewModel(
"Source1", "HighAlarm", 500, "Temperature high",
true, true, ackedState, DateTime.UtcNow,
hasEventId ? [1, 2, 3] : null, conditionNodeId);
}
// --- ShelveAlarmAsync ---
/// <summary>Verifies that a OneShot shelve calls the service and reports success.</summary>
[Fact]
public async Task ShelveAlarm_Connected_OneShot_Succeeds()
{
_vm.IsConnected = true;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
ok.ShouldBeTrue();
_service.ShelveCallCount.ShouldBe(1);
_service.LastShelveCall.ShouldNotBeNull();
_service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.OneShot);
_service.LastShelveCall.Value.ConditionNodeId.ShouldBe(alarm.ConditionNodeId);
}
/// <summary>Verifies that a Timed shelve passes the duration through to the service.</summary>
[Fact]
public async Task ShelveAlarm_Connected_Timed_PassesDuration()
{
_vm.IsConnected = true;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 300);
ok.ShouldBeTrue();
_service.LastShelveCall.ShouldNotBeNull();
_service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Timed);
_service.LastShelveCall.Value.ShelvingTimeSeconds.ShouldBe(300);
}
/// <summary>Verifies that an Unshelve calls the service and reports success.</summary>
[Fact]
public async Task ShelveAlarm_Connected_Unshelve_Succeeds()
{
_vm.IsConnected = true;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Unshelve, 0);
ok.ShouldBeTrue();
_service.LastShelveCall.ShouldNotBeNull();
_service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Unshelve);
}
/// <summary>Verifies that a Timed shelve with a non-positive duration is rejected without a service call.</summary>
[Fact]
public async Task ShelveAlarm_Timed_NonPositiveDuration_Fails_NoServiceCall()
{
_vm.IsConnected = true;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 0);
ok.ShouldBeFalse();
_service.ShelveCallCount.ShouldBe(0);
}
/// <summary>Verifies that shelving when disconnected is rejected without a service call.</summary>
[Fact]
public async Task ShelveAlarm_NotConnected_Fails_NoServiceCall()
{
_vm.IsConnected = false;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
ok.ShouldBeFalse();
_service.ShelveCallCount.ShouldBe(0);
}
/// <summary>Verifies that shelving an alarm without a ConditionNodeId is rejected without a service call.</summary>
[Fact]
public async Task ShelveAlarm_MissingConditionNodeId_Fails_NoServiceCall()
{
_vm.IsConnected = true;
var alarm = BuildAlarm(conditionNodeId: null);
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
ok.ShouldBeFalse();
_service.ShelveCallCount.ShouldBe(0);
}
/// <summary>Verifies that a Bad status code from the service surfaces a "Shelve failed" message.</summary>
[Fact]
public async Task ShelveAlarm_ServiceBad_Fails_WithMessage()
{
_vm.IsConnected = true;
_service.ShelveResult = StatusCodes.BadNodeIdUnknown;
var alarm = BuildAlarm();
var (ok, message) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
ok.ShouldBeFalse();
message.ShouldContain("Shelve failed");
}
// --- ConfirmAlarmAsync ---
/// <summary>Verifies that confirming an alarm calls the service with the supplied comment.</summary>
[Fact]
public async Task ConfirmAlarm_Connected_Succeeds_PassesComment()
{
_vm.IsConnected = true;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "operator note");
ok.ShouldBeTrue();
_service.ConfirmCallCount.ShouldBe(1);
_service.LastConfirmCall.ShouldNotBeNull();
_service.LastConfirmCall!.Value.Comment.ShouldBe("operator note");
}
/// <summary>Verifies that confirming when disconnected is rejected without a service call.</summary>
[Fact]
public async Task ConfirmAlarm_NotConnected_Fails_NoServiceCall()
{
_vm.IsConnected = false;
var alarm = BuildAlarm();
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
ok.ShouldBeFalse();
_service.ConfirmCallCount.ShouldBe(0);
}
/// <summary>Verifies that confirming an alarm without an EventId is rejected without a service call.</summary>
[Fact]
public async Task ConfirmAlarm_MissingEventId_Fails_NoServiceCall()
{
_vm.IsConnected = true;
var alarm = BuildAlarm(hasEventId: false);
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
ok.ShouldBeFalse();
_service.ConfirmCallCount.ShouldBe(0);
}
/// <summary>Verifies that a Bad status code from the service surfaces a "Confirm failed" message.</summary>
[Fact]
public async Task ConfirmAlarm_ServiceBad_Fails_WithMessage()
{
_vm.IsConnected = true;
_service.ConfirmResult = StatusCodes.BadNodeIdUnknown;
var alarm = BuildAlarm();
var (ok, message) = await _vm.ConfirmAlarmAsync(alarm, "note");
ok.ShouldBeFalse();
message.ShouldContain("Confirm failed");
}
// --- CanShelve / CanConfirm predicates ---
/// <summary>Verifies CanShelve is true when a ConditionNodeId is present.</summary>
[Fact]
public void CanShelve_True_WhenConditionNodeIdPresent()
{
BuildAlarm(conditionNodeId: "ns=2;s=Cond1").CanShelve.ShouldBeTrue();
}
/// <summary>Verifies CanShelve is false when the ConditionNodeId is missing.</summary>
[Fact]
public void CanShelve_False_WhenConditionNodeIdNull()
{
BuildAlarm(conditionNodeId: null).CanShelve.ShouldBeFalse();
}
/// <summary>Verifies CanConfirm is true when acked with EventId and ConditionNodeId present.</summary>
[Fact]
public void CanConfirm_True_WhenAckedWithEventIdAndConditionNodeId()
{
BuildAlarm(ackedState: true).CanConfirm.ShouldBeTrue();
}
/// <summary>Verifies CanConfirm is false when the alarm has not been acknowledged.</summary>
[Fact]
public void CanConfirm_False_WhenNotAcked()
{
BuildAlarm(ackedState: false).CanConfirm.ShouldBeFalse();
}
/// <summary>Verifies CanConfirm is false when the EventId is missing.</summary>
[Fact]
public void CanConfirm_False_WhenEventIdNull()
{
BuildAlarm(ackedState: true, hasEventId: false).CanConfirm.ShouldBeFalse();
}
/// <summary>Verifies CanConfirm is false when the ConditionNodeId is missing.</summary>
[Fact]
public void CanConfirm_False_WhenConditionNodeIdNull()
{
BuildAlarm(ackedState: true, conditionNodeId: null).CanConfirm.ShouldBeFalse();
}
}