refactor(configmanager): rename UI project and split test projects
Rename ConfigManager to ConfigManager.Ui to match the Core/CLI/UI project structure, and split the monolithic test project into Core.Tests, Cli.Tests, and Ui.Tests to align with the source project organization.
This commit is contained in:
+783
@@ -0,0 +1,783 @@
|
||||
using JdeScoping.ConfigManager.Core.Constants;
|
||||
using JdeScoping.ConfigManager.Core.Models;
|
||||
using JdeScoping.ConfigManager.Core.Services;
|
||||
using JdeScoping.ConfigManager.Core.Services.SecureStore;
|
||||
using JdeScoping.ConfigManager.Ui.Services;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels;
|
||||
using JdeScoping.ConfigManager.Ui.ViewModels.Forms;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
namespace JdeScoping.ConfigManager.Ui.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<MainWindowViewModel> _logger;
|
||||
|
||||
public MainWindowViewModelTests()
|
||||
{
|
||||
_fileSystem = Substitute.For<IFileSystem>();
|
||||
_configFileService = Substitute.For<IConfigFileService>();
|
||||
_validationService = Substitute.For<IValidationService>();
|
||||
_backupService = Substitute.For<IBackupService>();
|
||||
_autoDiscoveryService = Substitute.For<IAutoDiscoveryService>();
|
||||
_dialogService = Substitute.For<IDialogService>();
|
||||
_secureStoreManager = Substitute.For<ISecureStoreManager>();
|
||||
_clipboardService = Substitute.For<IClipboardService>();
|
||||
_runtimeValidationService = Substitute.For<IRuntimeConfigValidationService>();
|
||||
_connectionTestService = Substitute.For<IConnectionTestService>();
|
||||
_logger = Substitute.For<ILogger<MainWindowViewModel>>();
|
||||
|
||||
_validationService.ValidateAppSettings(Arg.Any<ConfigModel>())
|
||||
.Returns(new ValidationResult());
|
||||
_validationService.ValidatePipelines(Arg.Any<Dictionary<string, EtlPipelineConfig>>())
|
||||
.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>();
|
||||
((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>();
|
||||
((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>();
|
||||
((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>();
|
||||
((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>();
|
||||
((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>();
|
||||
((ExcelExportFormViewModel)sut.SelectedFormViewModel!).SelectedTimezone.ShouldBe("America/New_York");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectingPipelineNode_LoadsPipelineEditorViewModel()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["WorkOrders"] = new EtlPipelineConfig
|
||||
{
|
||||
Name = "WorkOrders",
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
|
||||
Destination = new DestinationElement { 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<PipelineEditorViewModel>();
|
||||
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 Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["Pipeline1"] = new EtlPipelineConfig { Name = "Pipeline1" },
|
||||
["Pipeline2"] = new EtlPipelineConfig { Name = "Pipeline2" }
|
||||
};
|
||||
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<string?>())
|
||||
.Returns(expectedFilePath);
|
||||
_configFileService.LoadAppSettingsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.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<string>(s => s.Contains(expectedFolder)),
|
||||
Arg.Any<CancellationToken>());
|
||||
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<string?>())
|
||||
.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<string?>());
|
||||
await _configFileService.DidNotReceive().LoadAppSettingsAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
sut.ConfigFolderPath.ShouldBe(originalPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveCommand_SavesAppSettings()
|
||||
{
|
||||
// Arrange
|
||||
var testFolderPath = "/test/config";
|
||||
var config = new ConfigModel();
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
// Simulate setting the config folder path and marking as changed
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, testFolderPath);
|
||||
sut.HasUnsavedChanges = true;
|
||||
|
||||
// Act
|
||||
sut.SaveCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - File.Exists is a static call so backup may not be called, but SaveAppSettings should be
|
||||
await _configFileService.Received().SaveAppSettingsAsync(Arg.Any<string>(), Arg.Any<ConfigModel>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveCommand_ResetsHasUnsavedChanges()
|
||||
{
|
||||
// Arrange
|
||||
var testFolderPath = Path.GetTempPath(); // Use real temp path so Directory.CreateDirectory works
|
||||
var config = new ConfigModel
|
||||
{
|
||||
Pipelines = new PipelinesSection { ConfigDirectory = "Pipelines" }
|
||||
};
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, testFolderPath);
|
||||
sut.HasUnsavedChanges = true;
|
||||
|
||||
// Act
|
||||
sut.SaveCommand.Execute(null);
|
||||
await Task.Delay(150);
|
||||
|
||||
// Assert - After successful save, HasUnsavedChanges should be false
|
||||
sut.HasUnsavedChanges.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// Note: ValidateCommand tests are skipped because the Validate() method creates
|
||||
// Avalonia SolidColorBrush objects which require UI thread access. These tests
|
||||
// would need to use [AvaloniaFact] and run in the UI context, but the command
|
||||
// validation logic is covered by the ValidationServiceTests.
|
||||
|
||||
[Fact]
|
||||
public async Task AddSecretCommand_WhenStoreNotOpen_DoesNotShowDialog()
|
||||
{
|
||||
// Arrange
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_secureStoreManager.IsStoreOpen.Returns(false);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
|
||||
// Act
|
||||
sut.AddSecretCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - Command should not execute when store is not open
|
||||
await _dialogService.DidNotReceive().ShowMessageAsync(
|
||||
Arg.Is<string>(s => s == "Add Secret"),
|
||||
Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSecretCommand_WhenStoreOpen_ShowsDialog()
|
||||
{
|
||||
// Arrange
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_secureStoreManager.IsStoreOpen.Returns(true);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
|
||||
// Act
|
||||
sut.AddSecretCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
await _dialogService.Received().ShowMessageAsync(
|
||||
Arg.Is<string>(s => s == "Add Secret"),
|
||||
Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSecretCommand_WhenConfirmed_DeletesSecret()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel
|
||||
{
|
||||
SecureStore = new SecureStoreSection
|
||||
{
|
||||
StorePath = "test.store",
|
||||
KeyFilePath = "test.key"
|
||||
}
|
||||
};
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_secureStoreManager.IsStoreOpen.Returns(true);
|
||||
_secureStoreManager.GetKeys().Returns(new List<string> { "TestSecret" });
|
||||
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(true);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
|
||||
// Set ConfigFolderPath first - required for SecureStore node to be built
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, "/test/config");
|
||||
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
// Select the secret node
|
||||
var secureStoreNode = sut.TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStore);
|
||||
secureStoreNode.ShouldNotBeNull();
|
||||
var secretNode = secureStoreNode.Children.FirstOrDefault();
|
||||
secretNode.ShouldNotBeNull();
|
||||
sut.SelectedNode = secretNode;
|
||||
|
||||
// Act
|
||||
sut.DeleteSecretCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
_secureStoreManager.Received().RemoveSecret("TestSecret");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSecretCommand_WhenCancelled_DoesNotDelete()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel
|
||||
{
|
||||
SecureStore = new SecureStoreSection
|
||||
{
|
||||
StorePath = "test.store",
|
||||
KeyFilePath = "test.key"
|
||||
}
|
||||
};
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_secureStoreManager.IsStoreOpen.Returns(true);
|
||||
_secureStoreManager.GetKeys().Returns(new List<string> { "TestSecret" });
|
||||
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
|
||||
// Set ConfigFolderPath first - required for SecureStore node to be built
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, "/test/config");
|
||||
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
// Select the secret node
|
||||
var secureStoreNode = sut.TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStore);
|
||||
secureStoreNode.ShouldNotBeNull();
|
||||
var secretNode = secureStoreNode.Children.FirstOrDefault();
|
||||
secretNode.ShouldNotBeNull();
|
||||
sut.SelectedNode = secretNode;
|
||||
|
||||
// Act
|
||||
sut.DeleteSecretCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
_secureStoreManager.DidNotReceive().RemoveSecret(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPipelineCommand_ShowsDialog_AndAddsPipeline()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>();
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_dialogService.ShowInputDialogAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns("NewPipeline");
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, pipelines);
|
||||
|
||||
// Act
|
||||
sut.AddPipelineCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
await _dialogService.Received().ShowInputDialogAsync("New Pipeline", "Enter pipeline name:");
|
||||
var pipelinesFolder = sut.TreeNodes.FirstOrDefault(n => n.Name == "Pipelines");
|
||||
pipelinesFolder.ShouldNotBeNull();
|
||||
pipelinesFolder.Children.Any(c => c.Name == "NewPipeline").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddPipelineCommand_WithDuplicateName_ShowsError()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["ExistingPipeline"] = new EtlPipelineConfig
|
||||
{
|
||||
Name = "ExistingPipeline",
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable" }
|
||||
}
|
||||
};
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_dialogService.ShowInputDialogAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns("ExistingPipeline");
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, pipelines);
|
||||
|
||||
// Act
|
||||
sut.AddPipelineCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
await _dialogService.Received().ShowMessageAsync(
|
||||
"Error",
|
||||
"Pipeline 'ExistingPipeline' already exists.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeletePipelineCommand_WhenConfirmed_RemovesPipelineFromTree()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel
|
||||
{
|
||||
Pipelines = new PipelinesSection { ConfigDirectory = "Pipelines" }
|
||||
};
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["TestPipeline"] = new EtlPipelineConfig
|
||||
{
|
||||
Name = "TestPipeline",
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable" }
|
||||
}
|
||||
};
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(true);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, pipelines);
|
||||
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, "/test/config");
|
||||
|
||||
// Select the pipeline node
|
||||
var pipelineNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "TestPipeline");
|
||||
sut.SelectedNode = pipelineNode;
|
||||
|
||||
// Act
|
||||
sut.DeletePipelineCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
await _dialogService.Received().ShowConfirmationAsync(
|
||||
"Delete Pipeline",
|
||||
"Are you sure you want to delete pipeline 'TestPipeline'?");
|
||||
// Pipeline should be removed from tree
|
||||
var pipelinesFolder = sut.TreeNodes.First(n => n.Name == "Pipelines");
|
||||
pipelinesFolder.Children.Any(c => c.SectionKey == "TestPipeline").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeletePipelineCommand_WhenCancelled_DoesNotDelete()
|
||||
{
|
||||
// Arrange
|
||||
var config = new ConfigModel();
|
||||
var pipelines = new Dictionary<string, EtlPipelineConfig>
|
||||
{
|
||||
["TestPipeline"] = new EtlPipelineConfig
|
||||
{
|
||||
Name = "TestPipeline",
|
||||
Source = new SourceElement { Connection = "jde", Query = "SELECT 1" },
|
||||
Destination = new DestinationElement { Table = "TestTable" }
|
||||
}
|
||||
};
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, pipelines);
|
||||
|
||||
// Select the pipeline node
|
||||
var pipelineNode = sut.TreeNodes
|
||||
.SelectMany(n => n.Children)
|
||||
.First(n => n.SectionKey == "TestPipeline");
|
||||
sut.SelectedNode = pipelineNode;
|
||||
var originalChildCount = sut.TreeNodes
|
||||
.First(n => n.Name == "Pipelines").Children.Count;
|
||||
|
||||
// Act
|
||||
sut.DeletePipelineCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
await _configFileService.DidNotReceive().DeletePipelineFileAsync(Arg.Any<string>());
|
||||
sut.TreeNodes.First(n => n.Name == "Pipelines").Children.Count.ShouldBe(originalChildCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateRuntimeConfigCommand_CallsRuntimeValidationService()
|
||||
{
|
||||
// Arrange
|
||||
var testFolderPath = "/test/config";
|
||||
var config = new ConfigModel();
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_runtimeValidationService.ValidateRuntimeConfig(Arg.Any<string>())
|
||||
.Returns(new List<RuntimeValidationResult>());
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, testFolderPath);
|
||||
|
||||
// Act
|
||||
sut.ValidateRuntimeConfigCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
_runtimeValidationService.Received().ValidateRuntimeConfig(testFolderPath);
|
||||
await _dialogService.Received().ShowValidationResultsAsync(
|
||||
Arg.Any<ValidationResult>(),
|
||||
Arg.Any<ValidationResult>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateRuntimeConfigCommand_WithErrors_ShowsErrorsInDialog()
|
||||
{
|
||||
// Arrange
|
||||
var testFolderPath = "/test/config";
|
||||
var config = new ConfigModel();
|
||||
|
||||
var runtimeResult = new RuntimeValidationResult
|
||||
{
|
||||
ValidatorName = "TestValidator"
|
||||
};
|
||||
runtimeResult.Errors.Add("Test runtime error");
|
||||
|
||||
_autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null);
|
||||
_runtimeValidationService.ValidateRuntimeConfig(Arg.Any<string>())
|
||||
.Returns(new List<RuntimeValidationResult> { runtimeResult });
|
||||
|
||||
var sut = CreateViewModel();
|
||||
await Task.Delay(50);
|
||||
sut.LoadConfigForTesting(config, null);
|
||||
|
||||
var configFolderProperty = typeof(MainWindowViewModel).GetProperty("ConfigFolderPath");
|
||||
configFolderProperty!.SetValue(sut, testFolderPath);
|
||||
|
||||
// Act
|
||||
sut.ValidateRuntimeConfigCommand.Execute(null);
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert
|
||||
await _dialogService.Received().ShowValidationResultsAsync(
|
||||
Arg.Is<ValidationResult>(r => r.Errors.Count > 0),
|
||||
Arg.Any<ValidationResult>());
|
||||
}
|
||||
|
||||
private MainWindowViewModel CreateViewModel()
|
||||
{
|
||||
return new MainWindowViewModel(
|
||||
_fileSystem,
|
||||
_configFileService,
|
||||
_validationService,
|
||||
_backupService,
|
||||
_autoDiscoveryService,
|
||||
_dialogService,
|
||||
_secureStoreManager,
|
||||
_clipboardService,
|
||||
_runtimeValidationService,
|
||||
_connectionTestService,
|
||||
_logger);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user