using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Client.Shared.Models; using ZB.MOM.WW.LmxOpcUa.Client.UI.Services; using ZB.MOM.WW.LmxOpcUa.Client.UI.Tests.Fakes; using ZB.MOM.WW.LmxOpcUa.Client.UI.ViewModels; using BrowseResult = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.BrowseResult; using ConnectionState = ZB.MOM.WW.LmxOpcUa.Client.Shared.Models.ConnectionState; namespace ZB.MOM.WW.LmxOpcUa.Client.UI.Tests; public class MainWindowViewModelTests { private readonly FakeOpcUaClientService _service; private readonly MainWindowViewModel _vm; public MainWindowViewModelTests() { _service = new FakeOpcUaClientService { ConnectResult = new ConnectionInfo( "opc.tcp://localhost:4840", "TestServer", "None", "http://opcfoundation.org/UA/SecurityPolicy#None", "session-1", "TestSession"), BrowseResults = new[] { new BrowseResult("ns=2;s=Root", "Root", "Object", true) }, RedundancyResult = new RedundancyInfo("None", 200, new[] { "urn:test" }, "urn:test") }; var factory = new FakeOpcUaClientServiceFactory(_service); var dispatcher = new SynchronousUiDispatcher(); _vm = new MainWindowViewModel(factory, dispatcher); } [Fact] public void DefaultState_IsDisconnected() { _vm.ConnectionState.ShouldBe(ConnectionState.Disconnected); _vm.IsConnected.ShouldBeFalse(); _vm.EndpointUrl.ShouldBe("opc.tcp://localhost:4840"); _vm.StatusMessage.ShouldBe("Disconnected"); } [Fact] public void ConnectCommand_CanExecute_WhenDisconnected() { _vm.ConnectCommand.CanExecute(null).ShouldBeTrue(); } [Fact] public void DisconnectCommand_CannotExecute_WhenDisconnected() { _vm.DisconnectCommand.CanExecute(null).ShouldBeFalse(); } [Fact] public async Task ConnectCommand_TransitionsToConnected() { await _vm.ConnectCommand.ExecuteAsync(null); _vm.ConnectionState.ShouldBe(ConnectionState.Connected); _vm.IsConnected.ShouldBeTrue(); _service.ConnectCallCount.ShouldBe(1); } [Fact] public async Task ConnectCommand_LoadsRootNodes() { await _vm.ConnectCommand.ExecuteAsync(null); _vm.BrowseTree.RootNodes.Count.ShouldBe(1); _vm.BrowseTree.RootNodes[0].DisplayName.ShouldBe("Root"); } [Fact] public async Task ConnectCommand_FetchesRedundancyInfo() { await _vm.ConnectCommand.ExecuteAsync(null); _vm.RedundancyInfo.ShouldNotBeNull(); _vm.RedundancyInfo!.Mode.ShouldBe("None"); _vm.RedundancyInfo.ServiceLevel.ShouldBe((byte)200); } [Fact] public async Task ConnectCommand_SetsSessionLabel() { await _vm.ConnectCommand.ExecuteAsync(null); _vm.SessionLabel.ShouldContain("TestServer"); _vm.SessionLabel.ShouldContain("TestSession"); } [Fact] public async Task DisconnectCommand_TransitionsToDisconnected() { await _vm.ConnectCommand.ExecuteAsync(null); await _vm.DisconnectCommand.ExecuteAsync(null); _vm.ConnectionState.ShouldBe(ConnectionState.Disconnected); _vm.IsConnected.ShouldBeFalse(); _service.DisconnectCallCount.ShouldBe(1); } [Fact] public async Task Disconnect_ClearsStateAndChildren() { await _vm.ConnectCommand.ExecuteAsync(null); await _vm.DisconnectCommand.ExecuteAsync(null); _vm.SessionLabel.ShouldBe(string.Empty); _vm.RedundancyInfo.ShouldBeNull(); _vm.BrowseTree.RootNodes.ShouldBeEmpty(); _vm.SubscriptionCount.ShouldBe(0); } [Fact] public void ConnectionStateChangedEvent_UpdatesState() { _service.RaiseConnectionStateChanged( new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Reconnecting, "opc.tcp://localhost:4840")); _vm.ConnectionState.ShouldBe(ConnectionState.Reconnecting); _vm.StatusMessage.ShouldBe("Reconnecting..."); } [Fact] public async Task SelectedTreeNode_PropagatesToChildViewModels() { await _vm.ConnectCommand.ExecuteAsync(null); var node = _vm.BrowseTree.RootNodes[0]; _vm.SelectedTreeNode = node; _vm.ReadWrite.SelectedNodeId.ShouldBe(node.NodeId); _vm.History.SelectedNodeId.ShouldBe(node.NodeId); } [Fact] public async Task ConnectCommand_PropagatesIsConnectedToChildViewModels() { await _vm.ConnectCommand.ExecuteAsync(null); _vm.ReadWrite.IsConnected.ShouldBeTrue(); _vm.Subscriptions.IsConnected.ShouldBeTrue(); _vm.Alarms.IsConnected.ShouldBeTrue(); _vm.History.IsConnected.ShouldBeTrue(); } [Fact] public async Task DisconnectCommand_PropagatesIsConnectedFalseToChildViewModels() { await _vm.ConnectCommand.ExecuteAsync(null); await _vm.DisconnectCommand.ExecuteAsync(null); _vm.ReadWrite.IsConnected.ShouldBeFalse(); _vm.Subscriptions.IsConnected.ShouldBeFalse(); _vm.Alarms.IsConnected.ShouldBeFalse(); _vm.History.IsConnected.ShouldBeFalse(); } [Fact] public async Task ConnectFailure_RevertsToDisconnected() { _service.ConnectException = new Exception("Connection refused"); await _vm.ConnectCommand.ExecuteAsync(null); _vm.ConnectionState.ShouldBe(ConnectionState.Disconnected); _vm.StatusMessage.ShouldContain("Connection refused"); } [Fact] public void PropertyChanged_FiredForConnectionState() { var changed = new List(); _vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!); _service.RaiseConnectionStateChanged( new ConnectionStateChangedEventArgs(ConnectionState.Disconnected, ConnectionState.Connected, "opc.tcp://localhost:4840")); changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState)); changed.ShouldContain(nameof(MainWindowViewModel.IsConnected)); } [Fact] public void DefaultState_HasCorrectAdvancedSettings() { _vm.FailoverUrls.ShouldBeNull(); _vm.SessionTimeoutSeconds.ShouldBe(60); _vm.AutoAcceptCertificates.ShouldBeTrue(); _vm.CertificateStorePath.ShouldContain("LmxOpcUaClient"); _vm.CertificateStorePath.ShouldContain("pki"); } [Fact] public async Task ConnectCommand_MapsFailoverUrlsToSettings() { _vm.FailoverUrls = "opc.tcp://backup1:4840, opc.tcp://backup2:4840"; await _vm.ConnectCommand.ExecuteAsync(null); _service.LastConnectionSettings.ShouldNotBeNull(); _service.LastConnectionSettings!.FailoverUrls.ShouldNotBeNull(); _service.LastConnectionSettings.FailoverUrls!.Length.ShouldBe(2); _service.LastConnectionSettings.FailoverUrls[0].ShouldBe("opc.tcp://backup1:4840"); _service.LastConnectionSettings.FailoverUrls[1].ShouldBe("opc.tcp://backup2:4840"); } [Fact] public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull() { _vm.FailoverUrls = ""; await _vm.ConnectCommand.ExecuteAsync(null); _service.LastConnectionSettings.ShouldNotBeNull(); _service.LastConnectionSettings!.FailoverUrls.ShouldBeNull(); } [Fact] public async Task ConnectCommand_MapsSessionTimeoutToSettings() { _vm.SessionTimeoutSeconds = 120; await _vm.ConnectCommand.ExecuteAsync(null); _service.LastConnectionSettings.ShouldNotBeNull(); _service.LastConnectionSettings!.SessionTimeoutSeconds.ShouldBe(120); } [Fact] public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings() { _vm.AutoAcceptCertificates = false; await _vm.ConnectCommand.ExecuteAsync(null); _service.LastConnectionSettings.ShouldNotBeNull(); _service.LastConnectionSettings!.AutoAcceptCertificates.ShouldBeFalse(); } [Fact] public async Task ConnectCommand_MapsCertificateStorePathToSettings() { _vm.CertificateStorePath = "/custom/pki/path"; await _vm.ConnectCommand.ExecuteAsync(null); _service.LastConnectionSettings.ShouldNotBeNull(); _service.LastConnectionSettings!.CertificateStorePath.ShouldBe("/custom/pki/path"); } [Fact] public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab() { await _vm.ConnectCommand.ExecuteAsync(null); var node1 = _vm.BrowseTree.RootNodes[0]; _vm.SelectedTreeNodes.Add(node1); await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null); _vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(1); _vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe(node1.NodeId); _vm.SelectedTabIndex.ShouldBe(1); } [Fact] public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection() { await _vm.ConnectCommand.ExecuteAsync(null); await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null); _vm.Subscriptions.ActiveSubscriptions.ShouldBeEmpty(); } [Fact] public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab() { await _vm.ConnectCommand.ExecuteAsync(null); var node = _vm.BrowseTree.RootNodes[0]; _vm.SelectedTreeNodes.Add(node); _vm.ViewHistoryForSelectedNodeCommand.Execute(null); _vm.History.SelectedNodeId.ShouldBe(node.NodeId); _vm.SelectedTabIndex.ShouldBe(3); } [Fact] public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode() { await _vm.ConnectCommand.ExecuteAsync(null); var variableNode = new TreeNodeViewModel( "ns=2;s=Var1", "Var1", "Variable", false, _service, new SynchronousUiDispatcher()); _vm.SelectedTreeNodes.Add(variableNode); _vm.UpdateHistoryEnabledForSelection(); _vm.IsHistoryEnabledForSelection.ShouldBeTrue(); } [Fact] public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode() { await _vm.ConnectCommand.ExecuteAsync(null); var objectNode = new TreeNodeViewModel( "ns=2;s=Obj1", "Obj1", "Object", true, _service, new SynchronousUiDispatcher()); _vm.SelectedTreeNodes.Add(objectNode); _vm.UpdateHistoryEnabledForSelection(); _vm.IsHistoryEnabledForSelection.ShouldBeFalse(); } [Fact] public void UpdateHistoryEnabledForSelection_FalseWhenDisconnected() { _vm.UpdateHistoryEnabledForSelection(); _vm.IsHistoryEnabledForSelection.ShouldBeFalse(); } }