test(configmanager): expand unit test coverage to 451 tests
Add comprehensive tests for services (ConnectionTestService, RuntimeConfigValidation), ViewModels (PipelineEditor, dialogs, transformers), and Avalonia headless UI tests for views and forms.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using JdeScoping.ConfigManager.Constants;
|
||||
using JdeScoping.ConfigManager.Models;
|
||||
using JdeScoping.ConfigManager.Services;
|
||||
using JdeScoping.ConfigManager.Services.SecureStore;
|
||||
@@ -368,6 +369,401 @@ public class MainWindowViewModelTests
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user