feat(configmanager): add ValidationService with tests

This commit is contained in:
Joseph Doherty
2026-01-19 17:42:14 -05:00
parent 0e441898a6
commit c8f3c0060d
3 changed files with 224 additions and 0 deletions
@@ -0,0 +1,25 @@
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Result of a validation operation.
/// </summary>
public class ValidationResult
{
public bool IsValid => Errors.Count == 0;
public List<string> Errors { get; } = [];
public List<string> Warnings { get; } = [];
public void AddError(string message) => Errors.Add(message);
public void AddWarning(string message) => Warnings.Add(message);
}
/// <summary>
/// Service for validating configuration files.
/// </summary>
public interface IValidationService
{
ValidationResult ValidateAppSettings(ConfigModel config);
ValidationResult ValidatePipelines(PipelinesConfigModel config);
}
@@ -0,0 +1,109 @@
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Service for validating configuration files.
/// </summary>
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");
}
}
}
@@ -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<string, PipelineModel>
{
[""] = 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<string, PipelineModel>
{
["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"));
}
}