using JdeScoping.ConfigManager.Models; using JdeScoping.ConfigManager.Services; using JdeScoping.ConfigManager.Services.SecureStore; using JdeScoping.ConfigManager.ViewModels; using JdeScoping.ConfigManager.ViewModels.Forms; using Microsoft.Extensions.Logging; using NSubstitute; namespace JdeScoping.ConfigManager.Tests.ViewModels; public class MainWindowViewModelTests { private readonly IFileSystem _fileSystem; private readonly IConfigFileService _configFileService; private readonly IValidationService _validationService; private readonly IBackupService _backupService; private readonly IAutoDiscoveryService _autoDiscoveryService; private readonly IDialogService _dialogService; private readonly ISecureStoreManager _secureStoreManager; private readonly IClipboardService _clipboardService; private readonly IRuntimeConfigValidationService _runtimeValidationService; private readonly IConnectionTestService _connectionTestService; private readonly ILogger _logger; public MainWindowViewModelTests() { _fileSystem = Substitute.For(); _configFileService = Substitute.For(); _validationService = Substitute.For(); _backupService = Substitute.For(); _autoDiscoveryService = Substitute.For(); _dialogService = Substitute.For(); _secureStoreManager = Substitute.For(); _clipboardService = Substitute.For(); _runtimeValidationService = Substitute.For(); _connectionTestService = Substitute.For(); _logger = Substitute.For>(); _validationService.ValidateAppSettings(Arg.Any()) .Returns(new ValidationResult()); _validationService.ValidatePipelines(Arg.Any()) .Returns(new ValidationResult()); } [Fact] public void SelectingDataSyncNode_LoadsDataSyncFormViewModel() { // Arrange var config = new ConfigModel { DataSync = new DataSyncSection { MaxDegreeOfParallelism = 8 } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var dataSyncNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "DataSync"); // Act sut.SelectedNode = dataSyncNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); ((DataSyncFormViewModel)sut.SelectedFormViewModel!).MaxDegreeOfParallelism.ShouldBe(8); } [Fact] public void SelectingDataAccessNode_LoadsDataAccessFormViewModel() { // Arrange var config = new ConfigModel { DataAccess = new DataAccessSection { ProductionSchema = "custom" } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var dataAccessNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "DataAccess"); // Act sut.SelectedNode = dataAccessNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); ((DataAccessFormViewModel)sut.SelectedFormViewModel!).ProductionSchema.ShouldBe("custom"); } [Fact] public void SelectingAuthNode_LoadsAuthFormViewModel() { // Arrange var config = new ConfigModel { Auth = new AuthSection { CookieName = "TestCookie" } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var authNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "Auth"); // Act sut.SelectedNode = authNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); ((AuthFormViewModel)sut.SelectedFormViewModel!).CookieName.ShouldBe("TestCookie"); } [Fact] public void SelectingLdapNode_LoadsLdapFormViewModel() { // Arrange var config = new ConfigModel { Ldap = new LdapSection { GroupDn = "CN=TestGroup" } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var ldapNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "Ldap"); // Act sut.SelectedNode = ldapNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); ((LdapFormViewModel)sut.SelectedFormViewModel!).GroupDn.ShouldBe("CN=TestGroup"); } [Fact] public void SelectingSearchNode_LoadsSearchFormViewModel() { // Arrange var config = new ConfigModel { Search = new SearchSection { MaxResultRows = 50000 } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var searchNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "Search"); // Act sut.SelectedNode = searchNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); ((SearchFormViewModel)sut.SelectedFormViewModel!).MaxResultRows.ShouldBe(50000); } [Fact] public void SelectingExcelExportNode_LoadsExcelExportFormViewModel() { // Arrange var config = new ConfigModel { ExcelExport = new ExcelExportSection { TimezoneId = "America/New_York" } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var excelNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "ExcelExport"); // Act sut.SelectedNode = excelNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); ((ExcelExportFormViewModel)sut.SelectedFormViewModel!).SelectedTimezone.ShouldBe("America/New_York"); } [Fact] public void SelectingPipelineNode_LoadsPipelineEditorViewModel() { // Arrange var config = new ConfigModel(); var pipelines = new PipelinesConfigModel { Pipelines = new Dictionary { ["WorkOrders"] = new PipelineModel { Source = new PipelineSource { Connection = "jde", Query = "SELECT * FROM WO" }, Destination = new PipelineDestination { Table = "WorkOrder_Curr" } } } }; var sut = CreateViewModel(); sut.LoadConfigForTesting(config, pipelines); var pipelineNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "WorkOrders"); // Act sut.SelectedNode = pipelineNode; // Assert sut.SelectedFormViewModel.ShouldBeOfType(); var pipelineEditor = (PipelineEditorViewModel)sut.SelectedFormViewModel!; pipelineEditor.Name.ShouldBe("WorkOrders"); pipelineEditor.Source.Connection.ShouldBe("jde"); } [Fact] public void ModifyingFormProperty_SetsHasUnsavedChanges() { // Arrange var config = new ConfigModel(); var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var dataSyncNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "DataSync"); sut.SelectedNode = dataSyncNode; // Act ((DataSyncFormViewModel)sut.SelectedFormViewModel!).BatchSize = 10000; // Assert sut.HasUnsavedChanges.ShouldBeTrue(); } [Fact] public void ModifyingFormProperty_MarksNodeAsModified() { // Arrange var config = new ConfigModel(); var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var dataSyncNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "DataSync"); sut.SelectedNode = dataSyncNode; // Act ((DataSyncFormViewModel)sut.SelectedFormViewModel!).MaxDegreeOfParallelism = 16; // Assert dataSyncNode.IsModified.ShouldBeTrue(); } [Fact] public void SelectingFolderNode_SetsSelectedFormViewModelToNull() { // Arrange var config = new ConfigModel(); var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var folderNode = sut.TreeNodes.First(); // Settings folder // Act sut.SelectedNode = folderNode; // Assert sut.SelectedFormViewModel.ShouldBeNull(); } [Fact] public void SelectingNull_SetsSelectedFormViewModelToNull() { // Arrange var config = new ConfigModel(); var sut = CreateViewModel(); sut.LoadConfigForTesting(config, null); var dataSyncNode = sut.TreeNodes .SelectMany(n => n.Children) .First(n => n.SectionKey == "DataSync"); sut.SelectedNode = dataSyncNode; // Act sut.SelectedNode = null; // Assert sut.SelectedFormViewModel.ShouldBeNull(); } [Fact] public void LoadConfigForTesting_BuildsTreeNodes() { // Arrange var config = new ConfigModel(); var sut = CreateViewModel(); // Act sut.LoadConfigForTesting(config, null); // Assert // Without a configured/open SecureStore, only Settings and Pipelines appear sut.TreeNodes.Count.ShouldBe(2); // Settings, Pipelines (no Secure Store when not configured) sut.TreeNodes[0].Name.ShouldBe("Settings"); sut.TreeNodes[0].Children.Count.ShouldBe(7); // ConnectionStrings, DataSync, DataAccess, Auth, Ldap, Search, ExcelExport sut.TreeNodes[1].Name.ShouldBe("Pipelines"); } [Fact] public void LoadConfigForTesting_WithPipelines_BuildsPipelineNodes() { // Arrange var config = new ConfigModel(); var pipelines = new PipelinesConfigModel { Pipelines = new Dictionary { ["Pipeline1"] = new PipelineModel(), ["Pipeline2"] = new PipelineModel() } }; var sut = CreateViewModel(); // Act sut.LoadConfigForTesting(config, pipelines); // Assert sut.TreeNodes[1].Name.ShouldBe("Pipelines"); sut.TreeNodes[1].Children.Count.ShouldBe(2); } [Fact] public async Task OpenFolderCommand_UsesFilePicker_AndDerivesFolder() { // Arrange var expectedFilePath = "/path/to/folder/appsettings.json"; var expectedFolder = "/path/to/folder"; var config = new ConfigModel(); // Ensure auto-discovery doesn't load config _autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null); _dialogService.ShowFilePickerAsync(Arg.Any()) .Returns(expectedFilePath); _configFileService.LoadAppSettingsAsync(Arg.Any(), Arg.Any()) .Returns(config); var sut = CreateViewModel(); // Wait for constructor async init to complete await Task.Delay(50); _configFileService.ClearReceivedCalls(); // Act sut.OpenFolderCommand.Execute(null); // Give async command time to complete await Task.Delay(100); // Assert await _dialogService.Received(1).ShowFilePickerAsync("Select Configuration File"); await _configFileService.Received(1).LoadAppSettingsAsync( Arg.Is(s => s.Contains(expectedFolder)), Arg.Any()); sut.ConfigFolderPath.ShouldBe(expectedFolder); } [Fact] public async Task OpenFolderCommand_WhenCancelled_DoesNotLoadConfig() { // Arrange // Ensure auto-discovery doesn't load config _autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null); _dialogService.ShowFilePickerAsync(Arg.Any()) .Returns((string?)null); var sut = CreateViewModel(); var originalPath = sut.ConfigFolderPath; // Wait for constructor async init to complete await Task.Delay(50); _configFileService.ClearReceivedCalls(); // Act sut.OpenFolderCommand.Execute(null); // Give async command time to complete await Task.Delay(100); // Assert await _dialogService.Received(1).ShowFilePickerAsync(Arg.Any()); await _configFileService.DidNotReceive().LoadAppSettingsAsync(Arg.Any(), Arg.Any()); sut.ConfigFolderPath.ShouldBe(originalPath); } private MainWindowViewModel CreateViewModel() { return new MainWindowViewModel( _fileSystem, _configFileService, _validationService, _backupService, _autoDiscoveryService, _dialogService, _secureStoreManager, _clipboardService, _runtimeValidationService, _connectionTestService, _logger); } }