Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit 31f12ad

Browse files
committed
WIP
1 parent 47f62dc commit 31f12ad

File tree

9 files changed

+351
-0
lines changed

9 files changed

+351
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
2+
3+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public class InlineMaxLengthValidator(string fieldName, int maxLength, string errorCodeMissing, string errorCodeTooLong, bool allowEmpty = false)
6+
: IRecordValidator
7+
{
8+
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
9+
{
10+
var value = fileDataRecord[fieldName];
11+
12+
if (value == null || (!allowEmpty && string.IsNullOrWhiteSpace(value)))
13+
{
14+
var error = $"{fieldName} is missing{(allowEmpty ? "" : " or empty")}";
15+
16+
yield return new ValidationError
17+
{
18+
RowNumber = fileDataRecord.RowNumber,
19+
Field = fieldName,
20+
Error = error,
21+
Code = errorCodeMissing,
22+
};
23+
yield break;
24+
}
25+
26+
if (value.Length > maxLength)
27+
{
28+
var error = $"{fieldName} exceeds maximum length of {maxLength}";
29+
30+
yield return new ValidationError
31+
{
32+
RowNumber = fileDataRecord.RowNumber,
33+
Field = fieldName,
34+
Error = error,
35+
Code = errorCodeTooLong,
36+
};
37+
}
38+
}
39+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Text.RegularExpressions;
2+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
3+
4+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
5+
6+
public class InlineRegexValidator(
7+
string fieldName,
8+
Regex pattern,
9+
string errorCodeMissing,
10+
string errorCodeInvalidFormat)
11+
: IRecordValidator
12+
{
13+
public IEnumerable<ValidationError> Validate(FileDataRecord fileDataRecord)
14+
{
15+
var value = fileDataRecord[fieldName];
16+
17+
if (value == null)
18+
{
19+
yield return new ValidationError
20+
{
21+
RowNumber = fileDataRecord.RowNumber,
22+
Field = fieldName,
23+
Error = $"{fieldName} is missing",
24+
Code = errorCodeMissing,
25+
};
26+
yield break;
27+
}
28+
29+
if (!pattern.IsMatch(value))
30+
{
31+
yield return new ValidationError
32+
{
33+
RowNumber = fileDataRecord.RowNumber,
34+
Field = fieldName,
35+
Error = $"{fieldName} is in an invalid format",
36+
Code = errorCodeInvalidFormat,
37+
};
38+
}
39+
}
40+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
4+
5+
public static partial class ValidatorRegistry
6+
{
7+
public static IEnumerable<IRecordValidator> GetAllRecordValidators()
8+
{
9+
return
10+
[
11+
new InlineRegexValidator("Sequence", SequenceRegex(), "NBSSAPPT012", "NBSSAPPT013"),
12+
new InlineMaxLengthValidator("Appointment ID", 27, "NBSSAPPT026", "NBSSAPPT027"),
13+
new InlineMaxLengthValidator("Clinic Address 3", 30, "NBSSAPPT059", "NBSSAPPT060", true)
14+
15+
16+
];
17+
}
18+
19+
public static IEnumerable<IFileValidator> GetAllFileValidators()
20+
{
21+
return [];
22+
}
23+
24+
[GeneratedRegex(@"^(?!000000)\d{6}$", RegexOptions.Compiled)]
25+
private static partial Regex SequenceRegex();
26+
}

src/ServiceLayer.sln.DotSettings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=nbssappt/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
public class AppointmentIdValidationTests : ValidationTestBase
4+
{
5+
[Fact]
6+
public void Validate_AppointmentIdMissing_ReturnsValidationError()
7+
{
8+
// Arrange
9+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Appointment ID"));
10+
11+
// Act
12+
var validationErrors = Validate(file);
13+
14+
// Assert
15+
validationErrors.ShouldBeSingleValidationError(
16+
"Appointment ID",
17+
"Appointment ID is missing or empty",
18+
"NBSSAPPT026"
19+
);
20+
}
21+
22+
[Theory]
23+
[InlineData("")]
24+
[InlineData(" ")]
25+
public void Validate_AppointmentIdBlank_ReturnsValidationError(string value)
26+
{
27+
// Arrange
28+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);
29+
30+
// Act
31+
var validationErrors = Validate(file);
32+
33+
// Assert
34+
validationErrors.ShouldBeSingleValidationError(
35+
"Appointment ID",
36+
"Appointment ID is missing or empty",
37+
"NBSSAPPT026"
38+
);
39+
}
40+
41+
[Theory]
42+
[InlineData("1234567890123456789012345678")] // 28 characters
43+
[InlineData("1234567890123456789012345678901234567890")] // 40 characters
44+
public void Validate_AppointmentIdTooLong_ReturnsValidationError(string value)
45+
{
46+
// Arrange
47+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);
48+
49+
// Act
50+
var validationErrors = Validate(file);
51+
52+
// Assert
53+
validationErrors.ShouldBeSingleValidationError(
54+
"Appointment ID",
55+
"Appointment ID exceeds maximum length of 27",
56+
"NBSSAPPT027"
57+
);
58+
}
59+
60+
[Theory]
61+
[InlineData("AS003-67240-RA1-DN-T1315-1")]
62+
[InlineData("AS003-67240-RA1-DN-T1045-01")]
63+
public void Validate_AppointmentIdValid_NoValidationErrorsReturned(string value)
64+
{
65+
// Arrange
66+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Appointment ID"] = value);
67+
68+
// Act
69+
var validationErrors = Validate(file);
70+
71+
// Assert
72+
Assert.Empty(validationErrors);
73+
}
74+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
public class ClinicAddress3ValidatorTests : ValidationTestBase
4+
{
5+
[Fact]
6+
public void Validate_ClinicAddress3Missing_ReturnsValidationError()
7+
{
8+
// Arrange
9+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Clinic Address 3"));
10+
11+
// Act
12+
var validationErrors = Validate(file);
13+
14+
// Assert
15+
validationErrors.ShouldBeSingleValidationError(
16+
"Clinic Address 3",
17+
"Clinic Address 3 is missing",
18+
"NBSSAPPT059"
19+
);
20+
}
21+
22+
[Theory]
23+
[InlineData("1234567890123456789012345678901")] // 31 characters
24+
[InlineData("1234567890123456789012345678901234567890")] // 40 characters
25+
public void Validate_ClinicAddress3TooLong_ReturnsValidationError(string value)
26+
{
27+
// Arrange
28+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Clinic Address 3"] = value);
29+
30+
// Act
31+
var validationErrors = Validate(file).ToList();
32+
33+
// Assert
34+
validationErrors.ShouldBeSingleValidationError(
35+
"Clinic Address 3",
36+
"Clinic Address 3 exceeds maximum length of 30",
37+
"NBSSAPPT060"
38+
);
39+
}
40+
41+
[Theory]
42+
[InlineData("")]
43+
[InlineData("123456789012345678901234567890")]
44+
[InlineData("Milton Keynes")]
45+
public void Validate_ClinicAddress3Valid_NoValidationErrorsReturned(string value)
46+
{
47+
// Arrange
48+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Clinic Address 3"] = value);
49+
50+
// Act
51+
var validationErrors = Validate(file).ToList();
52+
53+
// Assert
54+
Assert.Empty(validationErrors);
55+
}
56+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
public class SequenceValidatorTests : ValidationTestBase
4+
{
5+
[Fact]
6+
public void Validate_SequenceMissing_ReturnsValidationError()
7+
{
8+
// Arrange
9+
var file = ParsedFileWithModifiedRecord(r => r.Fields.Remove("Sequence"));
10+
11+
// Act
12+
var validationErrors = Validate(file);
13+
14+
// Assert
15+
validationErrors.ShouldBeSingleValidationError(
16+
"Sequence",
17+
"Sequence is missing",
18+
"NBSSAPPT012"
19+
);
20+
}
21+
22+
[Theory]
23+
[InlineData("1")] // Missing leading zeroes
24+
[InlineData("000000")] // Zero is invalid
25+
[InlineData("1000000")] // Too large
26+
[InlineData("")] // Blank
27+
[InlineData("asdf")] // NaN
28+
public void Validate_SequenceInvalidFormat_ReturnsValidationError(string value)
29+
{
30+
// Arrange
31+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Sequence"] = value);
32+
33+
// Act
34+
var validationErrors = Validate(file).ToList();
35+
36+
// Assert
37+
validationErrors.ShouldBeSingleValidationError(
38+
"Sequence",
39+
"Sequence is in an invalid format",
40+
"NBSSAPPT013"
41+
);
42+
}
43+
44+
[Theory]
45+
[InlineData("000001")]
46+
[InlineData("999999")]
47+
public void Validate_SequenceValidFormat_NoValidationErrorsReturned(string value)
48+
{
49+
// Arrange
50+
var file = ParsedFileWithModifiedRecord(r => r.Fields["Sequence"] = value);
51+
52+
// Act
53+
var validationErrors = Validate(file).ToList();
54+
55+
// Assert
56+
Assert.Empty(validationErrors);
57+
}
58+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
2+
3+
public static class ValidationErrorAssertions
4+
{
5+
public static ValidationError ShouldBeSingleValidationError(
6+
this IEnumerable<ValidationError> errors,
7+
string expectedField,
8+
string expectedError,
9+
string expectedCode,
10+
int? expectedRowNumber = null)
11+
{
12+
var error = Assert.Single(errors);
13+
Assert.Equal(expectedField, error.Field);
14+
Assert.Equal(expectedError, error.Error);
15+
16+
Assert.Equal(expectedCode, error.Code);
17+
18+
if (expectedRowNumber != null)
19+
{
20+
Assert.Equal(expectedRowNumber, error.RowNumber);
21+
}
22+
23+
return error;
24+
}
25+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
2+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
3+
4+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents.Validation;
5+
6+
public abstract class ValidationTestBase
7+
{
8+
protected readonly ValidationRunner SystemUnderTest;
9+
10+
protected ValidationTestBase()
11+
{
12+
var recordValidators = ValidatorRegistry.GetAllRecordValidators();
13+
var fileValidators = ValidatorRegistry.GetAllFileValidators();
14+
15+
SystemUnderTest = new ValidationRunner(fileValidators, recordValidators);
16+
}
17+
18+
protected static ParsedFile ValidParsedFile =>
19+
TestDataBuilder.BuildValidParsedFile();
20+
21+
protected static ParsedFile ParsedFileWithModifiedRecord(Action<FileDataRecord> mutate)
22+
{
23+
var file = TestDataBuilder.BuildValidParsedFile();
24+
mutate(file.DataRecords[0]); // Modify the first record
25+
return file;
26+
}
27+
28+
protected List<ValidationError> Validate(ParsedFile file){
29+
return SystemUnderTest.Validate(file).ToList();
30+
}
31+
}

0 commit comments

Comments
 (0)