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; using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult; namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests; public class SubscriptionsViewModelTests { private readonly FakeOpcUaClientService _service; private readonly SubscriptionsViewModel _vm; public SubscriptionsViewModelTests() { _service = new FakeOpcUaClientService(); var dispatcher = new SynchronousUiDispatcher(); _vm = new SubscriptionsViewModel(_service, dispatcher); } [Fact] public void AddSubscriptionCommand_CannotExecute_WhenDisconnected() { _vm.IsConnected = false; _vm.NewNodeIdText = "ns=2;s=SomeNode"; _vm.AddSubscriptionCommand.CanExecute(null).ShouldBeFalse(); } [Fact] public void AddSubscriptionCommand_CannotExecute_WhenNoNodeId() { _vm.IsConnected = true; _vm.NewNodeIdText = null; _vm.AddSubscriptionCommand.CanExecute(null).ShouldBeFalse(); } [Fact] public async Task AddSubscriptionCommand_AddsItem() { _vm.IsConnected = true; _vm.NewNodeIdText = "ns=2;s=SomeNode"; _vm.NewInterval = 500; await _vm.AddSubscriptionCommand.ExecuteAsync(null); _vm.ActiveSubscriptions.Count.ShouldBe(1); _vm.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=SomeNode"); _vm.ActiveSubscriptions[0].IntervalMs.ShouldBe(500); _vm.SubscriptionCount.ShouldBe(1); _service.SubscribeCallCount.ShouldBe(1); } [Fact] public async Task RemoveSubscriptionCommand_RemovesItem() { _vm.IsConnected = true; _vm.NewNodeIdText = "ns=2;s=SomeNode"; await _vm.AddSubscriptionCommand.ExecuteAsync(null); _vm.SelectedSubscription = _vm.ActiveSubscriptions[0]; await _vm.RemoveSubscriptionCommand.ExecuteAsync(null); _vm.ActiveSubscriptions.ShouldBeEmpty(); _vm.SubscriptionCount.ShouldBe(0); _service.UnsubscribeCallCount.ShouldBe(1); } [Fact] public void RemoveSubscriptionCommand_CannotExecute_WhenNoSelection() { _vm.IsConnected = true; _vm.SelectedSubscription = null; _vm.RemoveSubscriptionCommand.CanExecute(null).ShouldBeFalse(); } [Fact] public async Task DataChanged_UpdatesMatchingRow() { _vm.IsConnected = true; _vm.NewNodeIdText = "ns=2;s=SomeNode"; await _vm.AddSubscriptionCommand.ExecuteAsync(null); var dataValue = new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow); _service.RaiseDataChanged(new DataChangedEventArgs("ns=2;s=SomeNode", dataValue)); _vm.ActiveSubscriptions[0].Value.ShouldBe("42"); _vm.ActiveSubscriptions[0].Status.ShouldNotBeNull(); } [Fact] public async Task DataChanged_DoesNotUpdateNonMatchingRow() { _vm.IsConnected = true; _vm.NewNodeIdText = "ns=2;s=SomeNode"; await _vm.AddSubscriptionCommand.ExecuteAsync(null); var dataValue = new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow); _service.RaiseDataChanged(new DataChangedEventArgs("ns=2;s=OtherNode", dataValue)); _vm.ActiveSubscriptions[0].Value.ShouldBeNull(); } [Fact] public void Clear_RemovesAllSubscriptions() { _vm.ActiveSubscriptions.Add(new SubscriptionItemViewModel("ns=2;s=X", 1000)); _vm.SubscriptionCount = 1; _vm.Clear(); _vm.ActiveSubscriptions.ShouldBeEmpty(); _vm.SubscriptionCount.ShouldBe(0); } [Fact] public void Teardown_UnhooksEventHandler() { _vm.Teardown(); // After teardown, raising event should not update anything _vm.ActiveSubscriptions.Add(new SubscriptionItemViewModel("ns=2;s=X", 1000)); var dataValue = new DataValue(new Variant(42), StatusCodes.Good, DateTime.UtcNow); _service.RaiseDataChanged(new DataChangedEventArgs("ns=2;s=X", dataValue)); _vm.ActiveSubscriptions[0].Value.ShouldBeNull(); } [Fact] public void DefaultInterval_Is1000() { _vm.NewInterval.ShouldBe(1000); } [Fact] public async Task AddSubscriptionForNodeAsync_AddsSubscription() { _vm.IsConnected = true; await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode"); _vm.ActiveSubscriptions.Count.ShouldBe(1); _vm.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=TestNode"); _vm.SubscriptionCount.ShouldBe(1); _service.SubscribeCallCount.ShouldBe(1); } [Fact] public async Task AddSubscriptionForNodeAsync_SkipsDuplicate() { _vm.IsConnected = true; await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode"); await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode"); _vm.ActiveSubscriptions.Count.ShouldBe(1); _service.SubscribeCallCount.ShouldBe(1); } [Fact] public async Task AddSubscriptionForNodeAsync_DoesNothing_WhenDisconnected() { _vm.IsConnected = false; await _vm.AddSubscriptionForNodeAsync("ns=2;s=TestNode"); _vm.ActiveSubscriptions.ShouldBeEmpty(); _service.SubscribeCallCount.ShouldBe(0); } [Fact] public async Task GetSubscribedNodeIds_ReturnsActiveNodeIds() { _vm.IsConnected = true; await _vm.AddSubscriptionForNodeAsync("ns=2;s=Node1"); await _vm.AddSubscriptionForNodeAsync("ns=2;s=Node2"); var ids = _vm.GetSubscribedNodeIds(); ids.Count.ShouldBe(2); ids.ShouldContain("ns=2;s=Node1"); ids.ShouldContain("ns=2;s=Node2"); } [Fact] public async Task RestoreSubscriptionsAsync_SubscribesAllNodes() { _vm.IsConnected = true; await _vm.RestoreSubscriptionsAsync(["ns=2;s=A", "ns=2;s=B"]); _vm.ActiveSubscriptions.Count.ShouldBe(2); _service.SubscribeCallCount.ShouldBe(2); } [Fact] public async Task ValidateAndWriteAsync_SuccessReturnsTrue() { _vm.IsConnected = true; _service.ReadResult = new DataValue(new Variant(42), Opc.Ua.StatusCodes.Good, DateTime.UtcNow); _service.WriteResult = Opc.Ua.StatusCodes.Good; var (success, message) = await _vm.ValidateAndWriteAsync("ns=2;s=Node1", "99"); success.ShouldBeTrue(); message.ShouldContain("Good"); _service.WriteCallCount.ShouldBe(1); } [Fact] public async Task ValidateAndWriteAsync_ParseFailureReturnsFalse() { _vm.IsConnected = true; _service.ReadResult = new DataValue(new Variant(42), Opc.Ua.StatusCodes.Good, DateTime.UtcNow); var (success, message) = await _vm.ValidateAndWriteAsync("ns=2;s=Node1", "not-a-number"); success.ShouldBeFalse(); message.ShouldContain("Cannot parse"); message.ShouldContain("Int32"); _service.WriteCallCount.ShouldBe(0); } [Fact] public async Task ValidateAndWriteAsync_WriteFailureReturnsFalse() { _vm.IsConnected = true; _service.ReadResult = new DataValue(new Variant("hello"), Opc.Ua.StatusCodes.Good, DateTime.UtcNow); _service.WriteException = new Exception("Access denied"); var (success, message) = await _vm.ValidateAndWriteAsync("ns=2;s=Node1", "world"); success.ShouldBeFalse(); message.ShouldContain("Access denied"); } [Fact] public async Task ValidateAndWriteAsync_BadStatusReturnsFalse() { _vm.IsConnected = true; _service.ReadResult = new DataValue(new Variant("hello"), Opc.Ua.StatusCodes.Good, DateTime.UtcNow); _service.WriteResult = Opc.Ua.StatusCodes.BadNotWritable; var (success, message) = await _vm.ValidateAndWriteAsync("ns=2;s=Node1", "world"); success.ShouldBeFalse(); message.ShouldContain("Write failed"); } [Fact] public async Task AddSubscriptionRecursiveAsync_SubscribesVariableDirectly() { _vm.IsConnected = true; await _vm.AddSubscriptionRecursiveAsync("ns=2;s=Var1", "Variable"); _vm.ActiveSubscriptions.Count.ShouldBe(1); _vm.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=Var1"); } [Fact] public async Task AddSubscriptionRecursiveAsync_BrowsesObjectAndSubscribesVariableChildren() { _vm.IsConnected = true; _service.BrowseResultsByParent["ns=2;s=Folder"] = [ new BrowseResult("ns=2;s=Child1", "Child1", "Variable", false), new BrowseResult("ns=2;s=Child2", "Child2", "Variable", false) ]; await _vm.AddSubscriptionRecursiveAsync("ns=2;s=Folder", "Object"); _vm.ActiveSubscriptions.Count.ShouldBe(2); _service.SubscribeCallCount.ShouldBe(2); } [Fact] public async Task AddSubscriptionRecursiveAsync_RecursesNestedObjects() { _vm.IsConnected = true; _service.BrowseResultsByParent["ns=2;s=Root"] = [ new BrowseResult("ns=2;s=SubFolder", "SubFolder", "Object", true), new BrowseResult("ns=2;s=RootVar", "RootVar", "Variable", false) ]; _service.BrowseResultsByParent["ns=2;s=SubFolder"] = [ new BrowseResult("ns=2;s=DeepVar", "DeepVar", "Variable", false) ]; await _vm.AddSubscriptionRecursiveAsync("ns=2;s=Root", "Object"); _vm.ActiveSubscriptions.Count.ShouldBe(2); _vm.ActiveSubscriptions.ShouldContain(s => s.NodeId == "ns=2;s=RootVar"); _vm.ActiveSubscriptions.ShouldContain(s => s.NodeId == "ns=2;s=DeepVar"); } }