1fc7792cd1
Rename ConfigManager to ConfigManager.Ui to match the Core/CLI/UI project structure, and split the monolithic test project into Core.Tests, Cli.Tests, and Ui.Tests to align with the source project organization.
273 lines
9.2 KiB
C#
273 lines
9.2 KiB
C#
using JdeScoping.ConfigManager.Core.Models;
|
|
using JdeScoping.ConfigManager.Core.Services;
|
|
using JdeScoping.DataSync.Configuration;
|
|
|
|
namespace JdeScoping.ConfigManager.Core.Tests.Services;
|
|
|
|
public class ConfigFileServiceTests
|
|
{
|
|
private readonly IFileSystem _fileSystem;
|
|
private readonly ConfigFileService _sut;
|
|
|
|
public ConfigFileServiceTests()
|
|
{
|
|
_fileSystem = Substitute.For<IFileSystem>();
|
|
_sut = new ConfigFileService(_fileSystem);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadAppSettingsAsync_WithValidJson_ReturnsConfigModel()
|
|
{
|
|
// Arrange
|
|
var json = """
|
|
{
|
|
"DataSync": {
|
|
"Enabled": true,
|
|
"MaxDegreeOfParallelism": 8
|
|
}
|
|
}
|
|
""";
|
|
_fileSystem.ReadAllTextAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult(json));
|
|
|
|
// Act
|
|
var result = await _sut.LoadAppSettingsAsync("/config/appsettings.json");
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.DataSync.Enabled.ShouldBeTrue();
|
|
result.DataSync.MaxDegreeOfParallelism.ShouldBe(8);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadAppSettingsAsync_WithInvalidJson_ThrowsWithHelpfulMessage()
|
|
{
|
|
// 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.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");
|
|
}
|
|
}
|