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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user