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