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 FakeSettingsService _settingsService; 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 BrowseResult("ns=2;s=Root", "Root", "Object", true) ], RedundancyResult = new RedundancyInfo("None", 200, ["urn:test"], "urn:test") }; _settingsService = new FakeSettingsService(); var factory = new FakeOpcUaClientServiceFactory(_service); var dispatcher = new SynchronousUiDispatcher(); _vm = new MainWindowViewModel(factory, dispatcher, _settingsService); } [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 async Task ConnectionStateChangedEvent_UpdatesState() { await _vm.ConnectCommand.ExecuteAsync(null); _service.RaiseConnectionStateChanged( new ConnectionStateChangedEventArgs(ConnectionState.Connected, 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 async Task PropertyChanged_FiredForConnectionState() { await _vm.ConnectCommand.ExecuteAsync(null); var changed = new List(); _vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!); _service.RaiseConnectionStateChanged( new ConnectionStateChangedEventArgs(ConnectionState.Connected, ConnectionState.Reconnecting, "opc.tcp://localhost:4840")); changed.ShouldContain(nameof(MainWindowViewModel.ConnectionState)); } [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); // Use a Variable node so recursive subscribe subscribes it directly var varNode = new TreeNodeViewModel( "ns=2;s=TestVar", "TestVar", "Variable", false, _service, new SynchronousUiDispatcher()); _vm.SelectedTreeNodes.Add(varNode); await _vm.SubscribeSelectedNodesCommand.ExecuteAsync(null); _vm.Subscriptions!.ActiveSubscriptions.Count.ShouldBe(1); _vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=TestVar"); _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(); } [Fact] public void Constructor_LoadsSettingsFromService() { _settingsService.LoadCallCount.ShouldBe(1); } [Fact] public void Constructor_AppliesSavedSettings() { var saved = new UserSettings { EndpointUrl = "opc.tcp://saved:5555", Username = "savedUser", Password = "savedPass", SecurityMode = SecurityMode.Sign, FailoverUrls = "opc.tcp://backup:5555", SessionTimeoutSeconds = 120, AutoAcceptCertificates = false, CertificateStorePath = "/custom/path" }; var settingsService = new FakeSettingsService { Settings = saved }; var service = new FakeOpcUaClientService { ConnectResult = _service.ConnectResult, BrowseResults = _service.BrowseResults, RedundancyResult = _service.RedundancyResult }; var factory = new FakeOpcUaClientServiceFactory(service); var vm = new MainWindowViewModel(factory, new SynchronousUiDispatcher(), settingsService); vm.EndpointUrl.ShouldBe("opc.tcp://saved:5555"); vm.Username.ShouldBe("savedUser"); vm.Password.ShouldBe("savedPass"); vm.SelectedSecurityMode.ShouldBe(SecurityMode.Sign); vm.FailoverUrls.ShouldBe("opc.tcp://backup:5555"); vm.SessionTimeoutSeconds.ShouldBe(120); vm.AutoAcceptCertificates.ShouldBeFalse(); vm.CertificateStorePath.ShouldBe("/custom/path"); } [Fact] public async Task ConnectCommand_SavesSettingsOnSuccess() { _vm.EndpointUrl = "opc.tcp://myserver:4840"; _vm.Username = "admin"; await _vm.ConnectCommand.ExecuteAsync(null); _settingsService.SaveCallCount.ShouldBe(1); _settingsService.LastSaved.ShouldNotBeNull(); _settingsService.LastSaved!.EndpointUrl.ShouldBe("opc.tcp://myserver:4840"); _settingsService.LastSaved.Username.ShouldBe("admin"); } [Fact] public async Task ConnectCommand_DoesNotSaveOnFailure() { _service.ConnectException = new Exception("Connection refused"); await _vm.ConnectCommand.ExecuteAsync(null); _settingsService.SaveCallCount.ShouldBe(0); } [Fact] public async Task ConnectCommand_SavesSubscribedNodes() { await _vm.ConnectCommand.ExecuteAsync(null); // Add a subscription _vm.Subscriptions.IsConnected = true; await _vm.Subscriptions.AddSubscriptionForNodeAsync("ns=2;s=TestSub"); // Disconnect saves settings including subscriptions await _vm.DisconnectCommand.ExecuteAsync(null); _settingsService.LastSaved.ShouldNotBeNull(); _settingsService.LastSaved!.SubscribedNodes.ShouldContain("ns=2;s=TestSub"); } [Fact] public async Task ConnectCommand_RestoresSavedSubscriptions() { _settingsService.Settings.SubscribedNodes = ["ns=2;s=Restored1", "ns=2;s=Restored2"]; var service = new FakeOpcUaClientService { ConnectResult = _service.ConnectResult, BrowseResults = _service.BrowseResults, RedundancyResult = _service.RedundancyResult }; var factory = new FakeOpcUaClientServiceFactory(service); var vm = new MainWindowViewModel(factory, new SynchronousUiDispatcher(), _settingsService); await vm.ConnectCommand.ExecuteAsync(null); vm.Subscriptions.ActiveSubscriptions.Count.ShouldBe(2); vm.Subscriptions.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=Restored1"); vm.Subscriptions.ActiveSubscriptions[1].NodeId.ShouldBe("ns=2;s=Restored2"); vm.SubscriptionCount.ShouldBe(2); } }