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
@@ -63,4 +63,175 @@ public class BackupServiceTests
// Assert - should delete 5 oldest backups
await _fileSystem.Received(5).DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task GetBackupsAsync_ReturnsBackupsSortedByTimestampDescending()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Backups in random order
var backups = new[]
{
"/config/appsettings.2026-01-15_120000.bak",
"/config/appsettings.2026-01-10_120000.bak",
"/config/appsettings.2026-01-20_120000.bak"
};
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
// Mock GetFileNameWithoutExtension for each backup file
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
var result = await _sut.GetBackupsAsync(filePath);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(3);
// Should be sorted descending by timestamp (newest first)
result[0].Path.ShouldContain("2026-01-20");
result[1].Path.ShouldContain("2026-01-15");
result[2].Path.ShouldContain("2026-01-10");
}
[Fact]
public async Task GetBackupsAsync_WithNoBackups_ReturnsEmptyList()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(Array.Empty<string>()));
// Act
var result = await _sut.GetBackupsAsync(filePath);
// Assert
result.ShouldNotBeNull();
result.ShouldBeEmpty();
}
[Fact]
public async Task RestoreBackupAsync_CopiesBackupToTarget()
{
// Arrange
var backupPath = "/config/appsettings.2026-01-15_120000.bak";
var targetPath = "/config/appsettings.json";
// Act
await _sut.RestoreBackupAsync(backupPath, targetPath);
// Assert
await _fileSystem.Received(1).CopyFileAsync(backupPath, targetPath, Arg.Any<CancellationToken>());
}
[Fact]
public async Task CleanupOldBackupsAsync_WithFewerThanKeepCount_DeletesNone()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Only 3 backups, but keepCount is 10
var backups = new[]
{
"/config/appsettings.2026-01-15_120000.bak",
"/config/appsettings.2026-01-16_120000.bak",
"/config/appsettings.2026-01-17_120000.bak"
};
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 10);
// Assert - no files should be deleted
await _fileSystem.DidNotReceive().DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task CreateBackupAsync_WithNonExistentFile_ThrowsFileNotFoundException()
{
// Arrange
var sourcePath = "/config/nonexistent.json";
_fileSystem.FileExists(sourcePath).Returns(false);
// Act & Assert
await Should.ThrowAsync<FileNotFoundException>(
() => _sut.CreateBackupAsync(sourcePath));
}
[Fact]
public async Task GetBackupsAsync_SkipsFilesWithInvalidTimestampFormat()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Mix of valid and invalid backup filenames
var backups = new[]
{
"/config/appsettings.2026-01-15_120000.bak", // Valid
"/config/appsettings.invalid-format.bak", // Invalid
"/config/appsettings.2026-01-16_120000.bak" // Valid
};
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
_fileSystem.GetFileNameWithoutExtension(backups[0]).Returns("appsettings.2026-01-15_120000");
_fileSystem.GetFileNameWithoutExtension(backups[1]).Returns("appsettings.invalid-format");
_fileSystem.GetFileNameWithoutExtension(backups[2]).Returns("appsettings.2026-01-16_120000");
// Act
var result = await _sut.GetBackupsAsync(filePath);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(2); // Only valid backups
}
[Fact]
public async Task CleanupOldBackupsAsync_WithExactKeepCount_DeletesNone()
{
// Arrange
var filePath = "/config/appsettings.json";
_fileSystem.GetDirectoryName(filePath).Returns("/config");
_fileSystem.GetFileNameWithoutExtension(filePath).Returns("appsettings");
// Exactly 5 backups with keepCount of 5
var backups = Enumerable.Range(1, 5)
.Select(i => $"/config/appsettings.2026-01-{i:D2}_120000.bak")
.ToArray();
_fileSystem.GetFilesAsync("/config", "appsettings.*.bak", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(backups));
foreach (var backup in backups)
{
var fileName = backup.Split('/').Last().Replace(".bak", "");
_fileSystem.GetFileNameWithoutExtension(backup).Returns(fileName);
}
// Act
await _sut.CleanupOldBackupsAsync(filePath, keepCount: 5);
// Assert - no files should be deleted
await _fileSystem.DidNotReceive().DeleteFileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
}
@@ -1,5 +1,6 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -51,4 +52,221 @@ public class ConfigFileServiceTests
() => _sut.LoadAppSettingsAsync("/config/appsettings.json"));
ex.Message.ShouldContain("parse");
}
[Fact]
public async Task LoadPipelineAsync_WithValidJson_ReturnsPipelineConfig()
{
// Arrange
var json = """
{
"name": "TestPipeline",
"isEnabled": true,
"massSyncIntervalMinutes": 1440,
"source": {
"connectionStringName": "JDE",
"query": "SELECT * FROM test"
},
"destination": {
"connectionStringName": "LotFinder",
"tableName": "TestTable"
}
}
""";
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(json));
// Act
var result = await _sut.LoadPipelineAsync("/config/pipeline.test.json");
// Assert
result.ShouldNotBeNull();
result.Name.ShouldBe("TestPipeline");
result.IsEnabled.ShouldBeTrue();
result.MassSyncIntervalMinutes.ShouldBe(1440);
}
[Fact]
public async Task LoadPipelineAsync_WithInvalidJson_ThrowsConfigLoadException()
{
// Arrange
var json = "{ invalid json }";
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult(json));
// Act & Assert
var ex = await Should.ThrowAsync<ConfigLoadException>(
() => _sut.LoadPipelineAsync("/config/pipeline.test.json"));
ex.Message.ShouldContain("parse");
}
[Fact]
public async Task SavePipelineAsync_SerializesAndWritesPipeline()
{
// Arrange
var path = "/config/pipeline.test.json";
var pipeline = new EtlPipelineConfig
{
Name = "TestPipeline",
IsEnabled = true,
MassSyncIntervalMinutes = 1440
};
string? writtenContent = null;
_fileSystem.WriteAllTextAsync(Arg.Any<string>(), Arg.Do<string>(c => writtenContent = c), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
// Act
await _sut.SavePipelineAsync(path, pipeline);
// Assert
await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any<string>(), Arg.Any<CancellationToken>());
writtenContent.ShouldNotBeNull();
writtenContent.ShouldContain("TestPipeline");
}
[Fact]
public async Task LoadAllPipelinesAsync_WithMultiplePipelines_ReturnsAll()
{
// Arrange
var directory = "/config/pipelines";
var files = new[]
{
"/config/pipelines/pipeline.first.json",
"/config/pipelines/pipeline.second.json"
};
_fileSystem.DirectoryExists(directory).Returns(true);
_fileSystem.GetFilesAsync(directory, "pipeline.*.json", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(files));
_fileSystem.ReadAllTextAsync(files[0], Arg.Any<CancellationToken>())
.Returns(Task.FromResult("""{"name": "First", "isEnabled": true}"""));
_fileSystem.ReadAllTextAsync(files[1], Arg.Any<CancellationToken>())
.Returns(Task.FromResult("""{"name": "Second", "isEnabled": false}"""));
_fileSystem.GetFileNameWithoutExtension(files[0]).Returns("pipeline.first");
_fileSystem.GetFileNameWithoutExtension(files[1]).Returns("pipeline.second");
// Act
var result = await _sut.LoadAllPipelinesAsync(directory);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(2);
result.ContainsKey("first").ShouldBeTrue();
result.ContainsKey("second").ShouldBeTrue();
result["first"].Name.ShouldBe("First");
result["second"].IsEnabled.ShouldBeFalse();
}
[Fact]
public async Task LoadAllPipelinesAsync_WithNonExistentDirectory_ReturnsEmpty()
{
// Arrange
var directory = "/config/nonexistent";
_fileSystem.DirectoryExists(directory).Returns(false);
// Act
var result = await _sut.LoadAllPipelinesAsync(directory);
// Assert
result.ShouldNotBeNull();
result.ShouldBeEmpty();
}
[Fact]
public async Task DeletePipelineFileAsync_CallsFileSystemDelete()
{
// Arrange
var path = "/config/pipeline.test.json";
// Act
await _sut.DeletePipelineFileAsync(path);
// Assert
await _fileSystem.Received(1).DeleteFileAsync(path, Arg.Any<CancellationToken>());
}
[Fact]
public async Task SaveAppSettingsAsync_SerializesAndWritesConfig()
{
// Arrange
var path = "/config/appsettings.json";
var config = new ConfigModel
{
DataSync = new DataSyncSection
{
Enabled = true,
MaxDegreeOfParallelism = 16
}
};
string? writtenContent = null;
_fileSystem.WriteAllTextAsync(Arg.Any<string>(), Arg.Do<string>(c => writtenContent = c), Arg.Any<CancellationToken>())
.Returns(Task.CompletedTask);
// Act
await _sut.SaveAppSettingsAsync(path, config);
// Assert
await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any<string>(), Arg.Any<CancellationToken>());
writtenContent.ShouldNotBeNull();
writtenContent.ShouldContain("enabled");
writtenContent.ShouldContain("16");
}
[Fact]
public async Task LoadAllPipelinesAsync_SkipsInvalidPipelineFiles()
{
// Arrange
var directory = "/config/pipelines";
var files = new[]
{
"/config/pipelines/pipeline.valid.json",
"/config/pipelines/pipeline.invalid.json"
};
_fileSystem.DirectoryExists(directory).Returns(true);
_fileSystem.GetFilesAsync(directory, "pipeline.*.json", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(files));
_fileSystem.ReadAllTextAsync(files[0], Arg.Any<CancellationToken>())
.Returns(Task.FromResult("""{"name": "Valid", "isEnabled": true}"""));
_fileSystem.ReadAllTextAsync(files[1], Arg.Any<CancellationToken>())
.Returns(Task.FromResult("{ invalid json }"));
_fileSystem.GetFileNameWithoutExtension(files[0]).Returns("pipeline.valid");
_fileSystem.GetFileNameWithoutExtension(files[1]).Returns("pipeline.invalid");
// Act
var result = await _sut.LoadAllPipelinesAsync(directory);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(1);
result.ContainsKey("valid").ShouldBeTrue();
}
[Fact]
public async Task LoadAllPipelinesAsync_AssignsPipelineNameFromFilename_WhenNameIsEmpty()
{
// Arrange
var directory = "/config/pipelines";
var files = new[] { "/config/pipelines/pipeline.unnamed.json" };
_fileSystem.DirectoryExists(directory).Returns(true);
_fileSystem.GetFilesAsync(directory, "pipeline.*.json", Arg.Any<CancellationToken>())
.Returns(Task.FromResult(files));
// Pipeline with empty name
_fileSystem.ReadAllTextAsync(files[0], Arg.Any<CancellationToken>())
.Returns(Task.FromResult("""{"name": "", "isEnabled": true}"""));
_fileSystem.GetFileNameWithoutExtension(files[0]).Returns("pipeline.unnamed");
// Act
var result = await _sut.LoadAllPipelinesAsync(directory);
// Assert
result.ShouldNotBeNull();
result.Count.ShouldBe(1);
result["unnamed"].Name.ShouldBe("unnamed");
}
}
@@ -0,0 +1,130 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
public class ConnectionTestServiceTests
{
private readonly ConnectionTestService _sut;
public ConnectionTestServiceTests()
{
_sut = new ConnectionTestService();
}
[Fact]
public async Task TestConnectionAsync_Oracle_ReturnsNotImplemented()
{
// Arrange
var connectionString = "Data Source=oracle;User Id=user;Password=pass;";
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.Oracle);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldContain("not implemented");
}
[Fact]
public async Task TestConnectionAsync_Generic_ReturnsCannotTest()
{
// Arrange
var connectionString = "SomeGenericConnectionString";
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.Generic);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldContain("Cannot test generic");
}
[Fact]
public async Task TestConnectionAsync_UnknownProvider_ReturnsUnknownProviderError()
{
// Arrange
var connectionString = "Data Source=test";
var unknownProvider = (ConnectionProvider)999;
// Act
var result = await _sut.TestConnectionAsync(connectionString, unknownProvider);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldContain("Unknown provider");
}
[Fact]
public async Task TestConnectionAsync_SqlServer_WithInvalidConnectionString_ReturnsFailure()
{
// Arrange - Use an obviously invalid connection string that will fail fast
var connectionString = "Server=nonexistent-server-that-does-not-exist-12345;Database=TestDb;Integrated Security=true;Connect Timeout=1;";
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldNotBeNullOrEmpty();
result.Duration.ShouldNotBeNull();
}
[Fact]
public async Task TestConnectionAsync_SqlServer_MeasuresDuration()
{
// Arrange - Use an invalid connection string that will fail but should still measure duration
var connectionString = "Server=nonexistent-server-12345;Database=TestDb;Integrated Security=true;Connect Timeout=1;";
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
// Assert
result.Duration.ShouldNotBeNull();
result.Duration!.Value.ShouldBeGreaterThan(TimeSpan.Zero);
}
[Fact]
public async Task TestConnectionAsync_SqlServer_WithCancellation_ReturnsCancelledResult()
{
// Arrange
var connectionString = "Server=nonexistent-server-that-takes-forever-12345;Database=TestDb;Integrated Security=true;Connect Timeout=30;";
using var cts = new CancellationTokenSource();
cts.Cancel(); // Cancel immediately
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer, cts.Token);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldContain("cancelled");
}
[Fact]
public async Task TestConnectionAsync_SqlServer_WithMalformedConnectionString_ReturnsFailure()
{
// Arrange - Malformed connection string should be handled gracefully
var connectionString = "not a valid connection string at all!!!";
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldNotBeNullOrEmpty();
}
[Fact]
public async Task TestConnectionAsync_SqlServer_WithEmptyConnectionString_ReturnsFailure()
{
// Arrange
var connectionString = "";
// Act
var result = await _sut.TestConnectionAsync(connectionString, ConnectionProvider.SqlServer);
// Assert
result.Success.ShouldBeFalse();
result.Message.ShouldNotBeNullOrEmpty();
}
}
@@ -0,0 +1,176 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
namespace JdeScoping.ConfigManager.Tests.Services;
public class RuntimeConfigValidationServiceTests : IDisposable
{
private readonly string _testDirectory;
private readonly ISecureStoreManager _secureStoreManager;
private readonly RuntimeConfigValidationService _sut;
public RuntimeConfigValidationServiceTests()
{
_testDirectory = Path.Combine(Path.GetTempPath(), $"RuntimeConfigValidationTests_{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDirectory);
_secureStoreManager = Substitute.For<ISecureStoreManager>();
_sut = new RuntimeConfigValidationService(_secureStoreManager);
}
public void Dispose()
{
if (Directory.Exists(_testDirectory))
{
Directory.Delete(_testDirectory, recursive: true);
}
}
[Fact]
public void ValidateRuntimeConfig_WithMissingAppSettings_ReturnsConfigurationError()
{
// Arrange - directory exists but no appsettings.json
var configFolder = Path.Combine(_testDirectory, "missing");
Directory.CreateDirectory(configFolder);
// Act
var results = _sut.ValidateRuntimeConfig(configFolder);
// Assert
results.ShouldNotBeEmpty();
var configResult = results.FirstOrDefault(r => r.ValidatorName == "Configuration");
configResult.ShouldNotBeNull();
configResult.IsValid.ShouldBeFalse();
configResult.Errors.ShouldContain(e => e.Contains("appsettings.json not found"));
}
[Fact]
public void ValidateRuntimeConfig_WhenSecureStoreNotOpen_AddsWarning()
{
// Arrange
var configFolder = _testDirectory;
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
_secureStoreManager.IsStoreOpen.Returns(false);
// Act
var results = _sut.ValidateRuntimeConfig(configFolder);
// Assert
var storeResult = results.FirstOrDefault(r => r.ValidatorName == "SecureStore");
storeResult.ShouldNotBeNull();
storeResult.Warnings.ShouldContain(w => w.Contains("No SecureStore is currently open"));
}
[Fact]
public void ValidateRuntimeConfig_WhenSecureStoreIsOpen_NoSecureStoreWarning()
{
// Arrange
var configFolder = _testDirectory;
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
File.WriteAllText(appSettingsPath, "{}");
_secureStoreManager.IsStoreOpen.Returns(true);
// Act
var results = _sut.ValidateRuntimeConfig(configFolder);
// Assert
// There may be a SecureStore result from validators, but it should not have the
// "No SecureStore is currently open" warning
var storeResults = results.Where(r => r.ValidatorName == "SecureStore").ToList();
foreach (var storeResult in storeResults)
{
storeResult.Warnings.ShouldNotContain(w => w.Contains("No SecureStore is currently open"));
}
}
[Fact]
public void ValidateRuntimeConfig_WithValidAppSettings_RunsValidators()
{
// Arrange
var configFolder = _testDirectory;
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
var validConfig = """
{
"ConnectionStrings": {},
"DataSync": {
"Enabled": true
}
}
""";
File.WriteAllText(appSettingsPath, validConfig);
_secureStoreManager.IsStoreOpen.Returns(true);
// Act
var results = _sut.ValidateRuntimeConfig(configFolder);
// Assert
// Should return results (empty or with validators) without throwing
results.ShouldNotBeNull();
}
[Fact]
public void ValidateRuntimeConfig_WithNonExistentDirectory_ReturnsConfigurationError()
{
// Arrange
var nonExistentFolder = Path.Combine(_testDirectory, "does-not-exist");
// Act
var results = _sut.ValidateRuntimeConfig(nonExistentFolder);
// Assert
results.ShouldNotBeEmpty();
var configResult = results.FirstOrDefault(r => r.ValidatorName == "Configuration");
configResult.ShouldNotBeNull();
configResult.IsValid.ShouldBeFalse();
configResult.Errors.ShouldContain(e => e.Contains("not found"));
}
[Fact]
public void ValidateRuntimeConfig_WithInvalidJson_ReturnsError()
{
// Arrange
var configFolder = _testDirectory;
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
File.WriteAllText(appSettingsPath, "{ invalid json }");
_secureStoreManager.IsStoreOpen.Returns(true);
// Act & Assert
// The service should either throw or return an error result for invalid JSON
Should.Throw<Exception>(() => _sut.ValidateRuntimeConfig(configFolder));
}
[Fact]
public void ValidateRuntimeConfig_ReturnsMultipleResults_WhenMultipleValidatorsRun()
{
// Arrange
var configFolder = _testDirectory;
var appSettingsPath = Path.Combine(configFolder, "appsettings.json");
var validConfig = """
{
"ConnectionStrings": {},
"DataSync": {
"Enabled": true
},
"SecureStore": {
"StorePath": "data/secrets.json",
"KeyFilePath": "data/secrets.key"
}
}
""";
File.WriteAllText(appSettingsPath, validConfig);
_secureStoreManager.IsStoreOpen.Returns(false);
// Act
var results = _sut.ValidateRuntimeConfig(configFolder);
// Assert
// Should have at least the SecureStore warning
results.ShouldNotBeEmpty();
}
}
@@ -0,0 +1,21 @@
using Avalonia;
using Avalonia.Headless;
using JdeScoping.ConfigManager;
[assembly: AvaloniaTestApplication(typeof(JdeScoping.ConfigManager.Tests.TestAppBuilder))]
namespace JdeScoping.ConfigManager.Tests;
/// <summary>
/// Provides a headless Avalonia application builder for UI tests.
/// </summary>
public class TestAppBuilder
{
/// <summary>
/// Builds the Avalonia application configured for headless testing.
/// </summary>
/// <returns>An <see cref="AppBuilder"/> configured with headless platform options.</returns>
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UseHeadless(new AvaloniaHeadlessPlatformOptions());
}
@@ -0,0 +1,241 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
public class DiffPreviewDialogViewModelTests
{
[Fact]
public void Constructor_WithEmptyDiff_InitializesCorrectly()
{
// Arrange
var diff = new DiffResult
{
HasChanges = false,
Lines = [],
Insertions = 0,
Deletions = 0
};
// Act
var sut = new DiffPreviewDialogViewModel(diff);
// Assert
sut.Lines.Count.ShouldBe(0);
sut.HasChanges.ShouldBeFalse();
sut.Insertions.ShouldBe(0);
sut.Deletions.ShouldBe(0);
sut.Result.ShouldBeFalse();
}
[Fact]
public void Constructor_WithChanges_PopulatesLines()
{
// Arrange
var diff = new DiffResult
{
HasChanges = true,
Lines =
[
new DiffLine { OldLineNumber = 1, NewLineNumber = 1, Text = "unchanged line", Type = DiffLineType.Unchanged },
new DiffLine { OldLineNumber = 2, NewLineNumber = null, Text = "removed line", Type = DiffLineType.Removed },
new DiffLine { OldLineNumber = null, NewLineNumber = 2, Text = "added line", Type = DiffLineType.Added }
],
Insertions = 1,
Deletions = 1
};
// Act
var sut = new DiffPreviewDialogViewModel(diff);
// Assert
sut.Lines.Count.ShouldBe(3);
sut.HasChanges.ShouldBeTrue();
sut.Insertions.ShouldBe(1);
sut.Deletions.ShouldBe(1);
}
[Fact]
public void SaveCommand_SetsResultTrue_AndInvokesRequestClose()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
var closeInvoked = false;
sut.RequestClose = () => closeInvoked = true;
// Act
sut.SaveCommand.Execute(null);
// Assert
sut.Result.ShouldBeTrue();
closeInvoked.ShouldBeTrue();
}
[Fact]
public void CancelCommand_SetsResultFalse_AndInvokesRequestClose()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
var closeInvoked = false;
sut.RequestClose = () => closeInvoked = true;
// Act
sut.CancelCommand.Execute(null);
// Assert
sut.Result.ShouldBeFalse();
closeInvoked.ShouldBeTrue();
}
[Fact]
public void SaveCommand_DoesNotThrow_WhenRequestCloseIsNull()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
sut.RequestClose = null;
// Act & Assert - Should not throw
Should.NotThrow(() => sut.SaveCommand.Execute(null));
sut.Result.ShouldBeTrue();
}
[Fact]
public void CancelCommand_DoesNotThrow_WhenRequestCloseIsNull()
{
// Arrange
var diff = CreateEmptyDiff();
var sut = new DiffPreviewDialogViewModel(diff);
sut.RequestClose = null;
// Act & Assert - Should not throw
Should.NotThrow(() => sut.CancelCommand.Execute(null));
sut.Result.ShouldBeFalse();
}
[Fact]
public void Constructor_ThrowsOnNullDiff()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() => new DiffPreviewDialogViewModel(null!));
}
[Fact]
public void Result_InitialValue_IsFalse()
{
// Arrange
var diff = CreateEmptyDiff();
// Act
var sut = new DiffPreviewDialogViewModel(diff);
// Assert
sut.Result.ShouldBeFalse();
}
private static DiffResult CreateEmptyDiff()
{
return new DiffResult
{
HasChanges = false,
Lines = [],
Insertions = 0,
Deletions = 0
};
}
}
public class DiffLineViewModelTests
{
[Fact]
public void Constructor_UnchangedLine_SetsPropertiesCorrectly()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = 5,
NewLineNumber = 5,
Text = "unchanged content",
Type = DiffLineType.Unchanged
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("5");
sut.NewLineNumber.ShouldBe("5");
sut.Text.ShouldBe("unchanged content");
sut.Type.ShouldBe(DiffLineType.Unchanged);
sut.Background.ShouldBe("Transparent");
sut.BorderColor.ShouldBe("Transparent");
}
[Fact]
public void Constructor_AddedLine_SetsGreenStyling()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = null,
NewLineNumber = 10,
Text = "new line",
Type = DiffLineType.Added
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("");
sut.NewLineNumber.ShouldBe("10");
sut.Type.ShouldBe(DiffLineType.Added);
sut.Background.ShouldBe("#1A3DD68C");
sut.BorderColor.ShouldBe("#3DD68C");
}
[Fact]
public void Constructor_RemovedLine_SetsRedStyling()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = 7,
NewLineNumber = null,
Text = "deleted line",
Type = DiffLineType.Removed
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("7");
sut.NewLineNumber.ShouldBe("");
sut.Type.ShouldBe(DiffLineType.Removed);
sut.Background.ShouldBe("#1AFF6B6B");
sut.BorderColor.ShouldBe("#FF6B6B");
}
[Fact]
public void Constructor_NullLineNumbers_ReturnsEmptyStrings()
{
// Arrange
var line = new DiffLine
{
OldLineNumber = null,
NewLineNumber = null,
Text = "text",
Type = DiffLineType.Unchanged
};
// Act
var sut = new DiffLineViewModel(line);
// Assert
sut.OldLineNumber.ShouldBe("");
sut.NewLineNumber.ShouldBe("");
}
}
@@ -0,0 +1,193 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
public class ValidationResultsDialogViewModelTests
{
[Fact]
public void Constructor_WithEmptyResults_HasNoItems()
{
// Arrange
var appSettingsResult = new ValidationResult();
var pipelinesResult = new ValidationResult();
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(0);
sut.ErrorCount.ShouldBe(0);
sut.WarningCount.ShouldBe(0);
sut.IsValid.ShouldBeTrue();
}
[Fact]
public void Constructor_WithErrors_PopulatesItemsCorrectly()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddError("Missing connection string");
appSettingsResult.AddError("Invalid timeout value");
var pipelinesResult = new ValidationResult();
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(2);
sut.ErrorCount.ShouldBe(2);
sut.WarningCount.ShouldBe(0);
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void Constructor_WithWarnings_PopulatesItemsCorrectly()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddWarning("Deprecated setting used");
var pipelinesResult = new ValidationResult();
pipelinesResult.AddWarning("Pipeline has no transformers");
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(2);
sut.ErrorCount.ShouldBe(0);
sut.WarningCount.ShouldBe(2);
sut.IsValid.ShouldBeFalse(); // Both errors and warnings make it invalid
}
[Fact]
public void Constructor_WithMixedResults_PopulatesAllItems()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddError("Error in appsettings");
appSettingsResult.AddWarning("Warning in appsettings");
var pipelinesResult = new ValidationResult();
pipelinesResult.AddError("Error in pipelines");
pipelinesResult.AddWarning("Warning in pipelines");
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.Count.ShouldBe(4);
sut.ErrorCount.ShouldBe(2);
sut.WarningCount.ShouldBe(2);
sut.IsValid.ShouldBeFalse();
}
[Fact]
public void Constructor_SetsCorrectSourceOnItems()
{
// Arrange
var appSettingsResult = new ValidationResult();
appSettingsResult.AddError("App error");
var pipelinesResult = new ValidationResult();
pipelinesResult.AddError("Pipeline error");
// Act
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
// Assert
sut.Items.ShouldContain(i => i.Source == "appsettings.json" && i.Message == "App error");
sut.Items.ShouldContain(i => i.Source == "pipelines.json" && i.Message == "Pipeline error");
}
[Fact]
public void CloseCommand_InvokesRequestClose()
{
// Arrange
var appSettingsResult = new ValidationResult();
var pipelinesResult = new ValidationResult();
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
var closeInvoked = false;
sut.RequestClose = () => closeInvoked = true;
// Act
sut.CloseCommand.Execute(null);
// Assert
closeInvoked.ShouldBeTrue();
}
[Fact]
public void CloseCommand_DoesNotThrow_WhenRequestCloseIsNull()
{
// Arrange
var appSettingsResult = new ValidationResult();
var pipelinesResult = new ValidationResult();
var sut = new ValidationResultsDialogViewModel(appSettingsResult, pipelinesResult);
sut.RequestClose = null;
// Act & Assert - Should not throw
Should.NotThrow(() => sut.CloseCommand.Execute(null));
}
[Fact]
public void Constructor_ThrowsOnNullAppSettingsResult()
{
// Arrange
var pipelinesResult = new ValidationResult();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ValidationResultsDialogViewModel(null!, pipelinesResult));
}
[Fact]
public void Constructor_ThrowsOnNullPipelinesResult()
{
// Arrange
var appSettingsResult = new ValidationResult();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new ValidationResultsDialogViewModel(appSettingsResult, null!));
}
}
public class ValidationItemViewModelTests
{
[Fact]
public void Constructor_SetsPropertiesCorrectly()
{
// Act
var sut = new ValidationItemViewModel("Test message", "test.json", ValidationItemType.Error);
// Assert
sut.Message.ShouldBe("Test message");
sut.Source.ShouldBe("test.json");
sut.Type.ShouldBe(ValidationItemType.Error);
}
[Fact]
public void Constructor_ErrorType_SetsErrorStyling()
{
// Act
var sut = new ValidationItemViewModel("Error", "test.json", ValidationItemType.Error);
// Assert
sut.Icon.ShouldBe("\u2717"); // X mark
sut.IconColor.ShouldBe("#FF6B6B");
sut.Background.ShouldBe("#1AFF6B6B");
sut.BorderColor.ShouldBe("#FF6B6B");
}
[Fact]
public void Constructor_WarningType_SetsWarningStyling()
{
// Act
var sut = new ValidationItemViewModel("Warning", "test.json", ValidationItemType.Warning);
// Assert
sut.Icon.ShouldBe("\u26A0"); // Warning sign
sut.IconColor.ShouldBe("#FFB84D");
sut.Background.ShouldBe("#1AFFB84D");
sut.BorderColor.ShouldBe("#FFB84D");
}
}
@@ -22,7 +22,7 @@ public class ConnectionStringsFormViewModelTests
}
[Fact]
public void Constructor_InitializesFromModel()
public void Constructor_InitializesFromModel()
{
// Arrange
var model = new ConnectionStringsSection
@@ -54,67 +54,67 @@ public class ConnectionStringsFormViewModelTests
sut.Connections[0].Server.ShouldBe("server1");
sut.Connections[1].Name.ShouldBe("Connection2");
sut.Connections[1].Provider.ShouldBe(ConnectionProvider.Oracle);
sut.Connections[1].Host.ShouldBe("oracle-host");
}
[Fact]
public void Constructor_LoadsAndParsesSqlServerConnectionStringFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "LotFinder" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetSecret("LotFinder")
.Returns("Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=pass;TrustServerCertificate=true");
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("localhost,1434");
sut.Connections[0].Database.ShouldBe("ScopingTool");
sut.Connections[0].UserId.ShouldBe("scopingapp");
sut.Connections[0].Password.ShouldBe("pass");
sut.Connections[0].TrustServerCertificate.ShouldBeTrue();
}
[Fact]
public void Constructor_LoadsAndParsesOracleConnectionStringFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "CMS" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetSecret("CMS")
.Returns("HOST=ha-iman;Service Name=imanprd;Fetch Array Size=1280000;Port=1522;User ID=app_teamcenter;Password=pass;");
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.Oracle);
sut.Connections[0].Host.ShouldBe("ha-iman");
sut.Connections[0].ServiceName.ShouldBe("imanprd");
sut.Connections[0].Port.ShouldBe(1522);
sut.Connections[0].UserId.ShouldBe("app_teamcenter");
sut.Connections[0].Password.ShouldBe("pass");
}
[Fact]
public void Constructor_ThrowsOnNullModel()
sut.Connections[1].Host.ShouldBe("oracle-host");
}
[Fact]
public void Constructor_LoadsAndParsesSqlServerConnectionStringFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "LotFinder" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetSecret("LotFinder")
.Returns("Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=pass;TrustServerCertificate=true");
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("localhost,1434");
sut.Connections[0].Database.ShouldBe("ScopingTool");
sut.Connections[0].UserId.ShouldBe("scopingapp");
sut.Connections[0].Password.ShouldBe("pass");
sut.Connections[0].TrustServerCertificate.ShouldBeTrue();
}
[Fact]
public void Constructor_LoadsAndParsesOracleConnectionStringFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "CMS" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_secureStoreManager.GetSecret("CMS")
.Returns("HOST=ha-iman;Service Name=imanprd;Fetch Array Size=1280000;Port=1522;User ID=app_teamcenter;Password=pass;");
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.Oracle);
sut.Connections[0].Host.ShouldBe("ha-iman");
sut.Connections[0].ServiceName.ShouldBe("imanprd");
sut.Connections[0].Port.ShouldBe(1522);
sut.Connections[0].UserId.ShouldBe("app_teamcenter");
sut.Connections[0].Password.ShouldBe("pass");
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
@@ -237,4 +237,426 @@ public class ConnectionStringsFormViewModelTests
// Assert
sut.ConnectionCount.ShouldBe(4);
}
[Fact]
public void Constructor_InitializesConnectionsFromModel()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb",
UserId = "sa",
Password = "secret"
}
}
};
// Act
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Name.ShouldBe("TestConnection");
sut.Connections[0].Provider.ShouldBe(ConnectionProvider.SqlServer);
sut.Connections[0].Server.ShouldBe("localhost");
sut.Connections[0].Database.ShouldBe("TestDb");
}
[Fact]
public void AddConnectionCommand_AddsNewConnection()
{
// Arrange
var model = new ConnectionStringsSection();
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
var initialCount = sut.Connections.Count;
// Act
sut.AddConnectionCommand.Execute(null);
// Assert
sut.Connections.Count.ShouldBe(initialCount + 1);
sut.Connections.Last().Name.ShouldBe("NewConnection");
sut.Connections.Last().Provider.ShouldBe(ConnectionProvider.Generic);
}
[Fact]
public async Task DeleteConnectionCommand_WhenConfirmed_RemovesConnection()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "ToDelete" }
}
};
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.DeleteConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
sut.Connections.Count.ShouldBe(0);
model.Entries.Count.ShouldBe(0);
}
[Fact]
public async Task DeleteConnectionCommand_WhenCancelled_KeepsConnection()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "ToKeep" }
}
};
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(false);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.DeleteConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
sut.Connections.Count.ShouldBe(1);
sut.Connections[0].Name.ShouldBe("ToKeep");
}
[Fact]
public async Task ValidateConnectionCommand_WithEmptyConnectionString_ShowsError()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "EmptyConnection",
Provider = ConnectionProvider.Generic,
RawConnectionString = ""
}
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.ValidateConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Validation Failed",
Arg.Is<string>(s => s.Contains("empty connection string")));
}
[Fact]
public async Task ValidateConnectionCommand_WithValidConnectionString_ShowsSuccess()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "ValidConnection",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.ValidateConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Validation Passed",
Arg.Is<string>(s => s.Contains("valid connection string")));
}
[Fact]
public async Task TestConnectionCommand_WhenSuccessful_ShowsSuccessMessage()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConn",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
_connectionTestService.TestConnectionAsync(Arg.Any<string>(), Arg.Any<ConnectionProvider>(), Arg.Any<CancellationToken>())
.Returns(new ConnectionTestResult { Success = true, Duration = TimeSpan.FromMilliseconds(50) });
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.TestConnectionCommand.Execute(null);
await Task.Delay(150);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Connection Successful",
Arg.Is<string>(s => s.Contains("Successfully connected")));
}
[Fact]
public async Task TestConnectionCommand_WhenFailed_ShowsErrorMessage()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "FailConn",
Provider = ConnectionProvider.SqlServer,
Server = "badserver",
Database = "TestDb"
}
}
};
_connectionTestService.TestConnectionAsync(Arg.Any<string>(), Arg.Any<ConnectionProvider>(), Arg.Any<CancellationToken>())
.Returns(new ConnectionTestResult { Success = false, Message = "Connection refused" });
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.TestConnectionCommand.Execute(null);
await Task.Delay(150);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Connection Failed",
Arg.Is<string>(s => s.Contains("Failed to connect") && s.Contains("Connection refused")));
}
[Fact]
public async Task TestConnectionCommand_SetsIsTesting_DuringExecution()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConn",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
var taskCompletionSource = new TaskCompletionSource<ConnectionTestResult>();
_connectionTestService.TestConnectionAsync(Arg.Any<string>(), Arg.Any<ConnectionProvider>(), Arg.Any<CancellationToken>())
.Returns(taskCompletionSource.Task);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act - Start the command
sut.TestConnectionCommand.Execute(null);
await Task.Delay(50);
// Assert - IsTesting should be true during execution
sut.IsTesting.ShouldBeTrue();
// Complete the task
taskCompletionSource.SetResult(new ConnectionTestResult { Success = true });
await Task.Delay(100);
// Assert - IsTesting should be false after completion
sut.IsTesting.ShouldBeFalse();
}
[Fact]
public void SelectedConnection_WhenChanged_RaisesHasSelectionPropertyChanged()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "Conn1" }
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
var hasSelectionChangedRaised = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(ConnectionStringsFormViewModel.HasSelection))
hasSelectionChangedRaised = true;
};
// Act
sut.SelectedConnection = sut.Connections[0];
// Assert
hasSelectionChangedRaised.ShouldBeTrue();
}
[Fact]
public void OnEntryChanged_SavesValueToSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "TestConn",
Provider = ConnectionProvider.SqlServer,
Server = "localhost",
Database = "TestDb"
}
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
var onChangedCalled = false;
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => onChangedCalled = true, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act - Change a property on the selected connection
sut.SelectedConnection.Database = "NewDatabase";
// Assert
onChangedCalled.ShouldBeTrue();
_secureStoreManager.Received().SetSecret("TestConn", Arg.Any<string>());
}
[Fact]
public async Task DeleteConnectionCommand_RemovesFromSecureStore()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry { Name = "ToDelete" }
}
};
_secureStoreManager.IsStoreOpen.Returns(true);
_dialogService.ShowConfirmationAsync(Arg.Any<string>(), Arg.Any<string>())
.Returns(true);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.DeleteConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
_secureStoreManager.Received().RemoveSecret("ToDelete");
}
[Fact]
public void AddConnectionCommand_CreatesSecureStoreEntry()
{
// Arrange
var model = new ConnectionStringsSection();
_secureStoreManager.IsStoreOpen.Returns(true);
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
// Act
sut.AddConnectionCommand.Execute(null);
// Assert
_secureStoreManager.Received().SetSecret("NewConnection", string.Empty);
}
[Fact]
public async Task TestConnectionCommand_WithEmptyConnectionString_ShowsMessage()
{
// Arrange
var model = new ConnectionStringsSection
{
Entries = new List<ConnectionStringEntry>
{
new ConnectionStringEntry
{
Name = "EmptyConn",
Provider = ConnectionProvider.Generic,
RawConnectionString = ""
}
}
};
var sut = new ConnectionStringsFormViewModel(model, _secureStoreManager, () => { }, _dialogService, _connectionTestService);
sut.SelectedConnection = sut.Connections[0];
// Act
sut.TestConnectionCommand.Execute(null);
await Task.Delay(100);
// Assert
await _dialogService.Received().ShowMessageAsync(
"Test Connection",
Arg.Is<string>(s => s.Contains("connection string is empty")));
await _connectionTestService.DidNotReceive().TestConnectionAsync(
Arg.Any<string>(),
Arg.Any<ConnectionProvider>(),
Arg.Any<CancellationToken>());
}
[Fact]
public void AvailableProviders_ContainsAllProviders()
{
// Assert
ConnectionStringsFormViewModel.AvailableProviders.Count.ShouldBe(
Enum.GetValues<ConnectionProvider>().Length);
ConnectionStringsFormViewModel.AvailableProviders.ShouldContain(ConnectionProvider.SqlServer);
ConnectionStringsFormViewModel.AvailableProviders.ShouldContain(ConnectionProvider.Oracle);
ConnectionStringsFormViewModel.AvailableProviders.ShouldContain(ConnectionProvider.Generic);
}
[Fact]
public void EncryptOptions_ContainsExpectedValues()
{
// Assert
ConnectionStringsFormViewModel.EncryptOptions.Count.ShouldBe(3);
ConnectionStringsFormViewModel.EncryptOptions.ShouldContain("True");
ConnectionStringsFormViewModel.EncryptOptions.ShouldContain("False");
ConnectionStringsFormViewModel.EncryptOptions.ShouldContain("Strict");
}
}
@@ -0,0 +1,630 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.ViewModels.Forms;
using JdeScoping.ConfigManager.ViewModels.PipelineSteps;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
public class PipelineEditorViewModelTests
{
private readonly IDialogService _dialogService;
public PipelineEditorViewModelTests()
{
_dialogService = Substitute.For<IDialogService>();
}
[Fact]
public void Constructor_InitializesPropertiesCorrectly()
{
// Arrange
var model = CreateDefaultModel();
var connections = new List<string> { "jde", "cms", "lotfinder" };
// Act
var sut = new PipelineEditorViewModel("TestPipeline", model, connections, _dialogService, () => { });
// Assert
sut.Name.ShouldBe("TestPipeline");
sut.AvailableConnections.ShouldBe(connections);
sut.PreScripts.ShouldNotBeNull();
sut.Transformers.ShouldNotBeNull();
sut.PostScripts.ShouldNotBeNull();
sut.Source.ShouldNotBeNull();
sut.Destination.ShouldNotBeNull();
}
[Fact]
public void Constructor_BuildsSourceAndDestinationFromModel()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder_Curr" }
};
var connections = new List<string> { "jde" };
// Act
var sut = new PipelineEditorViewModel("TestPipeline", model, connections, _dialogService, () => { });
// Assert
sut.Source.Connection.ShouldBe("jde");
sut.Source.Query.ShouldBe("SELECT * FROM WO");
sut.Destination.Table.ShouldBe("WorkOrder_Curr");
}
[Fact]
public void IsEnabled_Setter_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = CreateDefaultModel();
model.IsEnabled = false;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.IsEnabled = true;
// Assert
sut.IsEnabled.ShouldBeTrue();
model.IsEnabled.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void IsManualOnly_Setter_UpdatesModelAndInvokesOnChanged()
{
// Arrange
var model = CreateDefaultModel();
model.IsManualOnly = false;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.IsManualOnly = true;
// Assert
sut.IsManualOnly.ShouldBeTrue();
model.IsManualOnly.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MassSyncEnabled_ToggleOn_SetsDefaultInterval()
{
// Arrange
var model = CreateDefaultModel();
model.MassSyncIntervalMinutes = null;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.MassSyncEnabled = true;
// Assert
sut.MassSyncEnabled.ShouldBeTrue();
sut.MassSyncIntervalMinutes.ShouldBe(10080); // 1 week default
model.MassSyncIntervalMinutes.ShouldBe(10080);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MassSyncEnabled_ToggleOff_ClearsInterval()
{
// Arrange
var model = CreateDefaultModel();
model.MassSyncIntervalMinutes = 10080;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.MassSyncEnabled = false;
// Assert
sut.MassSyncEnabled.ShouldBeFalse();
model.MassSyncIntervalMinutes.ShouldBeNull();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void DailySyncEnabled_ToggleOn_SetsDefaultInterval()
{
// Arrange
var model = CreateDefaultModel();
model.DailySyncIntervalMinutes = null;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.DailySyncEnabled = true;
// Assert
sut.DailySyncEnabled.ShouldBeTrue();
sut.DailySyncIntervalMinutes.ShouldBe(1440); // 1 day default
model.DailySyncIntervalMinutes.ShouldBe(1440);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void HourlySyncEnabled_ToggleOn_SetsDefaultInterval()
{
// Arrange
var model = CreateDefaultModel();
model.HourlySyncIntervalMinutes = null;
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.HourlySyncEnabled = true;
// Assert
sut.HourlySyncEnabled.ShouldBeTrue();
sut.HourlySyncIntervalMinutes.ShouldBe(60); // 1 hour default
model.HourlySyncIntervalMinutes.ShouldBe(60);
changedInvoked.ShouldBeTrue();
}
[Fact]
public void SelectedStep_Setter_DeselectsPreviousAndSelectsNew()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
var source = sut.Source;
var destination = sut.Destination;
// Act - Select source
sut.SelectedStep = source;
// Assert
source.IsSelected.ShouldBeTrue();
sut.SelectedStep.ShouldBe(source);
// Act - Select destination (should deselect source)
sut.SelectedStep = destination;
// Assert
source.IsSelected.ShouldBeFalse();
destination.IsSelected.ShouldBeTrue();
sut.SelectedStep.ShouldBe(destination);
}
[Fact]
public void CanDeleteSelectedStep_ReturnsFalse_ForSourceStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedStep = sut.Source;
// Assert
sut.CanDeleteSelectedStep.ShouldBeFalse();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsFalse_ForDestinationStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedStep = sut.Destination;
// Assert
sut.CanDeleteSelectedStep.ShouldBeFalse();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsTrue_ForTransformerStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add a transformer
sut.AddTransformerCommand.Execute(null);
var transformer = sut.Transformers[0];
// Act
sut.SelectedStep = transformer;
// Assert
sut.CanDeleteSelectedStep.ShouldBeTrue();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsTrue_ForPreScriptStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add a pre-script
sut.AddPreScriptCommand.Execute(null);
var preScript = sut.PreScripts[0];
// Act
sut.SelectedStep = preScript;
// Assert
sut.CanDeleteSelectedStep.ShouldBeTrue();
}
[Fact]
public void CanDeleteSelectedStep_ReturnsTrue_ForPostScriptStep()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add a post-script
sut.AddPostScriptCommand.Execute(null);
var postScript = sut.PostScripts[0];
// Act
sut.SelectedStep = postScript;
// Assert
sut.CanDeleteSelectedStep.ShouldBeTrue();
}
[Fact]
public void AddPreScriptCommand_AddsPreScriptAndSelectsIt()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.AddPreScriptCommand.Execute(null);
// Assert
sut.PreScripts.Count.ShouldBe(1);
sut.SelectedStep.ShouldBe(sut.PreScripts[0]);
sut.PreScripts[0].IsSelected.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void AddTransformerCommand_AddsTransformerAndSelectsIt()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.AddTransformerCommand.Execute(null);
// Assert
sut.Transformers.Count.ShouldBe(1);
sut.SelectedStep.ShouldBe(sut.Transformers[0]);
sut.Transformers[0].IsSelected.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void AddPostScriptCommand_AddsPostScriptAndSelectsIt()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Act
sut.AddPostScriptCommand.Execute(null);
// Assert
sut.PostScripts.Count.ShouldBe(1);
sut.SelectedStep.ShouldBe(sut.PostScripts[0]);
sut.PostScripts[0].IsSelected.ShouldBeTrue();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void RemoveStepCommand_RemovesSelectedStep()
{
// Arrange
var model = CreateDefaultModel();
var changedInvoked = false;
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => changedInvoked = true);
// Add a transformer first
sut.AddTransformerCommand.Execute(null);
var transformer = sut.Transformers[0];
changedInvoked = false;
// Act
sut.RemoveStepCommand.Execute(transformer);
// Assert
sut.Transformers.Count.ShouldBe(0);
sut.SelectedStep.ShouldBeNull();
changedInvoked.ShouldBeTrue();
}
[Fact]
public void MoveStepUpCommand_MovesStepUpInCollection()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Move second up
sut.MoveStepUpCommand.Execute(secondTransformer);
// Assert
sut.Transformers[0].ShouldBe(secondTransformer);
sut.Transformers[1].ShouldBe(firstTransformer);
}
[Fact]
public void MoveStepDownCommand_MovesStepDownInCollection()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Move first down
sut.MoveStepDownCommand.Execute(firstTransformer);
// Assert
sut.Transformers[0].ShouldBe(secondTransformer);
sut.Transformers[1].ShouldBe(firstTransformer);
}
[Fact]
public void MoveStepUpCommand_DoesNotMove_WhenAtTop()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Try to move first up (should do nothing)
sut.MoveStepUpCommand.Execute(firstTransformer);
// Assert
sut.Transformers[0].ShouldBe(firstTransformer);
sut.Transformers[1].ShouldBe(secondTransformer);
}
[Fact]
public void MoveStepDownCommand_DoesNotMove_WhenAtBottom()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Add two transformers
sut.AddTransformerCommand.Execute(null);
sut.AddTransformerCommand.Execute(null);
var firstTransformer = sut.Transformers[0];
var secondTransformer = sut.Transformers[1];
// Act - Try to move second down (should do nothing)
sut.MoveStepDownCommand.Execute(secondTransformer);
// Assert
sut.Transformers[0].ShouldBe(firstTransformer);
sut.Transformers[1].ShouldBe(secondTransformer);
}
[Fact]
public void Constructor_ThrowsOnNullName()
{
// Arrange
var model = CreateDefaultModel();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel(null!, model, [], _dialogService, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullModel()
{
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel("Test", null!, [], _dialogService, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullDialogService()
{
// Arrange
var model = CreateDefaultModel();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel("Test", model, [], null!, () => { }));
}
[Fact]
public void Constructor_ThrowsOnNullOnChanged()
{
// Arrange
var model = CreateDefaultModel();
// Act & Assert
Should.Throw<ArgumentNullException>(() =>
new PipelineEditorViewModel("Test", model, [], _dialogService, null!));
}
[Fact]
public void Constructor_LoadsExistingTransformers()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
Transforms =
[
new TransformElement { TransformType = "ColumnDrop" },
new TransformElement { TransformType = "ColumnRename" }
]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.Transformers.Count.ShouldBe(2);
sut.Transformers[0].ShouldBeOfType<ColumnDropTransformerViewModel>();
sut.Transformers[1].ShouldBeOfType<ColumnRenameTransformerViewModel>();
}
[Fact]
public void Constructor_LoadsExistingPreScripts()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
PreScripts = [new ScriptElement { Connection = "lotfinder", Script = "TRUNCATE TABLE Test" }]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.PreScripts.Count.ShouldBe(1);
sut.PreScripts[0].Script.ShouldBe("TRUNCATE TABLE Test");
}
[Fact]
public void Constructor_LoadsExistingPostScripts()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
PostScripts = [new ScriptElement { Connection = "lotfinder", Script = "UPDATE Stats SET LastRun = GETDATE()" }]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.PostScripts.Count.ShouldBe(1);
sut.PostScripts[0].Script.ShouldBe("UPDATE Stats SET LastRun = GETDATE()");
}
[Fact]
public void AllSteps_ReturnsAllStepsInOrder()
{
// Arrange
var model = new EtlPipelineConfig
{
Source = new SourceElement { Connection = "jde", Query = "SELECT * FROM WO" },
Destination = new DestinationElement { Table = "WorkOrder" },
PreScripts = [new ScriptElement { Script = "pre" }],
Transforms = [new TransformElement { TransformType = "ColumnDrop" }],
PostScripts = [new ScriptElement { Script = "post" }]
};
// Act
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
var allSteps = sut.AllSteps.ToList();
// Assert
allSteps.Count.ShouldBe(5);
allSteps[0].ShouldBeOfType<PreScriptStepViewModel>();
allSteps[1].ShouldBeOfType<SourceStepViewModel>();
allSteps[2].ShouldBeOfType<ColumnDropTransformerViewModel>();
allSteps[3].ShouldBeOfType<DestinationStepViewModel>();
allSteps[4].ShouldBeOfType<PostScriptStepViewModel>();
}
[Fact]
public void SelectedStepEditor_UpdatesWhenSelectedStepChanges()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedStep = sut.Source;
// Assert
sut.SelectedStepEditor.ShouldBe(sut.Source);
}
[Fact]
public void AddTransformerOfType_AddsSpecificTransformerType()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.AddTransformerOfType("JdeDate");
// Assert
sut.Transformers.Count.ShouldBe(1);
sut.Transformers[0].ShouldBeOfType<JdeDateTransformerViewModel>();
sut.SelectedStep.ShouldBe(sut.Transformers[0]);
}
[Fact]
public void SelectedTransformerType_SetterAndGetter_Work()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Act
sut.SelectedTransformerType = "ColumnRename";
// Assert
sut.SelectedTransformerType.ShouldBe("ColumnRename");
}
[Fact]
public void AvailableTransformerTypes_ReturnsExpectedTypes()
{
// Arrange
var model = CreateDefaultModel();
var sut = new PipelineEditorViewModel("Test", model, [], _dialogService, () => { });
// Assert
sut.AvailableTransformerTypes.ShouldContain("ColumnDrop");
sut.AvailableTransformerTypes.ShouldContain("ColumnRename");
sut.AvailableTransformerTypes.ShouldContain("JdeDate");
sut.AvailableTransformerTypes.ShouldContain("Regex");
}
private static EtlPipelineConfig CreateDefaultModel()
{
return new EtlPipelineConfig
{
Source = new SourceElement { Connection = string.Empty, Query = string.Empty },
Destination = new DestinationElement { Table = string.Empty }
};
}
}
@@ -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(
@@ -0,0 +1,756 @@
using System.Text.Json;
using JdeScoping.ConfigManager.ViewModels.PipelineSteps;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Tests.ViewModels.PipelineSteps;
public class ColumnDropTransformerViewModelTests
{
[Fact]
public void Constructor_WithElement_ParsesColumnsFromConfig()
{
// Arrange
var element = CreateColumnDropElement("Col1", "Col2", "Col3");
// Act
var sut = new ColumnDropTransformerViewModel(element, () => { });
// Assert
var columns = sut.GetColumns();
columns.ShouldContain("Col1");
columns.ShouldContain("Col2");
columns.ShouldContain("Col3");
}
[Fact]
public void Constructor_WithEmptyElement_InitializesEmpty()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnDrop" };
// Act
var sut = new ColumnDropTransformerViewModel(element, () => { });
// Assert
sut.GetColumns().ShouldBeEmpty();
}
[Fact]
public void Constructor_Default_InitializesEmpty()
{
// Act
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.GetColumns().ShouldBeEmpty();
sut.ColumnsText.ShouldBe(string.Empty);
}
[Fact]
public void ColumnsText_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnDropTransformerViewModel(() => onChangedCalled = true);
// Act
sut.ColumnsText = "Column1\nColumn2";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void GetColumns_ReturnsNewlineSeparatedColumns()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "Col1\nCol2\nCol3";
// Act
var columns = sut.GetColumns();
// Assert
columns.Count.ShouldBe(3);
columns[0].ShouldBe("Col1");
columns[1].ShouldBe("Col2");
columns[2].ShouldBe("Col3");
}
[Fact]
public void GetColumns_TrimsWhitespace()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = " Col1 \n Col2 ";
// Act
var columns = sut.GetColumns();
// Assert
columns[0].ShouldBe("Col1");
columns[1].ShouldBe("Col2");
}
[Fact]
public void GetColumns_IgnoresEmptyLines()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "Col1\n\n\nCol2";
// Act
var columns = sut.GetColumns();
// Assert
columns.Count.ShouldBe(2);
}
[Fact]
public void ToModel_ReturnsCorrectTransformElement()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "DropMe\nAlsoDropMe";
// Act
var model = sut.ToModel();
// Assert
model.TransformType.ShouldBe("ColumnDrop");
model.Config.ShouldNotBeNull();
}
[Fact]
public void TransformerType_ReturnsColumnDrop()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.TransformerType.ShouldBe("ColumnDrop");
}
[Fact]
public void DisplayName_ReturnsColumnDrop()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.DisplayName.ShouldBe("Column Drop");
}
[Fact]
public void Summary_WithColumns_ShowsCount()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
sut.ColumnsText = "Col1\nCol2\nCol3";
// Assert
sut.Summary.ShouldBe("Drop 3 columns");
}
[Fact]
public void Summary_WithNoColumns_ShowsNoColumns()
{
// Arrange
var sut = new ColumnDropTransformerViewModel(() => { });
// Assert
sut.Summary.ShouldBe("No columns");
}
private static TransformElement CreateColumnDropElement(params string[] columns)
{
var config = new { columns };
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "ColumnDrop",
Config = doc.RootElement.Clone()
};
}
}
public class ColumnRenameTransformerViewModelTests
{
[Fact]
public void Constructor_WithElement_ParsesMappingsFromConfig()
{
// Arrange
var element = CreateColumnRenameElement(("OldName", "NewName"), ("Source", "Target"));
// Act
var sut = new ColumnRenameTransformerViewModel(element, () => { });
// Assert
sut.Mappings.Count.ShouldBe(2);
sut.Mappings.ShouldContain(m => m.OldName == "OldName" && m.NewName == "NewName");
sut.Mappings.ShouldContain(m => m.OldName == "Source" && m.NewName == "Target");
}
[Fact]
public void Constructor_WithEmptyElement_InitializesEmpty()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnRename" };
// Act
var sut = new ColumnRenameTransformerViewModel(element, () => { });
// Assert
sut.Mappings.ShouldBeEmpty();
}
[Fact]
public void Constructor_Default_InitializesEmpty()
{
// Act
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.Mappings.ShouldBeEmpty();
}
[Fact]
public void AddMapping_AddsNewMapping()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Act
sut.AddMapping();
// Assert
sut.Mappings.Count.ShouldBe(1);
sut.Mappings[0].OldName.ShouldBe("");
sut.Mappings[0].NewName.ShouldBe("");
}
[Fact]
public void AddMapping_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnRenameTransformerViewModel(() => onChangedCalled = true);
// Act
sut.AddMapping();
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void RemoveMapping_RemovesMapping()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
sut.AddMapping();
var mapping = sut.Mappings[0];
// Act
sut.RemoveMapping(mapping);
// Assert
sut.Mappings.ShouldBeEmpty();
}
[Fact]
public void RemoveMapping_InvokesOnChanged()
{
// Arrange
var onChangedCount = 0;
var sut = new ColumnRenameTransformerViewModel(() => onChangedCount++);
sut.AddMapping();
var mapping = sut.Mappings[0];
onChangedCount = 0;
// Act
sut.RemoveMapping(mapping);
// Assert
onChangedCount.ShouldBe(1);
}
[Fact]
public void ToModel_ReturnsCorrectTransformElement()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
sut.AddMapping();
sut.Mappings[0].OldName = "OldCol";
sut.Mappings[0].NewName = "NewCol";
// Act
var model = sut.ToModel();
// Assert
model.TransformType.ShouldBe("ColumnRename");
model.Config.ShouldNotBeNull();
}
[Fact]
public void TransformerType_ReturnsColumnRename()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.TransformerType.ShouldBe("ColumnRename");
}
[Fact]
public void DisplayName_ReturnsColumnRename()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.DisplayName.ShouldBe("Column Rename");
}
[Fact]
public void Summary_WithMappings_ShowsCount()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
sut.AddMapping();
sut.AddMapping();
// Assert
sut.Summary.ShouldBe("Rename 2 columns");
}
[Fact]
public void Summary_WithNoMappings_ShowsNoMappings()
{
// Arrange
var sut = new ColumnRenameTransformerViewModel(() => { });
// Assert
sut.Summary.ShouldBe("No mappings");
}
private static TransformElement CreateColumnRenameElement(params (string oldName, string newName)[] mappings)
{
var dict = mappings.ToDictionary(m => m.oldName, m => m.newName);
var config = new { mappings = dict };
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "ColumnRename",
Config = doc.RootElement.Clone()
};
}
}
public class ColumnMappingViewModelTests
{
[Fact]
public void Constructor_SetsProperties()
{
// Act
var sut = new ColumnMappingViewModel("OldName", "NewName", () => { });
// Assert
sut.OldName.ShouldBe("OldName");
sut.NewName.ShouldBe("NewName");
}
[Fact]
public void OldName_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnMappingViewModel("Old", "New", () => onChangedCalled = true);
// Act
sut.OldName = "Updated";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void NewName_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new ColumnMappingViewModel("Old", "New", () => onChangedCalled = true);
// Act
sut.NewName = "Updated";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void OldName_Setter_HandlesNull()
{
// Arrange
var sut = new ColumnMappingViewModel("Old", "New", () => { });
// Act
sut.OldName = null!;
// Assert
sut.OldName.ShouldBe(string.Empty);
}
[Fact]
public void NewName_Setter_HandlesNull()
{
// Arrange
var sut = new ColumnMappingViewModel("Old", "New", () => { });
// Act
sut.NewName = null!;
// Assert
sut.NewName.ShouldBe(string.Empty);
}
}
public class JdeDateTransformerViewModelTests
{
[Fact]
public void Constructor_WithElement_ParsesPropertiesFromConfig()
{
// Arrange
var element = CreateJdeDateElement("WADDJ", "WADTM", "CompletionDate");
// Act
var sut = new JdeDateTransformerViewModel(element, () => { });
// Assert
sut.DateColumn.ShouldBe("WADDJ");
sut.TimeColumn.ShouldBe("WADTM");
sut.OutputColumn.ShouldBe("CompletionDate");
}
[Fact]
public void Constructor_WithEmptyElement_InitializesNull()
{
// Arrange
var element = new TransformElement { TransformType = "JdeDate" };
// Act
var sut = new JdeDateTransformerViewModel(element, () => { });
// Assert
sut.DateColumn.ShouldBeNull();
sut.TimeColumn.ShouldBeNull();
sut.OutputColumn.ShouldBeNull();
}
[Fact]
public void Constructor_Default_InitializesNull()
{
// Act
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.DateColumn.ShouldBeNull();
sut.TimeColumn.ShouldBeNull();
sut.OutputColumn.ShouldBeNull();
}
[Fact]
public void DateColumn_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new JdeDateTransformerViewModel(() => onChangedCalled = true);
// Act
sut.DateColumn = "WADDJ";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void TimeColumn_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new JdeDateTransformerViewModel(() => onChangedCalled = true);
// Act
sut.TimeColumn = "WADTM";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void OutputColumn_Setter_InvokesOnChanged()
{
// Arrange
var onChangedCalled = false;
var sut = new JdeDateTransformerViewModel(() => onChangedCalled = true);
// Act
sut.OutputColumn = "ResultDate";
// Assert
onChangedCalled.ShouldBeTrue();
}
[Fact]
public void ToModel_ReturnsCorrectTransformElement()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
sut.DateColumn = "DateCol";
sut.TimeColumn = "TimeCol";
sut.OutputColumn = "Output";
// Act
var model = sut.ToModel();
// Assert
model.TransformType.ShouldBe("JdeDate");
model.Config.ShouldNotBeNull();
}
[Fact]
public void TransformerType_ReturnsJdeDate()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.TransformerType.ShouldBe("JdeDate");
}
[Fact]
public void DisplayName_ReturnsJdeDateConvert()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.DisplayName.ShouldBe("JDE Date Convert");
}
[Fact]
public void Summary_WithOutputColumn_ShowsOutputColumn()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
sut.OutputColumn = "CompletionDate";
// Assert
sut.Summary.ShouldContain("CompletionDate");
}
[Fact]
public void Summary_WithNoOutputColumn_ShowsConfigure()
{
// Arrange
var sut = new JdeDateTransformerViewModel(() => { });
// Assert
sut.Summary.ShouldBe("Configure...");
}
private static TransformElement CreateJdeDateElement(string? dateColumn, string? timeColumn, string? outputColumn)
{
var config = new { dateColumn, timeColumn, outputColumn };
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
using var doc = JsonDocument.Parse(json);
return new TransformElement
{
TransformType = "JdeDate",
Config = doc.RootElement.Clone()
};
}
}
public class TransformerFactoryTests
{
[Fact]
public void Create_ColumnDrop_ReturnsColumnDropViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnDrop" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void Create_ColumnRename_ReturnsColumnRenameViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "ColumnRename" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<ColumnRenameTransformerViewModel>();
}
[Fact]
public void Create_JdeDate_ReturnsJdeDateViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "JdeDate" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<JdeDateTransformerViewModel>();
}
[Fact]
public void Create_Regex_ReturnsRegexViewModel()
{
// Arrange
var element = new TransformElement { TransformType = "Regex" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<RegexTransformerViewModel>();
}
[Fact]
public void Create_CaseInsensitive_WorksCorrectly()
{
// Arrange
var element = new TransformElement { TransformType = "COLUMNDROP" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void Create_UnknownType_ReturnsNull()
{
// Arrange
var element = new TransformElement { TransformType = "UnknownType" };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void Create_NullType_ReturnsNull()
{
// Arrange
var element = new TransformElement { TransformType = null };
// Act
var result = TransformerFactory.Create(element, () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void CreateNew_ColumnDrop_ReturnsNewColumnDropViewModel()
{
// Act
var result = TransformerFactory.CreateNew("ColumnDrop", () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void CreateNew_ColumnRename_ReturnsNewColumnRenameViewModel()
{
// Act
var result = TransformerFactory.CreateNew("ColumnRename", () => { });
// Assert
result.ShouldBeOfType<ColumnRenameTransformerViewModel>();
}
[Fact]
public void CreateNew_JdeDate_ReturnsNewJdeDateViewModel()
{
// Act
var result = TransformerFactory.CreateNew("JdeDate", () => { });
// Assert
result.ShouldBeOfType<JdeDateTransformerViewModel>();
}
[Fact]
public void CreateNew_Regex_ReturnsNewRegexViewModel()
{
// Act
var result = TransformerFactory.CreateNew("Regex", () => { });
// Assert
result.ShouldBeOfType<RegexTransformerViewModel>();
}
[Fact]
public void CreateNew_CaseInsensitive_WorksCorrectly()
{
// Act
var result = TransformerFactory.CreateNew("columndrop", () => { });
// Assert
result.ShouldBeOfType<ColumnDropTransformerViewModel>();
}
[Fact]
public void CreateNew_UnknownType_ReturnsNull()
{
// Act
var result = TransformerFactory.CreateNew("UnknownType", () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void CreateNew_NullType_ReturnsNull()
{
// Act
var result = TransformerFactory.CreateNew(null!, () => { });
// Assert
result.ShouldBeNull();
}
[Fact]
public void AvailableTypes_ContainsExpectedTypes()
{
// Assert
TransformerFactory.AvailableTypes.ShouldContain("ColumnDrop");
TransformerFactory.AvailableTypes.ShouldContain("ColumnRename");
TransformerFactory.AvailableTypes.ShouldContain("JdeDate");
TransformerFactory.AvailableTypes.ShouldContain("Regex");
}
[Fact]
public void AvailableTypes_HasFourTypes()
{
// Assert
TransformerFactory.AvailableTypes.Count.ShouldBe(4);
}
}
@@ -0,0 +1,243 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.VisualTree;
using JdeScoping.ConfigManager.Views.Dialogs;
namespace JdeScoping.ConfigManager.Tests.Views.Dialogs;
/// <summary>
/// UI tests for dialog views.
/// </summary>
public class DialogViewTests
{
/// <summary>
/// Verifies that NewStoreDialog renders with the expected input fields.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_RendersInputFields()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert - NewStoreDialog contains TextBoxes for store path and key file path
var textBoxes = dialog.GetVisualDescendants().OfType<TextBox>().ToList();
textBoxes.Count.ShouldBeGreaterThanOrEqualTo(2); // Store path and Key file path
// Verify section headers
var textBlocks = dialog.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Store Location").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Key File").ShouldBeTrue();
}
/// <summary>
/// Verifies that NewStoreDialog has Browse and Generate buttons for key file.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_HasBrowseAndGenerateButtons()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
buttonContents.ShouldContain("Browse...");
buttonContents.ShouldContain("Generate");
}
/// <summary>
/// Verifies that NewStoreDialog has Create and Cancel buttons.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_HasCreateAndCancelButtons()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
buttonContents.ShouldContain("Create");
buttonContents.ShouldContain("Cancel");
}
/// <summary>
/// Verifies that NewStoreDialog has the correct title.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_HasCorrectTitle()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert
dialog.Title.ShouldBe("Create New Secure Store");
}
/// <summary>
/// Verifies that NewStoreDialog has the expected size constraints.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_HasExpectedSize()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert - Width=550, Height=350, MinWidth=450, MinHeight=300
dialog.Width.ShouldBe(550);
dialog.Height.ShouldBe(350);
dialog.MinWidth.ShouldBe(450);
dialog.MinHeight.ShouldBe(300);
}
/// <summary>
/// Verifies that UnlockStoreDialog renders with key file input field.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_RendersKeyFileField()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert - UnlockStoreDialog contains TextBoxes for store path (read-only) and key file path
var textBoxes = dialog.GetVisualDescendants().OfType<TextBox>().ToList();
textBoxes.Count.ShouldBeGreaterThanOrEqualTo(2); // Store path and Key file path
// Verify section headers
var textBlocks = dialog.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Store File").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Key File").ShouldBeTrue();
}
/// <summary>
/// Verifies that UnlockStoreDialog has a Browse button for key file.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_HasBrowseButton()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
buttonContents.ShouldContain("Browse...");
}
/// <summary>
/// Verifies that UnlockStoreDialog has Unlock and Cancel buttons.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_HasUnlockAndCancelButtons()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert
var buttons = dialog.GetVisualDescendants().OfType<Button>().ToList();
var buttonContents = buttons.Select(b => b.Content?.ToString()).ToList();
buttonContents.ShouldContain("Unlock");
buttonContents.ShouldContain("Cancel");
}
/// <summary>
/// Verifies that UnlockStoreDialog has the correct title.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_HasCorrectTitle()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert
dialog.Title.ShouldBe("Unlock Secure Store");
}
/// <summary>
/// Verifies that UnlockStoreDialog has the expected size constraints.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_HasExpectedSize()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert - Width=500, Height=320, MinWidth=400, MinHeight=280
dialog.Width.ShouldBe(500);
dialog.Height.ShouldBe(320);
dialog.MinWidth.ShouldBe(400);
dialog.MinHeight.ShouldBe(280);
}
/// <summary>
/// Verifies that NewStoreDialog is not resizable.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_IsNotResizable()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert
dialog.CanResize.ShouldBeFalse();
}
/// <summary>
/// Verifies that UnlockStoreDialog is not resizable.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_IsNotResizable()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert
dialog.CanResize.ShouldBeFalse();
}
/// <summary>
/// Verifies that NewStoreDialog contains a ScrollViewer.
/// </summary>
[AvaloniaFact]
public void NewStoreDialogView_ContainsScrollViewer()
{
// Arrange & Act
var dialog = new NewStoreDialog();
dialog.Show();
// Assert
var scrollViewer = dialog.FindDescendantOfType<ScrollViewer>();
scrollViewer.ShouldNotBeNull();
}
/// <summary>
/// Verifies that UnlockStoreDialog contains a ScrollViewer.
/// </summary>
[AvaloniaFact]
public void UnlockStoreDialogView_ContainsScrollViewer()
{
// Arrange & Act
var dialog = new UnlockStoreDialog();
dialog.Show();
// Assert
var scrollViewer = dialog.FindDescendantOfType<ScrollViewer>();
scrollViewer.ShouldNotBeNull();
}
}
@@ -0,0 +1,233 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.VisualTree;
using JdeScoping.ConfigManager.Views.Forms;
namespace JdeScoping.ConfigManager.Tests.Views.Forms;
/// <summary>
/// UI tests for form views.
/// </summary>
public class FormViewTests
{
/// <summary>
/// Creates a test window to host a UserControl and shows it.
/// UserControls must be attached to a Window to have their visual tree built.
/// </summary>
private static Window ShowInTestWindow(Control content)
{
var window = new Window { Content = content };
window.Show();
return window;
}
/// <summary>
/// Verifies that AuthFormView renders with the expected controls.
/// </summary>
[AvaloniaFact]
public void AuthFormView_RendersWithCorrectControls()
{
// Arrange
var view = new AuthFormView();
ShowInTestWindow(view);
// Assert - AuthFormView contains TextBox for CookieName and NumericUpDown for expiration
var textBoxes = view.GetVisualDescendants().OfType<TextBox>().ToList();
textBoxes.Count.ShouldBeGreaterThan(0);
var numericUpDowns = view.GetVisualDescendants().OfType<NumericUpDown>().ToList();
numericUpDowns.Count.ShouldBeGreaterThan(0);
// Verify header is present
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Authentication Settings").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Cookie Settings").ShouldBeTrue();
}
/// <summary>
/// Verifies that DataSyncFormView renders with the expected controls.
/// </summary>
[AvaloniaFact]
public void DataSyncFormView_RendersWithCorrectControls()
{
// Arrange
var view = new DataSyncFormView();
ShowInTestWindow(view);
// Assert - DataSyncFormView contains CheckBox for Enabled and multiple NumericUpDowns
var checkBoxes = view.GetVisualDescendants().OfType<CheckBox>().ToList();
checkBoxes.Count.ShouldBeGreaterThan(0);
var numericUpDowns = view.GetVisualDescendants().OfType<NumericUpDown>().ToList();
numericUpDowns.Count.ShouldBeGreaterThanOrEqualTo(6); // Check Interval, Timeout, Max Parallelism, Batch Size, Bulk Copy Batch Size, Lookback Multiplier, Purge Retention
// Verify section headers
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Data Sync Settings").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Sync Intervals").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Performance").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Data Retention").ShouldBeTrue();
}
/// <summary>
/// Verifies that ConnectionStringsFormView renders with a DataGrid.
/// </summary>
[AvaloniaFact]
public void ConnectionStringsFormView_RendersDataGrid()
{
// Arrange
var view = new ConnectionStringsFormView();
ShowInTestWindow(view);
// Assert - ConnectionStringsFormView contains a DataGrid for connections
var dataGrid = view.FindDescendantOfType<DataGrid>();
dataGrid.ShouldNotBeNull();
// Verify header is present
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Connection Strings").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Connections").ShouldBeTrue();
}
/// <summary>
/// Verifies that ConnectionStringsFormView has Add and Delete buttons.
/// </summary>
[AvaloniaFact]
public void ConnectionStringsFormView_HasAddAndDeleteButtons()
{
// Arrange
var view = new ConnectionStringsFormView();
ShowInTestWindow(view);
// Assert
var buttons = view.GetVisualDescendants().OfType<Button>().ToList();
// Find buttons containing TextBlock with Add/Delete text
var buttonTexts = buttons
.SelectMany(b => b.GetVisualDescendants().OfType<TextBlock>())
.Select(tb => tb.Text)
.ToList();
buttonTexts.ShouldContain("Add");
buttonTexts.ShouldContain("Delete");
}
/// <summary>
/// Verifies that LdapFormView renders with server URL list input.
/// </summary>
[AvaloniaFact]
public void LdapFormView_RendersServerList()
{
// Arrange
var view = new LdapFormView();
ShowInTestWindow(view);
// Assert - LdapFormView contains TextBoxes for server URLs, Group DN, Search Base
var textBoxes = view.GetVisualDescendants().OfType<TextBox>().ToList();
textBoxes.Count.ShouldBeGreaterThanOrEqualTo(3); // Server URLs, Group DN, Search Base, Admin Bypass Users
// Verify section headers
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "LDAP Settings").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Server Configuration").ShouldBeTrue();
textBlocks.Any(tb => tb.Text == "Directory Structure").ShouldBeTrue();
}
/// <summary>
/// Verifies that LdapFormView contains a CheckBox for fake authentication.
/// </summary>
[AvaloniaFact]
public void LdapFormView_HasFakeAuthCheckBox()
{
// Arrange
var view = new LdapFormView();
ShowInTestWindow(view);
// Assert
var checkBoxes = view.GetVisualDescendants().OfType<CheckBox>().ToList();
checkBoxes.Count.ShouldBeGreaterThan(0);
// Verify development options section
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Development Options").ShouldBeTrue();
}
/// <summary>
/// Verifies that LdapFormView contains a NumericUpDown for connection timeout.
/// </summary>
[AvaloniaFact]
public void LdapFormView_HasConnectionTimeoutNumericUpDown()
{
// Arrange
var view = new LdapFormView();
ShowInTestWindow(view);
// Assert
var numericUpDowns = view.GetVisualDescendants().OfType<NumericUpDown>().ToList();
numericUpDowns.Count.ShouldBeGreaterThan(0);
// Verify timeout label is present
var textBlocks = view.GetVisualDescendants().OfType<TextBlock>().ToList();
textBlocks.Any(tb => tb.Text == "Connection Timeout (seconds)").ShouldBeTrue();
}
/// <summary>
/// Verifies that AuthFormView contains a ScrollViewer.
/// </summary>
[AvaloniaFact]
public void AuthFormView_ContainsScrollViewer()
{
// Arrange
var view = new AuthFormView();
ShowInTestWindow(view);
// Assert
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
scrollViewer.ShouldNotBeNull();
}
/// <summary>
/// Verifies that DataSyncFormView contains a ScrollViewer.
/// </summary>
[AvaloniaFact]
public void DataSyncFormView_ContainsScrollViewer()
{
// Arrange
var view = new DataSyncFormView();
ShowInTestWindow(view);
// Assert
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
scrollViewer.ShouldNotBeNull();
}
/// <summary>
/// Verifies that ConnectionStringsFormView contains a ScrollViewer.
/// </summary>
[AvaloniaFact]
public void ConnectionStringsFormView_ContainsScrollViewer()
{
// Arrange
var view = new ConnectionStringsFormView();
ShowInTestWindow(view);
// Assert
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
scrollViewer.ShouldNotBeNull();
}
/// <summary>
/// Verifies that LdapFormView contains a ScrollViewer.
/// </summary>
[AvaloniaFact]
public void LdapFormView_ContainsScrollViewer()
{
// Arrange
var view = new LdapFormView();
ShowInTestWindow(view);
// Assert
var scrollViewer = view.FindDescendantOfType<ScrollViewer>();
scrollViewer.ShouldNotBeNull();
}
}
@@ -0,0 +1,161 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.VisualTree;
using JdeScoping.ConfigManager.Views;
namespace JdeScoping.ConfigManager.Tests.Views;
/// <summary>
/// UI tests for <see cref="MainWindow"/>.
/// </summary>
public class MainWindowTests
{
/// <summary>
/// Verifies that the main window displays with the correct title.
/// </summary>
[AvaloniaFact]
public void MainWindow_ShowsWithCorrectTitle()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
window.Title.ShouldBe("JdeScoping ConfigManager");
}
/// <summary>
/// Verifies that the main window contains a TreeView for navigation.
/// </summary>
[AvaloniaFact]
public void MainWindow_ContainsTreeView()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
var treeView = window.FindDescendantOfType<TreeView>();
treeView.ShouldNotBeNull();
}
/// <summary>
/// Verifies that the main window contains a menu bar.
/// </summary>
[AvaloniaFact]
public void MainWindow_ContainsMenuBar()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
var menu = window.FindDescendantOfType<Menu>();
menu.ShouldNotBeNull();
}
/// <summary>
/// Verifies that the main window has the expected minimum size constraints.
/// </summary>
[AvaloniaFact]
public void MainWindow_HasExpectedMinimumSize()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert - MinWidth=900, MinHeight=600 as defined in AXAML
window.MinWidth.ShouldBe(900);
window.MinHeight.ShouldBe(600);
}
/// <summary>
/// Verifies that the main window has the expected default size.
/// </summary>
[AvaloniaFact]
public void MainWindow_HasExpectedDefaultSize()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert - Width=1200, Height=800 as defined in AXAML
window.Width.ShouldBe(1200);
window.Height.ShouldBe(800);
}
/// <summary>
/// Verifies that the main window contains toolbar buttons.
/// </summary>
[AvaloniaFact]
public void MainWindow_ContainsToolbarButtons()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
var buttons = window.GetVisualDescendants().OfType<Button>().ToList();
buttons.Count.ShouldBeGreaterThan(0);
// Verify toolbar buttons exist with expected content
buttons.Any(b => b.Content?.ToString() == "Open").ShouldBeTrue();
buttons.Any(b => b.Content?.ToString() == "Save").ShouldBeTrue();
buttons.Any(b => b.Content?.ToString() == "Undo").ShouldBeTrue();
buttons.Any(b => b.Content?.ToString() == "Redo").ShouldBeTrue();
buttons.Any(b => b.Content?.ToString() == "Validate").ShouldBeTrue();
}
/// <summary>
/// Verifies that the main window contains a ContentControl for form display.
/// </summary>
[AvaloniaFact]
public void MainWindow_ContainsFormContentControl()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
var contentControl = window.FindDescendantOfType<ContentControl>();
contentControl.ShouldNotBeNull();
}
/// <summary>
/// Verifies that the main window contains a GridSplitter for resizing panels.
/// </summary>
[AvaloniaFact]
public void MainWindow_ContainsGridSplitter()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
var splitter = window.FindDescendantOfType<GridSplitter>();
splitter.ShouldNotBeNull();
}
/// <summary>
/// Verifies that the menu contains expected menu items.
/// </summary>
[AvaloniaFact]
public void MainWindow_ContainsExpectedMenuItems()
{
// Arrange & Act
var window = new MainWindow();
window.Show();
// Assert
var menuItems = window.GetVisualDescendants().OfType<MenuItem>().ToList();
menuItems.Count.ShouldBeGreaterThan(0);
// Verify top-level menu items exist
menuItems.Any(m => m.Header?.ToString() == "_File").ShouldBeTrue();
menuItems.Any(m => m.Header?.ToString() == "_Edit").ShouldBeTrue();
menuItems.Any(m => m.Header?.ToString() == "_Tools").ShouldBeTrue();
menuItems.Any(m => m.Header?.ToString() == "_Pipelines").ShouldBeTrue();
menuItems.Any(m => m.Header?.ToString() == "_Secure Stores").ShouldBeTrue();
menuItems.Any(m => m.Header?.ToString() == "_Help").ShouldBeTrue();
}
}