From e59056e325ab527f02bd994cac706461bb70f822 Mon Sep 17 00:00:00 2001 From: Ian Nelson Date: Thu, 29 May 2025 16:45:38 +0100 Subject: [PATCH 1/2] feat: DTOSS-9161 - validation for date/time-related fields --- .../Validation/DateFormatValidator.cs | 39 ++++++++++ .../Validation/ValidatorRegistry.cs | 9 +++ .../ActionTimestampValidatorTests.cs | 66 +++++++++++++++++ .../Validation/ApptDateValidatorTests.cs | 66 +++++++++++++++++ .../Validation/ApptTimeValidatorTests.cs | 64 +++++++++++++++++ .../Validation/DateFormatValidatorTests.cs | 71 +++++++++++++++++++ .../Validation/EpisodeStartValidatorTests.cs | 66 +++++++++++++++++ .../Validation/EpisodeTypeValidatorTests.cs | 2 +- 8 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ActionTimestampValidatorTests.cs create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptDateValidatorTests.cs create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptTimeValidatorTests.cs create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs create mode 100644 tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeStartValidatorTests.cs diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs new file mode 100644 index 0000000..c268308 --- /dev/null +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs @@ -0,0 +1,39 @@ +using System.Globalization; +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; + +namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +public class DateFormatValidator( + string fieldName, + string format, + string errorCodeMissing, + string errorCodeInvalidFormat) : IRecordValidator +{ + public IEnumerable Validate(FileDataRecord fileDataRecord) + { + var value = fileDataRecord[fieldName]; + + if (value == null) + { + yield return new ValidationError + { + RowNumber = fileDataRecord.RowNumber, + Field = fieldName, + Error = $"{fieldName} is missing", + Code = errorCodeMissing, + }; + yield break; + } + + if (!DateTime.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out _)) + { + yield return new ValidationError + { + RowNumber = fileDataRecord.RowNumber, + Field = fieldName, + Error = $"{fieldName} is in an invalid format", + Code = errorCodeInvalidFormat, + }; + } + } +} diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs index 76baf02..0847dc0 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/ValidatorRegistry.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices.JavaScript; using System.Text.RegularExpressions; namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; @@ -27,6 +28,8 @@ public static IEnumerable GetAllRecordValidators() new NhsNumValidator(), new RegexValidator("Episode Type", EpisodeTypeRegex(), ErrorCodes.MissingEpisodeType, ErrorCodes.InvalidEpisodeType), + new DateFormatValidator("Episode Start", "yyyyMMdd", ErrorCodes.MissingEpisodeStart, + ErrorCodes.InvalidEpisodeStart), new MaxLengthValidator("Batch ID", 9, ErrorCodes.MissingBatchId, ErrorCodes.InvalidBatchId), new RegexValidator("Screen or Asses", ScreenOrAssesRegex(), ErrorCodes.MissingScreenOrAsses, @@ -37,6 +40,10 @@ public static IEnumerable GetAllRecordValidators() ErrorCodes.InvalidBookedBy), new RegexValidator("Cancelled By", CancelledByRegex(), ErrorCodes.MissingCancelledBy, ErrorCodes.InvalidCancelledBy), + new DateFormatValidator("Appt Date", "yyyyMMdd", ErrorCodes.MissingApptDate, + ErrorCodes.InvalidApptDate), + new DateFormatValidator("Appt Time", "HHmm", ErrorCodes.MissingApptTime, + ErrorCodes.InvalidApptTime), new MaxLengthValidator("Location", 5, ErrorCodes.MissingLocation, ErrorCodes.InvalidLocation), new MaxLengthValidator("Clinic Name", 40, ErrorCodes.MissingClinicName, @@ -55,6 +62,8 @@ public static IEnumerable GetAllRecordValidators() ErrorCodes.InvalidClinicAddress5, true), new MaxLengthValidator("Postcode", 8, ErrorCodes.MissingPostcode, ErrorCodes.InvalidPostcode, true), + new DateFormatValidator("Action Timestamp", "yyyyMMdd-HHmmss", ErrorCodes.MissingActionTimestamp, + ErrorCodes.InvalidActionTimestamp) ]; } diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ActionTimestampValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ActionTimestampValidatorTests.cs new file mode 100644 index 0000000..99c4ef2 --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ActionTimestampValidatorTests.cs @@ -0,0 +1,66 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class ActionTimestampValidatorTests : ValidationTestBase +{ + [Fact] + public void Validate_ActionTimestampMissing_ReturnsValidationError() + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Action Timestamp")); + + // Act + var validationErrors = Validate(file); + + // Assert + validationErrors.ShouldContainValidationError( + "Action Timestamp", + "Action Timestamp is missing", + ErrorCodes.MissingActionTimestamp + ); + } + + [Theory] + [InlineData("20250631-183156")] // too many days in June + [InlineData("202S0630-183156")] // invalid character + [InlineData("20250630-1831")] // too short + [InlineData("20250630T1831")] // unexpected separator + [InlineData("250630-183156")] // too short, ddMMyy + [InlineData("20250630-1456")] // No seconds + [InlineData("20250630-18:31:56")] // unexpected separators + [InlineData("20250229-183156")] // Not a leap year + public void Validate_ActionTimestampInvalidFormat_ReturnsValidationError(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Action Timestamp"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + validationErrors.ShouldContainValidationError( + "Action Timestamp", + "Action Timestamp is in an invalid format", + ErrorCodes.InvalidActionTimestamp + ); + } + + [Theory] + [InlineData("20250529-163243")] + [InlineData("20240229-163243")] + [InlineData("20250731-163243")] + [InlineData("19990806-235959")] + [InlineData("20561212-000000")] + public void Validate_ActionTimestampValidFormat_NoValidationErrorsReturned(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Action Timestamp"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + Assert.Empty(validationErrors); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptDateValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptDateValidatorTests.cs new file mode 100644 index 0000000..2b7a686 --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptDateValidatorTests.cs @@ -0,0 +1,66 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class ApptDateValidatorTests : ValidationTestBase +{ + [Fact] + public void Validate_ApptDateMissing_ReturnsValidationError() + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appt Date")); + + // Act + var validationErrors = Validate(file); + + // Assert + validationErrors.ShouldContainValidationError( + "Appt Date", + "Appt Date is missing", + ErrorCodes.MissingApptDate + ); + } + + [Theory] + [InlineData("20250631")] // too many days in June + [InlineData("202S0630")] // invalid character + [InlineData("202506")] // too short + [InlineData("30062025")] // ddMMyyyy and not valid as yyyyMMdd + [InlineData("250630")] // too short, ddMMyy + [InlineData("20250630-145621")] // Includes time + [InlineData("20250229")] // Not a leap year + public void Validate_ApptDateInvalidFormat_ReturnsValidationError(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Date"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + validationErrors.ShouldContainValidationError( + "Appt Date", + "Appt Date is in an invalid format", + ErrorCodes.InvalidApptDate + ); + } + + [Theory] + [InlineData("20250101")] + [InlineData("20250228")] + [InlineData("20250331")] + [InlineData("20251231")] + [InlineData("20240229")] + [InlineData("19990331")] + public void Validate_ApptDateValidFormat_NoValidationErrorsReturned(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Date"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + Assert.Empty(validationErrors); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptTimeValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptTimeValidatorTests.cs new file mode 100644 index 0000000..7b36e7e --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/ApptTimeValidatorTests.cs @@ -0,0 +1,64 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class ApptTimeValidatorTests : ValidationTestBase +{ + [Fact] + public void Validate_ApptTimeMissing_ReturnsValidationError() + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appt Time")); + + // Act + var validationErrors = Validate(file); + + // Assert + validationErrors.ShouldContainValidationError( + "Appt Time", + "Appt Time is missing", + ErrorCodes.MissingApptTime + ); + } + + [Theory] + [InlineData("2407")] // too many hours + [InlineData("1960")] // too many minutes + [InlineData("842")] // too short + [InlineData("10435")] // too long + [InlineData("193S")] // invalid characters + public void Validate_ApptTimeInvalidFormat_ReturnsValidationError(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Time"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + validationErrors.ShouldContainValidationError( + "Appt Time", + "Appt Time is in an invalid format", + ErrorCodes.InvalidApptTime + ); + } + + [Theory] + [InlineData("0000")] + [InlineData("2359")] + [InlineData("0001")] + [InlineData("2358")] + [InlineData("1200")] + [InlineData("1300")] + public void Validate_ApptTimeValidFormat_NoValidationErrorsReturned(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Appt Time"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + Assert.Empty(validationErrors); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs new file mode 100644 index 0000000..7396f27 --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs @@ -0,0 +1,71 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; +using System.Text.RegularExpressions; +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class DateFormatValidatorTests +{ + private const string FieldName = "TestField"; + private const string MissingCode = "ERR001"; + private const string InvalidFormatCode = "ERR002"; + private const string Format = "yyyyMMdd"; + + [Fact] + public void Validate_NullValue_ShouldReturnMissingError() + { + // Arrange + var record = new FileDataRecord + { + RowNumber = 1 + }; + record.Fields.Clear(); + + var validator = new DateFormatValidator(FieldName, Format, MissingCode, InvalidFormatCode); + + // Act + var errors = validator.Validate(record).ToList(); + + // Assert + errors.ShouldContainValidationError(FieldName, $"{FieldName} is missing", MissingCode, 1); + } + + [Theory] + [InlineData("")] + [InlineData("20250631")] + public void Validate_ValueNotMatchingPattern_ShouldReturnInvalidFormatError(string invalidValue) + { + // Arrange + var record = new FileDataRecord + { + RowNumber = 2 + }; + record.Fields.Add(FieldName, invalidValue); + + var validator = new DateFormatValidator(FieldName, Format, MissingCode, InvalidFormatCode); + + // Act + var errors = validator.Validate(record).ToList(); + + // Assert + errors.ShouldContainValidationError(FieldName, $"{FieldName} is in an invalid format", InvalidFormatCode, 2); + } + + [Theory] + [InlineData("20250630")] + [InlineData("19990807")] + public void Validate_ValueMatchingPattern_ShouldReturnNoErrors(string validValue) + { + var record = new FileDataRecord + { + RowNumber = 3 + }; + record.Fields.Add(FieldName, validValue); + + var validator = new DateFormatValidator(FieldName, Format, MissingCode, InvalidFormatCode); + + var errors = validator.Validate(record).ToList(); + + Assert.Empty(errors); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeStartValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeStartValidatorTests.cs new file mode 100644 index 0000000..b3d9566 --- /dev/null +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeStartValidatorTests.cs @@ -0,0 +1,66 @@ +using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; + +namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation; + +public class EpisodeStartValidatorTests : ValidationTestBase +{ + [Fact] + public void Validate_EpisodeStartMissing_ReturnsValidationError() + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Episode Start")); + + // Act + var validationErrors = Validate(file); + + // Assert + validationErrors.ShouldContainValidationError( + "Episode Start", + "Episode Start is missing", + ErrorCodes.MissingEpisodeStart + ); + } + + [Theory] + [InlineData("20250631")] // too many days in June + [InlineData("202S0630")] // invalid character + [InlineData("202506")] // too short + [InlineData("30062025")] // ddMMyyyy and not valid as yyyyMMdd + [InlineData("250630")] // too short, ddMMyy + [InlineData("20250630-145621")] // Includes time + [InlineData("20250229")] // Not a leap year + public void Validate_EpisodeStartInvalidFormat_ReturnsValidationError(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Episode Start"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + validationErrors.ShouldContainValidationError( + "Episode Start", + "Episode Start is in an invalid format", + ErrorCodes.InvalidEpisodeStart + ); + } + + [Theory] + [InlineData("20250101")] + [InlineData("20250228")] + [InlineData("20250331")] + [InlineData("20251231")] + [InlineData("20240229")] + [InlineData("19990331")] + public void Validate_EpisodeStartValidFormat_NoValidationErrorsReturned(string value) + { + // Arrange + var file = ParsedFileWithModifiedRecord(r => r.Fields["Episode Start"] = value); + + // Act + var validationErrors = Validate(file).ToList(); + + // Assert + Assert.Empty(validationErrors); + } +} diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeTypeValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeTypeValidatorTests.cs index 63a7fab..63a32df 100644 --- a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeTypeValidatorTests.cs +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/EpisodeTypeValidatorTests.cs @@ -30,7 +30,7 @@ public void Validate_EpisodeTypeMissing_ReturnsValidationError() [InlineData(" ")] // Whitespace [InlineData("FG")] // Too many characters [InlineData("RST")] // Too many characters - public void Validate_EpisoderTypeInvalidFormat_ReturnsValidationError(string value) + public void Validate_EpisodeTypeInvalidFormat_ReturnsValidationError(string value) { // Arrange var file = ParsedFileWithModifiedRecord(r => r.Fields["Episode Type"] = value); From 30c8e955a878e6ee667eeb3b142401fdd7fb3db9 Mon Sep 17 00:00:00 2001 From: Ian Nelson Date: Thu, 29 May 2025 19:48:38 +0100 Subject: [PATCH 2/2] Resolved errors following merge --- .../NbssAppointmentEvents/Validation/DateFormatValidator.cs | 2 ++ .../Validation/DateFormatValidatorTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs index c268308..5129387 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidator.cs @@ -17,6 +17,7 @@ public IEnumerable Validate(FileDataRecord fileDataRecord) { yield return new ValidationError { + Scope = ValidationErrorScope.Record, RowNumber = fileDataRecord.RowNumber, Field = fieldName, Error = $"{fieldName} is missing", @@ -29,6 +30,7 @@ public IEnumerable Validate(FileDataRecord fileDataRecord) { yield return new ValidationError { + Scope = ValidationErrorScope.Record, RowNumber = fileDataRecord.RowNumber, Field = fieldName, Error = $"{fieldName} is in an invalid format", diff --git a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs index 7396f27..8212ebb 100644 --- a/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs +++ b/tests/ServiceLayer.Mesh.Tests/FileTypes/NbssAppointmentEvents/Validation/DateFormatValidatorTests.cs @@ -27,7 +27,7 @@ public void Validate_NullValue_ShouldReturnMissingError() var errors = validator.Validate(record).ToList(); // Assert - errors.ShouldContainValidationError(FieldName, $"{FieldName} is missing", MissingCode, 1); + errors.ShouldContainValidationError(FieldName, $"{FieldName} is missing", MissingCode, ValidationErrorScope.Record, 1); } [Theory] @@ -48,7 +48,7 @@ public void Validate_ValueNotMatchingPattern_ShouldReturnInvalidFormatError(stri var errors = validator.Validate(record).ToList(); // Assert - errors.ShouldContainValidationError(FieldName, $"{FieldName} is in an invalid format", InvalidFormatCode, 2); + errors.ShouldContainValidationError(FieldName, $"{FieldName} is in an invalid format", InvalidFormatCode,ValidationErrorScope.Record, 2); } [Theory]