diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/BackupServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/BackupServiceTests.cs index cd76e50..52a7cfe 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/BackupServiceTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/BackupServiceTests.cs @@ -63,4 +63,175 @@ public class BackupServiceTests // Assert - should delete 5 oldest backups await _fileSystem.Received(5).DeleteFileAsync(Arg.Any(), Arg.Any()); } + + [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()) + .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()) + .Returns(Task.FromResult(Array.Empty())); + + // 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()); + } + + [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()) + .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(), Arg.Any()); + } + + [Fact] + public async Task CreateBackupAsync_WithNonExistentFile_ThrowsFileNotFoundException() + { + // Arrange + var sourcePath = "/config/nonexistent.json"; + _fileSystem.FileExists(sourcePath).Returns(false); + + // Act & Assert + await Should.ThrowAsync( + () => _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()) + .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()) + .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(), Arg.Any()); + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs index 351de8f..ef255d4 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConfigFileServiceTests.cs @@ -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(), Arg.Any()) + .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(), Arg.Any()) + .Returns(Task.FromResult(json)); + + // Act & Assert + var ex = await Should.ThrowAsync( + () => _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(), Arg.Do(c => writtenContent = c), Arg.Any()) + .Returns(Task.CompletedTask); + + // Act + await _sut.SavePipelineAsync(path, pipeline); + + // Assert + await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any(), Arg.Any()); + 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()) + .Returns(Task.FromResult(files)); + + _fileSystem.ReadAllTextAsync(files[0], Arg.Any()) + .Returns(Task.FromResult("""{"name": "First", "isEnabled": true}""")); + _fileSystem.ReadAllTextAsync(files[1], Arg.Any()) + .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()); + } + + [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(), Arg.Do(c => writtenContent = c), Arg.Any()) + .Returns(Task.CompletedTask); + + // Act + await _sut.SaveAppSettingsAsync(path, config); + + // Assert + await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any(), Arg.Any()); + 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()) + .Returns(Task.FromResult(files)); + + _fileSystem.ReadAllTextAsync(files[0], Arg.Any()) + .Returns(Task.FromResult("""{"name": "Valid", "isEnabled": true}""")); + _fileSystem.ReadAllTextAsync(files[1], Arg.Any()) + .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()) + .Returns(Task.FromResult(files)); + + // Pipeline with empty name + _fileSystem.ReadAllTextAsync(files[0], Arg.Any()) + .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"); + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConnectionTestServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConnectionTestServiceTests.cs new file mode 100644 index 0000000..4296e2c --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ConnectionTestServiceTests.cs @@ -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(); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/RuntimeConfigValidationServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/RuntimeConfigValidationServiceTests.cs new file mode 100644 index 0000000..cbb68f4 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/RuntimeConfigValidationServiceTests.cs @@ -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(); + _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(() => _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(); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/TestAppBuilder.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/TestAppBuilder.cs new file mode 100644 index 0000000..a757742 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/TestAppBuilder.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Headless; +using JdeScoping.ConfigManager; + +[assembly: AvaloniaTestApplication(typeof(JdeScoping.ConfigManager.Tests.TestAppBuilder))] + +namespace JdeScoping.ConfigManager.Tests; + +/// +/// Provides a headless Avalonia application builder for UI tests. +/// +public class TestAppBuilder +{ + /// + /// Builds the Avalonia application configured for headless testing. + /// + /// An configured with headless platform options. + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()); +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Dialogs/DiffPreviewDialogViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Dialogs/DiffPreviewDialogViewModelTests.cs new file mode 100644 index 0000000..e462ab9 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Dialogs/DiffPreviewDialogViewModelTests.cs @@ -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(() => 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(""); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Dialogs/ValidationResultsDialogViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Dialogs/ValidationResultsDialogViewModelTests.cs new file mode 100644 index 0000000..91809a6 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Dialogs/ValidationResultsDialogViewModelTests.cs @@ -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(() => + new ValidationResultsDialogViewModel(null!, pipelinesResult)); + } + + [Fact] + public void Constructor_ThrowsOnNullPipelinesResult() + { + // Arrange + var appSettingsResult = new ValidationResult(); + + // Act & Assert + Should.Throw(() => + 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"); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs index f5b34cf..c8edfde 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/ConnectionStringsFormViewModelTests.cs @@ -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 - { - 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 - { - 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 + { + 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 + { + 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(() => @@ -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 + { + 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 + { + new ConnectionStringEntry { Name = "ToDelete" } + } + }; + _dialogService.ShowConfirmationAsync(Arg.Any(), Arg.Any()) + .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 + { + new ConnectionStringEntry { Name = "ToKeep" } + } + }; + _dialogService.ShowConfirmationAsync(Arg.Any(), Arg.Any()) + .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 + { + 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(s => s.Contains("empty connection string"))); + } + + [Fact] + public async Task ValidateConnectionCommand_WithValidConnectionString_ShowsSuccess() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + 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(s => s.Contains("valid connection string"))); + } + + [Fact] + public async Task TestConnectionCommand_WhenSuccessful_ShowsSuccessMessage() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry + { + Name = "TestConn", + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb" + } + } + }; + _connectionTestService.TestConnectionAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .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(s => s.Contains("Successfully connected"))); + } + + [Fact] + public async Task TestConnectionCommand_WhenFailed_ShowsErrorMessage() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry + { + Name = "FailConn", + Provider = ConnectionProvider.SqlServer, + Server = "badserver", + Database = "TestDb" + } + } + }; + _connectionTestService.TestConnectionAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .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(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 + { + new ConnectionStringEntry + { + Name = "TestConn", + Provider = ConnectionProvider.SqlServer, + Server = "localhost", + Database = "TestDb" + } + } + }; + + var taskCompletionSource = new TaskCompletionSource(); + _connectionTestService.TestConnectionAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .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 + { + 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 + { + 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()); + } + + [Fact] + public async Task DeleteConnectionCommand_RemovesFromSecureStore() + { + // Arrange + var model = new ConnectionStringsSection + { + Entries = new List + { + new ConnectionStringEntry { Name = "ToDelete" } + } + }; + _secureStoreManager.IsStoreOpen.Returns(true); + _dialogService.ShowConfirmationAsync(Arg.Any(), Arg.Any()) + .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 + { + 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(s => s.Contains("connection string is empty"))); + await _connectionTestService.DidNotReceive().TestConnectionAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Fact] + public void AvailableProviders_ContainsAllProviders() + { + // Assert + ConnectionStringsFormViewModel.AvailableProviders.Count.ShouldBe( + Enum.GetValues().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"); + } } diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/PipelineEditorViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/PipelineEditorViewModelTests.cs new file mode 100644 index 0000000..f7e777b --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/Forms/PipelineEditorViewModelTests.cs @@ -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(); + } + + [Fact] + public void Constructor_InitializesPropertiesCorrectly() + { + // Arrange + var model = CreateDefaultModel(); + var connections = new List { "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 { "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(() => + new PipelineEditorViewModel(null!, model, [], _dialogService, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullModel() + { + // Act & Assert + Should.Throw(() => + new PipelineEditorViewModel("Test", null!, [], _dialogService, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullDialogService() + { + // Arrange + var model = CreateDefaultModel(); + + // Act & Assert + Should.Throw(() => + new PipelineEditorViewModel("Test", model, [], null!, () => { })); + } + + [Fact] + public void Constructor_ThrowsOnNullOnChanged() + { + // Arrange + var model = CreateDefaultModel(); + + // Act & Assert + Should.Throw(() => + 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(); + sut.Transformers[1].ShouldBeOfType(); + } + + [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(); + allSteps[1].ShouldBeOfType(); + allSteps[2].ShouldBeOfType(); + allSteps[3].ShouldBeOfType(); + allSteps[4].ShouldBeOfType(); + } + + [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(); + 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 } + }; + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs index f2516ea..8b728f0 100644 --- a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs @@ -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(), Arg.Any(), Arg.Any()); + } + + [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(s => s == "Add Secret"), + Arg.Any()); + } + + [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(s => s == "Add Secret"), + Arg.Any()); + } + + [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 { "TestSecret" }); + _dialogService.ShowConfirmationAsync(Arg.Any(), Arg.Any()) + .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 { "TestSecret" }); + _dialogService.ShowConfirmationAsync(Arg.Any(), Arg.Any()) + .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()); + } + + [Fact] + public async Task AddPipelineCommand_ShowsDialog_AndAddsPipeline() + { + // Arrange + var config = new ConfigModel(); + var pipelines = new Dictionary(); + + _autoDiscoveryService.FindConfigFolderAsync().Returns((string?)null); + _dialogService.ShowInputDialogAsync(Arg.Any(), Arg.Any()) + .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 + { + ["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(), Arg.Any()) + .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 + { + ["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(), Arg.Any()) + .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 + { + ["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(), Arg.Any()) + .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()); + 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()) + .Returns(new List()); + + 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(), + Arg.Any()); + } + + [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()) + .Returns(new List { 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(r => r.Errors.Count > 0), + Arg.Any()); + } + private MainWindowViewModel CreateViewModel() { return new MainWindowViewModel( diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/PipelineSteps/TransformerViewModelsTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/PipelineSteps/TransformerViewModelsTests.cs new file mode 100644 index 0000000..5e0ecaa --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/PipelineSteps/TransformerViewModelsTests.cs @@ -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(); + } + + [Fact] + public void Create_ColumnRename_ReturnsColumnRenameViewModel() + { + // Arrange + var element = new TransformElement { TransformType = "ColumnRename" }; + + // Act + var result = TransformerFactory.Create(element, () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [Fact] + public void Create_JdeDate_ReturnsJdeDateViewModel() + { + // Arrange + var element = new TransformElement { TransformType = "JdeDate" }; + + // Act + var result = TransformerFactory.Create(element, () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [Fact] + public void Create_Regex_ReturnsRegexViewModel() + { + // Arrange + var element = new TransformElement { TransformType = "Regex" }; + + // Act + var result = TransformerFactory.Create(element, () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [Fact] + public void Create_CaseInsensitive_WorksCorrectly() + { + // Arrange + var element = new TransformElement { TransformType = "COLUMNDROP" }; + + // Act + var result = TransformerFactory.Create(element, () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [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(); + } + + [Fact] + public void CreateNew_ColumnRename_ReturnsNewColumnRenameViewModel() + { + // Act + var result = TransformerFactory.CreateNew("ColumnRename", () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [Fact] + public void CreateNew_JdeDate_ReturnsNewJdeDateViewModel() + { + // Act + var result = TransformerFactory.CreateNew("JdeDate", () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [Fact] + public void CreateNew_Regex_ReturnsNewRegexViewModel() + { + // Act + var result = TransformerFactory.CreateNew("Regex", () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [Fact] + public void CreateNew_CaseInsensitive_WorksCorrectly() + { + // Act + var result = TransformerFactory.CreateNew("columndrop", () => { }); + + // Assert + result.ShouldBeOfType(); + } + + [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); + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Views/Dialogs/DialogViewTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Views/Dialogs/DialogViewTests.cs new file mode 100644 index 0000000..084f410 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Views/Dialogs/DialogViewTests.cs @@ -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; + +/// +/// UI tests for dialog views. +/// +public class DialogViewTests +{ + /// + /// Verifies that NewStoreDialog renders with the expected input fields. + /// + [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().ToList(); + textBoxes.Count.ShouldBeGreaterThanOrEqualTo(2); // Store path and Key file path + + // Verify section headers + var textBlocks = dialog.GetVisualDescendants().OfType().ToList(); + textBlocks.Any(tb => tb.Text == "Store Location").ShouldBeTrue(); + textBlocks.Any(tb => tb.Text == "Key File").ShouldBeTrue(); + } + + /// + /// Verifies that NewStoreDialog has Browse and Generate buttons for key file. + /// + [AvaloniaFact] + public void NewStoreDialogView_HasBrowseAndGenerateButtons() + { + // Arrange & Act + var dialog = new NewStoreDialog(); + dialog.Show(); + + // Assert + var buttons = dialog.GetVisualDescendants().OfType