Files
Joseph Doherty 1fc7792cd1 refactor(configmanager): rename UI project and split test projects
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.
2026-01-28 10:24:36 -05:00

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");
}
}