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

Commit 6e7c68a

Browse files
feat: fileParser validation handling (#45)
Co-authored-by: Ian Nelson <[email protected]>
1 parent 1f24ea7 commit 6e7c68a

File tree

11 files changed

+445
-19
lines changed

11 files changed

+445
-19
lines changed

src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/FileParser.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using CsvHelper;
22
using CsvHelper.Configuration;
33
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
4+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
45
using System.Globalization;
56
using System.Text;
67

@@ -56,7 +57,10 @@ public ParsedFile Parse(Stream stream)
5657
break;
5758

5859
default:
59-
throw new InvalidOperationException($"Unknown record identifier: {recordIdentifier}");
60+
recordIdentifier ??= "No Record Identifier found";
61+
throw new FileParsingException(
62+
ErrorCodes.UnknownRecordTypeIdentifier,
63+
$"Unknown Record Identifier {recordIdentifier}");
6064
}
6165
}
6266

@@ -98,7 +102,7 @@ private static FileDataRecord ParseDataRecord(CsvReader csv, List<string> column
98102
{
99103
if (columnHeadings.Count == 0)
100104
{
101-
throw new InvalidOperationException("Field headers (NBSSAPPT_FLDS) must appear before data records.");
105+
throw new FileParsingException(ErrorCodes.MissingFieldHeadings, "Field headings are missing");
102106
}
103107

104108
const int dataFieldStartIndex = 1;
@@ -140,4 +144,3 @@ public FileHeaderRecordMap()
140144
}
141145
}
142146
}
143-
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;
2+
3+
public class FileParsingException : Exception
4+
{
5+
public string Code { get; }
6+
7+
public FileParsingException(string code, string message)
8+
: base(message)
9+
{
10+
Code = code;
11+
}
12+
13+
public FileParsingException(string code, string message, Exception innerException)
14+
: base(message, innerException)
15+
{
16+
Code = code;
17+
}
18+
}
19+
Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.Extensions.Logging;
12
using ServiceLayer.Data.Models;
23
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
34

@@ -6,22 +7,65 @@ namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;
67
public class FileTransformer(
78
IFileParser fileParser,
89
IValidationRunner validationRunner,
9-
IStagingPersister stagingPersister)
10+
IStagingPersister stagingPersister,
11+
ILogger<FileTransformer> logger)
1012
: FileTransformerBase
1113
{
1214
protected override MeshFileType HandlesFileType => MeshFileType.NbssAppointmentEvents;
1315

1416
public override async Task<IList<ValidationError>> TransformFileAsync(Stream stream, MeshFile metaData)
1517
{
16-
// TODO - wrap this parsing in a try-catch and return a List<ValidationError> in case of any unforeseen parsing issues (file is totally unlike anything we expect)
17-
var parsed = fileParser.Parse(stream);
18+
try
19+
{
20+
var parsed = fileParser.Parse(stream);
21+
var validationErrors = validationRunner.Validate(parsed);
22+
23+
if (!validationErrors.Any())
24+
{
25+
await stagingPersister.WriteStagedData(parsed, metaData);
26+
}
1827

19-
var validationErrors = validationRunner.Validate(parsed);
20-
if (!validationErrors.Any())
28+
return validationErrors;
29+
}
30+
catch (FileParsingException ex)
2131
{
22-
await stagingPersister.WriteStagedData(parsed, metaData);
32+
return HandleFileParsingException(ex);
2333
}
34+
catch (Exception ex)
35+
{
36+
return HandleUnexpectedException(ex, metaData);
37+
}
38+
}
39+
40+
private List<ValidationError> HandleFileParsingException(FileParsingException ex)
41+
{
42+
logger.LogError("File parsing failed with validation error. Code: {ErrorCode}, Message: {ErrorMessage}",
43+
ex.Code, ex.Message);
44+
45+
return
46+
[
47+
new ValidationError
48+
{
49+
Code = ex.Code,
50+
Error = ex.Message,
51+
Scope = ValidationErrorScope.File
52+
}
53+
];
54+
}
55+
56+
private IList<ValidationError> HandleUnexpectedException(Exception ex, MeshFile metaData)
57+
{
58+
logger.LogError(ex, "System error occurred while parsing NBSS appointment file. File: {FileName}",
59+
metaData.FileId);
2460

25-
return validationErrors;
61+
return
62+
[
63+
new ValidationError
64+
{
65+
Code = ErrorCodes.UnableToParseFile,
66+
Error = "Unable to parse file",
67+
Scope = ValidationErrorScope.File
68+
}
69+
];
2670
}
2771
}

