feat(configmanager): add ConfigFileService with tests
Add config file loading and saving service following TDD approach: - IConfigFileService interface for loading/saving config files - ConfigLoadException for descriptive error handling - ConfigFileService implementation with JSON serialization - Unit tests with mocked IFileSystem dependency
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for loading and saving configuration files.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigFileService : IConfigFileService
|
||||||
|
{
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILogger<ConfigFileService>? _logger;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public ConfigFileService(IFileSystem fileSystem, ILogger<ConfigFileService>? logger = null)
|
||||||
|
{
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("Loading appsettings from {Path}", path);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _fileSystem.ReadAllTextAsync(path, ct);
|
||||||
|
var config = JsonSerializer.Deserialize<ConfigModel>(json, JsonOptions);
|
||||||
|
return config ?? new ConfigModel();
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new ConfigLoadException(path, $"Failed to parse appsettings.json: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("Loading pipelines from {Path}", path);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _fileSystem.ReadAllTextAsync(path, ct);
|
||||||
|
var config = JsonSerializer.Deserialize<PipelinesConfigModel>(json, JsonOptions);
|
||||||
|
return config ?? new PipelinesConfigModel();
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new ConfigLoadException(path, $"Failed to parse pipelines.json: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("Saving appsettings to {Path}", path);
|
||||||
|
var json = JsonSerializer.Serialize(config, JsonOptions);
|
||||||
|
await _fileSystem.WriteAllTextAsync(path, json, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_logger?.LogInformation("Saving pipelines to {Path}", path);
|
||||||
|
var json = JsonSerializer.Serialize(config, JsonOptions);
|
||||||
|
await _fileSystem.WriteAllTextAsync(path, json, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace JdeScoping.ConfigManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when configuration file loading fails.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigLoadException : Exception
|
||||||
|
{
|
||||||
|
public string FilePath { get; }
|
||||||
|
|
||||||
|
public ConfigLoadException(string filePath, string message, Exception? inner = null)
|
||||||
|
: base(message, inner)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for loading and saving configuration files.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigFileService
|
||||||
|
{
|
||||||
|
Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default);
|
||||||
|
Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default);
|
||||||
|
Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default);
|
||||||
|
Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using JdeScoping.ConfigManager.Models;
|
||||||
|
using JdeScoping.ConfigManager.Services;
|
||||||
|
|
||||||
|
namespace JdeScoping.ConfigManager.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user