feat(configmanager): add ValidationService with tests
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user