src/ServiceLayer.Mesh/Functions/FileExtractFunction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ private async Task ProcessFileExtraction(MeshFile file)
109109

110110
private async Task HandleExtractionError(MeshFile file, FileExtractQueueMessage message, Exception ex)
111111
{
112-
logger.LogError(ex, "An exception occurred during file extraction for fileId: {fileId}", message.FileId);
112+
logger.LogError(ex, "An exception occurred during file extraction for fileId: {FileId}", message.FileId);
113113
file.Status = MeshFileStatus.FailedExtract;
114114
file.LastUpdatedUtc = DateTime.UtcNow;
115115
await serviceLayerDbContext.SaveChangesAsync();

src/ServiceLayer.Shared/Data/Models/MeshFileType.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ namespace ServiceLayer.Data.Models;
22

33
public enum MeshFileType
44
{
5-
NbssAppointmentEvents
5+
NbssAppointmentEvents,
6+
Unknown
67
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
using Microsoft.Extensions.Logging;
2+
using Moq;
3+
using ServiceLayer.Data.Models;
4+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents;
5+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Models;
6+
using ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation;
7+
using ServiceLayer.TestUtilities;
8+
9+
namespace ServiceLayer.Mesh.Tests.FileTypes.NbssAppointmentEvents;
10+
11+
public class FileTransformerTests
12+
{
13+
private readonly Mock<IFileParser> _fileParserMock = new();
14+
private readonly Mock<IValidationRunner> _validationRunnerMock = new();
15+
private readonly Mock<IStagingPersister> _stagingPersisterMock = new();
16+
private readonly Mock<ILogger<FileTransformer>> _loggerMock = new();
17+
private readonly FileTransformer _fileTransformer;
18+
private readonly MeshFile _testMeshFile;
19+
private readonly Stream _testStream;
20+
private readonly ParsedFile parsedFile = new();
21+
22+
public FileTransformerTests()
23+
{
24+
_fileTransformer = new FileTransformer(
25+
_fileParserMock.Object,
26+
_validationRunnerMock.Object,
27+
_stagingPersisterMock.Object,
28+
_loggerMock.Object);
29+
30+
_testMeshFile = new MeshFile
31+
{
32+
FileId = "test-file-123",
33+
FileType = MeshFileType.NbssAppointmentEvents,
34+
MailboxId = "testMailboxId",
35+
Status = MeshFileStatus.Extracted
36+
};
37+
38+
_testStream = new MemoryStream();
39+
}
40+
41+
[Fact]
42+
public void CanHandle_NbssAppointmentEventsFileType_ReturnsTrue()
43+
{
44+
// Act
45+
var result = _fileTransformer.CanHandle(MeshFileType.NbssAppointmentEvents);
46+
47+
// Assert
48+
Assert.True(result);
49+
}
50+
51+
[Fact]
52+
public void CanHandle_OtherFileType_ReturnsFalse()
53+
{
54+
// Act
55+
var result = _fileTransformer.CanHandle(MeshFileType.Unknown);
56+
57+
// Assert
58+
Assert.False(result);
59+
}
60+
61+
[Fact]
62+
public async Task TransformFileAsync_ValidFileWithNoValidationErrors_ParsesValidatesAndPersists()
63+
{
64+
// Arrange
65+
var validationErrors = new List<ValidationError>();
66+
67+
_fileParserMock.Setup(p => p.Parse(_testStream)).Returns(parsedFile);
68+
_validationRunnerMock.Setup(v => v.Validate(parsedFile)).Returns(validationErrors);
69+
70+
// Act
71+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
72+
73+
// Assert
74+
Assert.Empty(result);
75+
_fileParserMock.Verify(p => p.Parse(_testStream), Times.Once);
76+
_validationRunnerMock.Verify(v => v.Validate(parsedFile), Times.Once);
77+
_stagingPersisterMock.Verify(s => s.WriteStagedData(parsedFile, _testMeshFile), Times.Once);
78+
_loggerMock.VerifyNoLogs(LogLevel.Error);
79+
}
80+
81+
[Fact]
82+
public async Task TransformFileAsync_ValidFileWithValidationErrors_DoesNotPersistData()
83+
{
84+
// Arrange
85+
var validationErrors = new List<ValidationError>
86+
{
87+
new() { Code = "TEST001", Error = "Test validation error", Scope = ValidationErrorScope.Record, RowNumber = 1 }
88+
};
89+
90+
_fileParserMock.Setup(p => p.Parse(_testStream)).Returns(parsedFile);
91+
_validationRunnerMock.Setup(v => v.Validate(parsedFile)).Returns(validationErrors);
92+
93+
// Act
94+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
95+
96+
// Assert
97+
Assert.Equal(validationErrors, result);
98+
_fileParserMock.Verify(p => p.Parse(_testStream), Times.Once);
99+
_validationRunnerMock.Verify(v => v.Validate(parsedFile), Times.Once);
100+
_stagingPersisterMock.Verify(s => s.WriteStagedData(It.IsAny<ParsedFile>(), It.IsAny<MeshFile>()), Times.Never);
101+
_loggerMock.VerifyNoLogs(LogLevel.Error);
102+
}
103+
104+
[Fact]
105+
public async Task TransformFileAsync_FileParsingExceptionThrown_ReturnsFileValidationError()
106+
{
107+
// Arrange
108+
var fileParsingException = new FileParsingException(ErrorCodes.UnknownRecordTypeIdentifier, "Unknown record type identifier 'INVALID_TYPE'");
109+
110+
_fileParserMock.Setup(p => p.Parse(_testStream)).Throws(fileParsingException);
111+
112+
// Act
113+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
114+
115+
// Assert
116+
Assert.Single(result);
117+
var validationError = result[0];
118+
Assert.Equal(ErrorCodes.UnknownRecordTypeIdentifier, validationError.Code);
119+
Assert.Equal("Unknown record type identifier 'INVALID_TYPE'", validationError.Error);
120+
Assert.Equal(ValidationErrorScope.File, validationError.Scope);
121+
122+
_fileParserMock.Verify(p => p.Parse(_testStream), Times.Once);
123+
_validationRunnerMock.Verify(v => v.Validate(It.IsAny<ParsedFile>()), Times.Never);
124+
_stagingPersisterMock.Verify(s => s.WriteStagedData(It.IsAny<ParsedFile>(), It.IsAny<MeshFile>()), Times.Never);
125+
126+
_loggerMock.VerifyLogger(LogLevel.Error,
127+
$"File parsing failed with validation error. Code: {ErrorCodes.UnknownRecordTypeIdentifier}, Message: Unknown record type identifier 'INVALID_TYPE'");
128+
}
129+
130+
[Fact]
131+
public async Task TransformFileAsync_UnexpectedExceptionThrown_ReturnsSystemValidationError()
132+
{
133+
// Arrange
134+
var unexpectedException = new InvalidOperationException("Something went wrong");
135+
_fileParserMock.Setup(p => p.Parse(_testStream)).Throws(unexpectedException);
136+
137+
// Act
138+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
139+
140+
// Assert
141+
Assert.Single(result);
142+
var validationError = result[0];
143+
Assert.Equal(ErrorCodes.UnableToParseFile, validationError.Code);
144+
Assert.Equal("Unable to parse file", validationError.Error);
145+
Assert.Equal(ValidationErrorScope.File, validationError.Scope);
146+
147+
_fileParserMock.Verify(p => p.Parse(_testStream), Times.Once);
148+
_validationRunnerMock.Verify(v => v.Validate(It.IsAny<ParsedFile>()), Times.Never);
149+
_stagingPersisterMock.Verify(s => s.WriteStagedData(It.IsAny<ParsedFile>(), It.IsAny<MeshFile>()), Times.Never);
150+
151+
_loggerMock.VerifyLogger(LogLevel.Error,
152+
$"System error occurred while parsing NBSS appointment file. File: {_testMeshFile.FileId}",
153+
ex => ex == unexpectedException);
154+
}
155+
156+
[Fact]
157+
public async Task TransformFileAsync_ValidationRunnerThrowsException_ReturnsSystemValidationError()
158+
{
159+
// Arrange
160+
var validationException = new InvalidOperationException("Validation failed");
161+
162+
_fileParserMock.Setup(p => p.Parse(_testStream)).Returns(parsedFile);
163+
_validationRunnerMock.Setup(v => v.Validate(parsedFile)).Throws(validationException);
164+
165+
// Act
166+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
167+
168+
// Assert
169+
Assert.Single(result);
170+
var validationError = result[0];
171+
Assert.Equal(ErrorCodes.UnableToParseFile, validationError.Code);
172+
Assert.Equal("Unable to parse file", validationError.Error);
173+
Assert.Equal(ValidationErrorScope.File, validationError.Scope);
174+
175+
_fileParserMock.Verify(p => p.Parse(_testStream), Times.Once);
176+
_validationRunnerMock.Verify(v => v.Validate(parsedFile), Times.Once);
177+
_stagingPersisterMock.Verify(s => s.WriteStagedData(It.IsAny<ParsedFile>(), It.IsAny<MeshFile>()), Times.Never);
178+
179+
_loggerMock.VerifyLogger(LogLevel.Error,
180+
$"System error occurred while parsing NBSS appointment file. File: {_testMeshFile.FileId}",
181+
ex => ex == validationException);
182+
}
183+
184+
[Fact]
185+
public async Task TransformFileAsync_StagingPersisterThrowsException_ReturnsSystemValidationError()
186+
{
187+
// Arrange
188+
var validationErrors = new List<ValidationError>();
189+
var persistException = new InvalidOperationException("Database error");
190+
191+
_fileParserMock.Setup(p => p.Parse(_testStream)).Returns(parsedFile);
192+
_validationRunnerMock.Setup(v => v.Validate(parsedFile)).Returns(validationErrors);
193+
_stagingPersisterMock.Setup(s => s.WriteStagedData(parsedFile, _testMeshFile)).ThrowsAsync(persistException);
194+
195+
// Act
196+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
197+
198+
// Assert
199+
Assert.Single(result);
200+
var validationError = result[0];
201+
Assert.Equal(ErrorCodes.UnableToParseFile, validationError.Code);
202+
Assert.Equal("Unable to parse file", validationError.Error);
203+
Assert.Equal(ValidationErrorScope.File, validationError.Scope);
204+
205+
_fileParserMock.Verify(p => p.Parse(_testStream), Times.Once);
206+
_validationRunnerMock.Verify(v => v.Validate(parsedFile), Times.Once);
207+
_stagingPersisterMock.Verify(s => s.WriteStagedData(parsedFile, _testMeshFile), Times.Once);
208+
209+
_loggerMock.VerifyLogger(LogLevel.Error,
210+
$"System error occurred while parsing NBSS appointment file. File: {_testMeshFile.FileId}",
211+
ex => ex == persistException);
212+
}
213+
214+
[Theory]
215+
[InlineData(ErrorCodes.MissingFieldHeadings, "Field headings are missing")]
216+
[InlineData(ErrorCodes.UnknownRecordTypeIdentifier, "Unknown record type 'INVALID'")]
217+
[InlineData("CUSTOM001", "Custom validation error")]
218+
public async Task TransformFileAsync_DifferentFileParsingExceptions_ReturnsCorrectValidationErrors(string errorCode, string errorMessage)
219+
{
220+
// Arrange
221+
var fileParsingException = new FileParsingException(errorCode, errorMessage);
222+
_fileParserMock.Setup(p => p.Parse(_testStream)).Throws(fileParsingException);
223+
224+
// Act
225+
var result = await _fileTransformer.TransformFileAsync(_testStream, _testMeshFile);
226+
227+
// Assert
228+
Assert.Single(result);
229+
var validationError = result[0];
230+
Assert.Equal(errorCode, validationError.Code);
231+
Assert.Equal(errorMessage, validationError.Error);
232+
Assert.Equal(ValidationErrorScope.File, validationError.Scope);
233+
234+
_loggerMock.VerifyLogger(LogLevel.Error,
235+
$"File parsing failed with validation error. Code: {errorCode}, Message: {errorMessage}");
236+
}
237+
}

0 commit comments

Comments
 (0)