Add UK education verification and security fixes

Features:
- Add UK institution recognition (170+ universities)
- Add diploma mill detection (100+ blacklisted institutions)
- Add education verification service with date plausibility checks
- Add local file storage option (no Azure required)
- Add default admin user seeding on startup
- Enhance Serilog logging with file output

Security fixes:
- Fix path traversal vulnerability in LocalFileStorageService
- Fix open redirect in login endpoint (use LocalRedirect)
- Fix password validation message (12 chars, not 6)
- Fix login to use HTTP POST endpoint (avoid Blazor cookie issues)

Code improvements:
- Add CancellationToken propagation to CV parser
- Add shared helpers (JsonDefaults, DateHelpers, ScoreThresholds)
- Add IUserContextService for user ID extraction
- Parallelized company verification in ProcessCVCheckJob
- Add 28 unit tests for education verification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 16:45:43 +01:00
parent c6d52a38b2
commit f1ccd217d8
35 changed files with 1791 additions and 415 deletions

View File

@@ -18,6 +18,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
private readonly Mock<IFileStorageService> _fileStorageServiceMock;
private readonly Mock<ICVParserService> _cvParserServiceMock;
private readonly Mock<ICompanyVerifierService> _companyVerifierServiceMock;
private readonly Mock<IEducationVerifierService> _educationVerifierServiceMock;
private readonly Mock<ITimelineAnalyserService> _timelineAnalyserServiceMock;
private readonly Mock<ILogger<ProcessCVCheckJob>> _loggerMock;
private readonly ProcessCVCheckJob _sut;
@@ -37,6 +38,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
_fileStorageServiceMock = new Mock<IFileStorageService>();
_cvParserServiceMock = new Mock<ICVParserService>();
_companyVerifierServiceMock = new Mock<ICompanyVerifierService>();
_educationVerifierServiceMock = new Mock<IEducationVerifierService>();
_timelineAnalyserServiceMock = new Mock<ITimelineAnalyserService>();
_loggerMock = new Mock<ILogger<ProcessCVCheckJob>>();
@@ -45,6 +47,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
_fileStorageServiceMock.Object,
_cvParserServiceMock.Object,
_companyVerifierServiceMock.Object,
_educationVerifierServiceMock.Object,
_timelineAnalyserServiceMock.Object,
_loggerMock.Object);
}
@@ -159,7 +162,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
// Assert
_cvParserServiceMock.Verify(
x => x.ParseAsync(It.IsAny<Stream>(), "resume.pdf"),
x => x.ParseAsync(It.IsAny<Stream>(), "resume.pdf", It.IsAny<CancellationToken>()),
Times.Once);
_dbContext.ChangeTracker.Clear();
@@ -843,7 +846,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
x => x.DownloadAsync(It.IsAny<string>()),
Times.Never);
_cvParserServiceMock.Verify(
x => x.ParseAsync(It.IsAny<Stream>(), It.IsAny<string>()),
x => x.ParseAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),
Times.Never);
}
@@ -1007,6 +1010,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
private void SetupDefaultMocks(
CVData? cvData = null,
List<CompanyVerificationResult>? verificationResults = null,
List<EducationVerificationResult>? educationResults = null,
TimelineAnalysisResult? timelineResult = null)
{
cvData ??= CreateTestCVData();
@@ -1017,7 +1021,7 @@ public sealed class ProcessCVCheckJobTests : IDisposable
.ReturnsAsync(new MemoryStream());
_cvParserServiceMock
.Setup(x => x.ParseAsync(It.IsAny<Stream>(), It.IsAny<string>()))
.Setup(x => x.ParseAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(cvData);
if (verificationResults != null)
@@ -1040,6 +1044,12 @@ public sealed class ProcessCVCheckJobTests : IDisposable
.ReturnsAsync(CreateDefaultVerificationResult());
}
_educationVerifierServiceMock
.Setup(x => x.VerifyAll(
It.IsAny<List<EducationEntry>>(),
It.IsAny<List<EmploymentEntry>?>()))
.Returns(educationResults ?? []);
_timelineAnalyserServiceMock
.Setup(x => x.Analyse(It.IsAny<List<EmploymentEntry>>()))
.Returns(timelineResult);