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(); _sut = new ConfigFileService(_fileSystem); } [Fact] public async Task LoadAppSettingsAsync_WithValidJson_ReturnsConfigModel() { // Arrange var json = """ { "DataSync": { "Enabled": true, "MaxDegreeOfParallelism": 8 } } """; _fileSystem.ReadAllTextAsync(Arg.Any(), Arg.Any()) .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(), Arg.Any()) .Returns(Task.FromResult(json)); // Act & Assert var ex = await Should.ThrowAsync( () => _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(), Arg.Any()) .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(), Arg.Any()) .Returns(Task.FromResult(json)); // Act & Assert var ex = await Should.ThrowAsync( () => _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(), Arg.Do(c => writtenContent = c), Arg.Any()) .Returns(Task.CompletedTask); // Act await _sut.SavePipelineAsync(path, pipeline); // Assert await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any(), Arg.Any()); 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()) .Returns(Task.FromResult(files)); _fileSystem.ReadAllTextAsync(files[0], Arg.Any()) .Returns(Task.FromResult("""{"name": "First", "isEnabled": true}""")); _fileSystem.ReadAllTextAsync(files[1], Arg.Any()) .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()); } [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(), Arg.Do(c => writtenContent = c), Arg.Any()) .Returns(Task.CompletedTask); // Act await _sut.SaveAppSettingsAsync(path, config); // Assert await _fileSystem.Received(1).WriteAllTextAsync(path, Arg.Any(), Arg.Any()); 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()) .Returns(Task.FromResult(files)); _fileSystem.ReadAllTextAsync(files[0], Arg.Any()) .Returns(Task.FromResult("""{"name": "Valid", "isEnabled": true}""")); _fileSystem.ReadAllTextAsync(files[1], Arg.Any()) .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()) .Returns(Task.FromResult(files)); // Pipeline with empty name _fileSystem.ReadAllTextAsync(files[0], Arg.Any()) .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"); } }