Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System.Text.RegularExpressions;
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public partial class FileValidator : IFileValidator
{
private readonly HeaderFieldRegexValidator _headerExtractIdValidator = new(
x => x.ExtractId, "Extract ID", ExtractIdRegex(),
ErrorCodes.MissingExtractId, ErrorCodes.InvalidExtractId);

private readonly HeaderFieldRegexValidator _headerIdRecordCountValidator = new(
x => x.RecordCount, "Record count", RecordCountRegex(),
ErrorCodes.MissingRecordCount, ErrorCodes.InvalidRecordCount);

public IEnumerable<ValidationError> Validate(ParsedFile file)
{
return ValidateHeaderPresence(file)
.Concat(ValidateTrailerPresence(file))
.Concat(ValidateExtractId(file))
.Concat(ValidateRecordCount(file));
}

private static IEnumerable<ValidationError> ValidateHeaderPresence(ParsedFile file)
{
if (file.FileHeader == null)
{
yield return new ValidationError
{
Code = ErrorCodes.MissingHeader,
Error = "Header is missing",
Scope = ValidationErrorScope.File
};
}
}

private static IEnumerable<ValidationError> ValidateTrailerPresence(ParsedFile file)
{
if (file.FileTrailer == null)
{
yield return new ValidationError
{
Code = ErrorCodes.MissingTrailer,
Error = "Trailer is missing",
Scope = ValidationErrorScope.File
};
}
}

private IEnumerable<ValidationError> ValidateExtractId(ParsedFile file)
{
if (file.FileHeader == null) yield break;

foreach (var error in _headerExtractIdValidator.Validate(file))
{
yield return error;
}

if (file.FileTrailer != null && file.FileHeader.ExtractId != file.FileTrailer.ExtractId)
{
yield return new ValidationError
{
Field = "Extract ID",
Code = ErrorCodes.InconsistentExtractId,
Error = "Extract ID does not match value in header",
Scope = ValidationErrorScope.Trailer
};
}
}

private IEnumerable<ValidationError> ValidateRecordCount(ParsedFile file)
{
if (file.FileHeader == null) yield break;

var headerRecordCountErrors = _headerIdRecordCountValidator.Validate(file).ToList();

foreach (var error in headerRecordCountErrors)
{
yield return error;
}

if (file.FileTrailer != null && file.FileHeader.RecordCount != file.FileTrailer.RecordCount)
{
yield return new ValidationError
{
Field = "Record count",
Code = ErrorCodes.InconsistentRecordCount,
Error = "Record count does not match value in header",
Scope = ValidationErrorScope.Trailer
};
}
else if (headerRecordCountErrors.Count == 0 &&
file.DataRecords.Count != int.Parse(file.FileHeader.RecordCount!))
{
yield return new ValidationError
{
Code = ErrorCodes.UnexpectedRecordCount,
Error = "Record count does not match value in header and trailer",
Scope = ValidationErrorScope.File
};
}
}

[GeneratedRegex(@"^\d{8}$")]
private static partial Regex ExtractIdRegex();

[GeneratedRegex(@"^(?!000000)\d{6}$")]
private static partial Regex RecordCountRegex();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

public class HeaderFieldRegexValidator(
Expression<Func<FileHeaderRecord, string?>> fieldSelector,
string fieldName,
Regex pattern,
string errorCodeMissing,
string errorCodeInvalidFormat)
: IFileValidator
{
public IEnumerable<ValidationError> Validate(ParsedFile file)
{
var header = file.FileHeader!;
var value = fieldSelector.Compile().Invoke(header);

if (value == null)
{
yield return new ValidationError
{
Scope = ValidationErrorScope.Header,
Field = fieldName,
Error = $"{fieldName} is missing",
Code = errorCodeMissing,
};
yield break;
}

if (!pattern.IsMatch(value))
{
yield return new ValidationError
{
Scope = ValidationErrorScope.Header,
Field = fieldName,
Error = $"{fieldName} is in an invalid format",
Code = errorCodeInvalidFormat,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Runtime.InteropServices.JavaScript;
using System.Text.RegularExpressions;

namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
Expand Down Expand Up @@ -69,7 +68,7 @@ public static IEnumerable<IRecordValidator> GetAllRecordValidators()

public static IEnumerable<IFileValidator> GetAllFileValidators()
{
return [];
return [new FileValidator()];
}

[GeneratedRegex(@"^[BCU]$", RegexOptions.Compiled)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static ParsedFile BuildValidParsedFile(int numberOfRecords = 3)
ExtractId = "00000107",
TransferStartDate = "20250519",
TransferStartTime = "153806",
RecordCount = numberOfRecords.ToString()
RecordCount = numberOfRecords.ToString("D6")
};

var trailer = new FileTrailerRecord
Expand All @@ -21,7 +21,7 @@ public static ParsedFile BuildValidParsedFile(int numberOfRecords = 3)
ExtractId = "00000107",
TransferEndDate = "20250519",
TransferEndTime = "153957",
RecordCount = numberOfRecords.ToString()
RecordCount = numberOfRecords.ToString("D6")
};

var dataRecords = Enumerable.Range(1, numberOfRecords)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;

namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;

public class FileValidatorTests : ValidationTestBase
{
[Fact]
public void Validate_HeaderMissing_ReturnsValidationError()
{
// Arrange
var file = ValidParsedFile;
file.FileHeader = null;

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
null,
"Header is missing",
ErrorCodes.MissingHeader,
ValidationErrorScope.File
);
}

[Fact]
public void Validate_TrailerMissing_ReturnsValidationError()
{
// Arrange
var file = ValidParsedFile;
file.FileTrailer = null;

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
null,
"Trailer is missing",
ErrorCodes.MissingTrailer,
ValidationErrorScope.File
);
}

[Fact]
public void Validate_HeaderExtractIdMissing_ReturnsValidationError()
{
// Arrange
var file = ValidParsedFile;
file.FileHeader!.ExtractId = null;

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
"Extract ID",
"Extract ID is missing",
ErrorCodes.MissingExtractId,
ValidationErrorScope.Header
);
}

[Theory]
[InlineData("1")] // Missing leading zeroes
[InlineData("100000000")] // Too large
[InlineData("")] // Blank
[InlineData("asdf")] // NaN
public void Validate_HeaderExtractIdInvalidFormat_ReturnsValidationError(string value)
{
// Arrange
var file = ValidParsedFile;
file.FileHeader!.ExtractId = value;

// Act
var validationErrors = Validate(file).ToList();

// Assert
validationErrors.ShouldContainValidationError(
"Extract ID",
"Extract ID is in an invalid format",
ErrorCodes.InvalidExtractId,
ValidationErrorScope.Header
);
}

[Theory]
[InlineData("00000000")]
[InlineData("00000001")]
[InlineData("99999999")]
public void Validate_HeaderExtractIdValidFormat_NoValidationErrorsReturned(string value)
{
// Arrange
var file = ValidParsedFile;
file.FileHeader!.ExtractId = value;
file.FileTrailer!.ExtractId = value;

// Act
var validationErrors = Validate(file).ToList();

// Assert
Assert.Empty(validationErrors);
}

[Fact]
public void Validate_HeaderRecordCountMissing_ReturnsValidationError()
{
// Arrange
var file = ValidParsedFile;
file.FileHeader!.RecordCount = null;

// Act
var validationErrors = Validate(file);

// Assert
validationErrors.ShouldContainValidationError(
"Record count",
"Record count is missing",
ErrorCodes.MissingRecordCount,
ValidationErrorScope.Header
);
}

[Theory]
[InlineData("1")] // Missing leading zeroes
[InlineData("000000")] // All zeroes
[InlineData("1000000")] // Too large
[InlineData("")] // Blank
[InlineData("asdf")] // NaN
public void Validate_HeaderRecordCountInvalidFormat_ReturnsValidationError(string value)
{
// Arrange
var file = ValidParsedFile;
file.FileHeader!.RecordCount = value;

// Act
var validationErrors = Validate(file).ToList();

// Assert
validationErrors.ShouldContainValidationError(
"Record count",
"Record count is in an invalid format",
ErrorCodes.InvalidRecordCount,
ValidationErrorScope.Header
);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("00000108")]
public void Validate_TrailerExtractIdMismatch_ReturnsValidationError(string? value)
{
// Arrange
var file = ValidParsedFile;
file.FileTrailer!.ExtractId = value;

// Act
var validationErrors = Validate(file).ToList();

// Assert
validationErrors.ShouldContainValidationError(
"Extract ID",
"Extract ID does not match value in header",
ErrorCodes.InconsistentExtractId,
ValidationErrorScope.Trailer
);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("000002")]
[InlineData("000004")]
public void Validate_TrailerRecordCountMismatch_ReturnsValidationError(string? value)
{
// Arrange
var file = ValidParsedFile;
file.FileTrailer!.RecordCount = value;

// Act
var validationErrors = Validate(file).ToList();

// Assert
validationErrors.ShouldContainValidationError(
"Record count",
"Record count does not match value in header",
ErrorCodes.InconsistentRecordCount,
ValidationErrorScope.Trailer
);
}

[Fact]
public void Validate_UnexpectedRecordCount_ReturnsValidationError()
{
// Arrange
var file = ValidParsedFile;
file.DataRecords.RemoveAt(0);

// Act
var validationErrors = Validate(file).ToList();

// Assert
validationErrors.ShouldContainValidationError(
null,
"Record count does not match value in header and trailer",
ErrorCodes.UnexpectedRecordCount,
ValidationErrorScope.File
);
}
}
Loading
Loading