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:
Joseph Doherty
2026-01-27 07:24:55 -05:00
parent 227a749cdf
commit 937eb66ac8
14 changed files with 4053 additions and 62 deletions
@@ -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(