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