diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ExtractIdValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ExtractIdValidator.cs new file mode 100644 index 0000000..253a391 --- /dev/null +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ExtractIdValidator.cs @@ -0,0 +1,25 @@ +using System.Text.RegularExpressions; +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; + +namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +public partial class ExtractIdValidator() : + HeaderFieldRegexValidator(h => h.ExtractId, "Extract ID", ExtractIdRegex(), ErrorCodes.MissingExtractId, ErrorCodes.InvalidExtractId) +{ + [GeneratedRegex(@"^\d{8}$")] + private static partial Regex ExtractIdRegex(); + + protected override IEnumerable RunAdditionalChecks(ParsedFile file, string value, bool hasErrored) + { + 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 + }; + } + } +} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/FileValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/FileValidator.cs deleted file mode 100644 index c818914..0000000 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/FileValidator.cs +++ /dev/null @@ -1,109 +0,0 @@ -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 Validate(ParsedFile file) - { - return ValidateHeaderPresence(file) - .Concat(ValidateTrailerPresence(file)) - .Concat(ValidateExtractId(file)) - .Concat(ValidateRecordCount(file)); - } - - private static IEnumerable ValidateHeaderPresence(ParsedFile file) - { - if (file.FileHeader == null) - { - yield return new ValidationError - { - Code = ErrorCodes.MissingHeader, - Error = "Header is missing", - Scope = ValidationErrorScope.File - }; - } - } - - private static IEnumerable ValidateTrailerPresence(ParsedFile file) - { - if (file.FileTrailer == null) - { - yield return new ValidationError - { - Code = ErrorCodes.MissingTrailer, - Error = "Trailer is missing", - Scope = ValidationErrorScope.File - }; - } - } - - private IEnumerable 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 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(); -} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderFieldRegexValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderFieldRegexValidator.cs index de3bce5..d4dea05 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderFieldRegexValidator.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderFieldRegexValidator.cs @@ -14,6 +14,12 @@ public class HeaderFieldRegexValidator( { public IEnumerable Validate(ParsedFile file) { + if (file.FileHeader == null) + { + yield break; + } + + var hasErrored = false; var header = file.FileHeader!; var value = fieldSelector.Compile().Invoke(header); @@ -31,6 +37,7 @@ public IEnumerable Validate(ParsedFile file) if (!pattern.IsMatch(value)) { + hasErrored = true; yield return new ValidationError { Scope = ValidationErrorScope.Header, @@ -39,5 +46,15 @@ public IEnumerable Validate(ParsedFile file) Code = errorCodeInvalidFormat, }; } + + foreach (var additionalError in RunAdditionalChecks(file, value, hasErrored)) + { + yield return additionalError; + } + } + + protected virtual IEnumerable RunAdditionalChecks(ParsedFile file, string value, bool hasErrored) + { + yield break; } } diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderPresenceValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderPresenceValidator.cs new file mode 100644 index 0000000..b875921 --- /dev/null +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/HeaderPresenceValidator.cs @@ -0,0 +1,19 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; + +namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +public class HeaderPresenceValidator : IFileValidator +{ + public IEnumerable Validate(ParsedFile file) + { + if (file.FileHeader == null) + { + yield return new ValidationError + { + Code = ErrorCodes.MissingHeader, + Error = "Header is missing", + Scope = ValidationErrorScope.File + }; + } + } +} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RecordCountValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RecordCountValidator.cs new file mode 100644 index 0000000..99182c3 --- /dev/null +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/RecordCountValidator.cs @@ -0,0 +1,34 @@ +using System.Text.RegularExpressions; +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; + +namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +public partial class RecordCountValidator() : + HeaderFieldRegexValidator(h => h.RecordCount, "Record count", RecordCountRegex(), ErrorCodes.MissingRecordCount, ErrorCodes.InvalidRecordCount) +{ + [GeneratedRegex(@"^(?!000000)\d{6}$")] + private static partial Regex RecordCountRegex(); + + protected override IEnumerable RunAdditionalChecks(ParsedFile file, string value, bool hasErrored) + { + 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 (!hasErrored && 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 + }; + } + } +} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/TrailerPresenceValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/TrailerPresenceValidator.cs new file mode 100644 index 0000000..26c5457 --- /dev/null +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/TrailerPresenceValidator.cs @@ -0,0 +1,19 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; + +namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +public class TrailerPresenceValidator : IFileValidator +{ + public IEnumerable Validate(ParsedFile file) + { + if (file.FileTrailer == null) + { + yield return new ValidationError + { + Code = ErrorCodes.MissingTrailer, + Error = "Trailer is missing", + Scope = ValidationErrorScope.File + }; + } + } +} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs index 4c8bed5..cb887e3 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs @@ -68,7 +68,12 @@ public static IEnumerable GetAllRecordValidators() public static IEnumerable GetAllFileValidators() { - return [new FileValidator()]; + return [ + new HeaderPresenceValidator(), + new TrailerPresenceValidator(), + new ExtractIdValidator(), + new RecordCountValidator() + ]; } [GeneratedRegex(@"^[BCU]$", RegexOptions.Compiled)] diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ExtractIdValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ExtractIdValidatorTests.cs new file mode 100644 index 0000000..63b30ab --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ExtractIdValidatorTests.cs @@ -0,0 +1,88 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class ExtractIdValidatorTests : ValidationTestBase +{ + [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); + } + + [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 + ); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/FileValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/FileValidatorTests.cs deleted file mode 100644 index e086846..0000000 --- a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/FileValidatorTests.cs +++ /dev/null @@ -1,211 +0,0 @@ -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 - ); - } -} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/HeaderPresenceValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/HeaderPresenceValidatorTests.cs new file mode 100644 index 0000000..96425c9 --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/HeaderPresenceValidatorTests.cs @@ -0,0 +1,25 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class HeaderPresenceValidatorTests : 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 + ); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/RecordCountValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/RecordCountValidatorTests.cs new file mode 100644 index 0000000..dc233da --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/RecordCountValidatorTests.cs @@ -0,0 +1,91 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class RecordCountValidatorTests : ValidationTestBase +{ + [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("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 + ); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/TrailerPresenceValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/TrailerPresenceValidatorTests.cs new file mode 100644 index 0000000..e2d88aa --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/TrailerPresenceValidatorTests.cs @@ -0,0 +1,25 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class TrailerPresenceValidatorTests : ValidationTestBase +{ + [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 + ); + } +}