diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs new file mode 100644 index 0000000..1cb4499 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs @@ -0,0 +1,25 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Result of a validation operation. +/// +public class ValidationResult +{ + public bool IsValid => Errors.Count == 0; + public List Errors { get; } = []; + public List Warnings { get; } = []; + + public void AddError(string message) => Errors.Add(message); + public void AddWarning(string message) => Warnings.Add(message); +} + +/// +/// Service for validating configuration files. +/// +public interface IValidationService +{ + ValidationResult ValidateAppSettings(ConfigModel config); + ValidationResult ValidatePipelines(PipelinesConfigModel config); +} diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs new file mode 100644 index 0000000..753c6e9 --- /dev/null +++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs @@ -0,0 +1,109 @@ +using JdeScoping.ConfigManager.Models; + +namespace JdeScoping.ConfigManager.Services; + +/// +/// Service for validating configuration files. +/// +public class ValidationService : IValidationService +{ + private static readonly string[] ValidConnections = ["jde", "cms", "giw", "lotfinderdb"]; + + public ValidationResult ValidateAppSettings(ConfigModel config) + { + var result = new ValidationResult(); + + // DataSync validation + if (config.DataSync.MaxDegreeOfParallelism < 1 || config.DataSync.MaxDegreeOfParallelism > 32) + result.AddError("DataSync.MaxDegreeOfParallelism must be between 1 and 32"); + + if (config.DataSync.BatchSize < 1000 || config.DataSync.BatchSize > 10_000_000) + result.AddError("DataSync.BatchSize must be between 1,000 and 10,000,000"); + + if (config.DataSync.BulkCopyBatchSize < 100 || config.DataSync.BulkCopyBatchSize > 100_000) + result.AddError("DataSync.BulkCopyBatchSize must be between 100 and 100,000"); + + if (config.DataSync.LookbackMultiplier < 1 || config.DataSync.LookbackMultiplier > 10) + result.AddError("DataSync.LookbackMultiplier must be between 1 and 10"); + + if (config.DataSync.PurgeRetentionDays < 1 || config.DataSync.PurgeRetentionDays > 365) + result.AddError("DataSync.PurgeRetentionDays must be between 1 and 365"); + + if (config.DataSync.SyncTimeoutSeconds < 60 || config.DataSync.SyncTimeoutSeconds > 86400) + result.AddError("DataSync.SyncTimeoutSeconds must be between 60 and 86,400"); + + // DataAccess validation + if (config.DataAccess.DefaultTimeoutSeconds < 1) + result.AddError("DataAccess.DefaultTimeoutSeconds must be at least 1"); + + // Ldap validation + if (config.Ldap.ConnectionTimeoutSeconds < 1 || config.Ldap.ConnectionTimeoutSeconds > 300) + result.AddError("Ldap.ConnectionTimeoutSeconds must be between 1 and 300"); + + // Search validation + if (config.Search.MaxResultRows < 1) + result.AddError("Search.MaxResultRows must be at least 1"); + + if (config.Search.MaxConcurrentSearches < 1) + result.AddError("Search.MaxConcurrentSearches must be at least 1"); + + return result; + } + + public ValidationResult ValidatePipelines(PipelinesConfigModel config) + { + var result = new ValidationResult(); + + foreach (var (name, pipeline) in config.Pipelines) + { + if (string.IsNullOrWhiteSpace(name)) + { + result.AddError("Pipeline name cannot be empty"); + continue; + } + + // Source validation + if (string.IsNullOrWhiteSpace(pipeline.Source.Connection)) + { + result.AddError($"Pipeline '{name}': Source.Connection is required"); + } + else if (!ValidConnections.Contains(pipeline.Source.Connection.ToLowerInvariant())) + { + result.AddError($"Pipeline '{name}': Source.Connection '{pipeline.Source.Connection}' is not valid. Must be one of: {string.Join(", ", ValidConnections)}"); + } + + if (string.IsNullOrWhiteSpace(pipeline.Source.Query)) + { + result.AddError($"Pipeline '{name}': Source.Query is required"); + } + + // Destination validation + if (string.IsNullOrWhiteSpace(pipeline.Destination.Table)) + { + result.AddError($"Pipeline '{name}': Destination.Table is required"); + } + + if (pipeline.Destination.MatchColumns.Length == 0) + { + result.AddWarning($"Pipeline '{name}': No MatchColumns specified - all rows will be inserted"); + } + + // Schedule validation + ValidateSchedule(result, name, "Mass", pipeline.Schedules.Mass, 60); + ValidateSchedule(result, name, "Daily", pipeline.Schedules.Daily, 60); + ValidateSchedule(result, name, "Hourly", pipeline.Schedules.Hourly, 15); + } + + return result; + } + + private void ValidateSchedule(ValidationResult result, string pipelineName, string scheduleName, ScheduleModel? schedule, int minInterval) + { + if (schedule == null) return; + + if (schedule.Enabled && schedule.IntervalMinutes < minInterval) + { + result.AddError($"Pipeline '{pipelineName}': {scheduleName} schedule interval must be at least {minInterval} minutes"); + } + } +} diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs new file mode 100644 index 0000000..0fddef1 --- /dev/null +++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs @@ -0,0 +1,90 @@ +using JdeScoping.ConfigManager.Models; +using JdeScoping.ConfigManager.Services; + +namespace JdeScoping.ConfigManager.Tests.Services; + +public class ValidationServiceTests +{ + private readonly ValidationService _sut; + + public ValidationServiceTests() + { + _sut = new ValidationService(); + } + + [Fact] + public void ValidateAppSettings_WithValidConfig_ReturnsNoErrors() + { + // Arrange + var config = new ConfigModel + { + DataSync = new DataSyncSection { MaxDegreeOfParallelism = 4 } + }; + + // Act + var result = _sut.ValidateAppSettings(config); + + // Assert + result.IsValid.ShouldBeTrue(); + result.Errors.ShouldBeEmpty(); + } + + [Fact] + public void ValidateAppSettings_WithInvalidParallelism_ReturnsError() + { + // Arrange + var config = new ConfigModel + { + DataSync = new DataSyncSection { MaxDegreeOfParallelism = 0 } + }; + + // Act + var result = _sut.ValidateAppSettings(config); + + // Assert + result.IsValid.ShouldBeFalse(); + result.Errors.ShouldContain(e => e.Contains("MaxDegreeOfParallelism")); + } + + [Fact] + public void ValidatePipelines_WithDuplicateNames_ReturnsError() + { + // Arrange - duplicate keys not possible in dictionary, but empty names are invalid + var config = new PipelinesConfigModel + { + Pipelines = new Dictionary + { + [""] = new PipelineModel() + } + }; + + // Act + var result = _sut.ValidatePipelines(config); + + // Assert + result.IsValid.ShouldBeFalse(); + } + + [Fact] + public void ValidatePipelines_WithInvalidConnection_ReturnsError() + { + // Arrange + var config = new PipelinesConfigModel + { + Pipelines = new Dictionary + { + ["Test"] = new PipelineModel + { + Source = new PipelineSource { Connection = "invalid" } + } + } + }; + + // Act + var result = _sut.ValidatePipelines(config); + + // Assert + result.IsValid.ShouldBeFalse(); + result.Errors.ShouldContain(e => e.Contains("Connection")); + } +}