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:
418
tests/TrueCV.Tests/Services/EducationVerifierServiceTests.cs
Normal file
418
tests/TrueCV.Tests/Services/EducationVerifierServiceTests.cs
Normal file
@@ -0,0 +1,418 @@
|
||||
using FluentAssertions;
|
||||
using TrueCV.Application.Models;
|
||||
using TrueCV.Infrastructure.Services;
|
||||
|
||||
namespace TrueCV.Tests.Services;
|
||||
|
||||
public sealed class EducationVerifierServiceTests
|
||||
{
|
||||
private readonly EducationVerifierService _sut = new();
|
||||
|
||||
#region Diploma Mill Detection
|
||||
|
||||
[Theory]
|
||||
[InlineData("Belford University")]
|
||||
[InlineData("Ashwood University")]
|
||||
[InlineData("Rochville University")]
|
||||
[InlineData("St Regis University")]
|
||||
public void Verify_DiplomaMillInstitution_ReturnsDiplomaMill(string institution)
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = institution,
|
||||
Qualification = "PhD",
|
||||
Subject = "Business",
|
||||
StartDate = new DateOnly(2020, 1, 1),
|
||||
EndDate = new DateOnly(2020, 6, 1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be("DiplomaMill");
|
||||
result.IsDiplomaMill.Should().BeTrue();
|
||||
result.IsSuspicious.Should().BeTrue();
|
||||
result.IsVerified.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_DiplomaMillInstitution_IncludesVerificationNotes()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "Belford University",
|
||||
Qualification = "MBA"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.VerificationNotes.Should().Contain("diploma mill blacklist");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Suspicious Pattern Detection
|
||||
|
||||
[Theory]
|
||||
[InlineData("Global Online University")]
|
||||
[InlineData("Premier University of Excellence")]
|
||||
[InlineData("Executive Virtual University")]
|
||||
public void Verify_SuspiciousPatternInstitution_ReturnsSuspicious(string institution)
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = institution
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be("Suspicious");
|
||||
result.IsSuspicious.Should().BeTrue();
|
||||
result.IsDiplomaMill.Should().BeFalse();
|
||||
result.IsVerified.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UK Institution Recognition
|
||||
|
||||
[Theory]
|
||||
[InlineData("University of Cambridge", "University of Cambridge")]
|
||||
[InlineData("Cambridge", "University of Cambridge")]
|
||||
[InlineData("University of Oxford", "University of Oxford")]
|
||||
[InlineData("Oxford", "University of Oxford")]
|
||||
[InlineData("Imperial College London", "Imperial College London")]
|
||||
[InlineData("UCL", "UCL")] // UCL is directly in the recognised list
|
||||
[InlineData("LSE", "LSE")] // LSE is directly in the recognised list
|
||||
public void Verify_RecognisedUKInstitution_ReturnsRecognised(string input, string expectedMatch)
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = input,
|
||||
Qualification = "BSc",
|
||||
StartDate = new DateOnly(2018, 9, 1),
|
||||
EndDate = new DateOnly(2021, 6, 1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be("Recognised");
|
||||
result.IsVerified.Should().BeTrue();
|
||||
result.IsDiplomaMill.Should().BeFalse();
|
||||
result.IsSuspicious.Should().BeFalse();
|
||||
result.MatchedInstitution.Should().Be(expectedMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_RecognisedInstitution_IncludesVerificationNotes()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Manchester"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.VerificationNotes.Should().Contain("Verified UK higher education institution");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_RecognisedInstitutionVariation_NotesMatchedName()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "Cambridge"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.VerificationNotes.Should().Contain("Matched to official name");
|
||||
result.MatchedInstitution.Should().Be("University of Cambridge");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unknown Institutions
|
||||
|
||||
[Fact]
|
||||
public void Verify_UnknownInstitution_ReturnsUnknown()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Ljubljana",
|
||||
Qualification = "BSc"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be("Unknown");
|
||||
result.IsVerified.Should().BeFalse();
|
||||
result.IsDiplomaMill.Should().BeFalse();
|
||||
result.IsSuspicious.Should().BeFalse();
|
||||
result.VerificationNotes.Should().Contain("international institution");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Date Plausibility
|
||||
|
||||
[Fact]
|
||||
public void Verify_PlausibleDates_ReturnsPlausible()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2018, 9, 1),
|
||||
EndDate = new DateOnly(2021, 6, 1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.DatesArePlausible.Should().BeTrue();
|
||||
result.DatePlausibilityNotes.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_TooShortCourseDuration_ReturnsImplausible()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2020, 1, 1),
|
||||
EndDate = new DateOnly(2020, 6, 1) // 6 months
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.DatesArePlausible.Should().BeFalse();
|
||||
result.DatePlausibilityNotes.Should().Contain("unusually short");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_TooLongCourseDuration_ReturnsImplausible()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2010, 1, 1),
|
||||
EndDate = new DateOnly(2020, 1, 1) // 10 years
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.DatesArePlausible.Should().BeFalse();
|
||||
result.DatePlausibilityNotes.Should().Contain("unusually long");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_EndDateBeforeStartDate_ReturnsImplausible()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2021, 1, 1),
|
||||
EndDate = new DateOnly(2020, 1, 1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.DatesArePlausible.Should().BeFalse();
|
||||
result.DatePlausibilityNotes.Should().Contain("before or equal to start date");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_NoDates_AssumesPlausible()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Bristol"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.DatesArePlausible.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region VerifyAll
|
||||
|
||||
[Fact]
|
||||
public void VerifyAll_MultipleEducations_ReturnsResultsForEach()
|
||||
{
|
||||
// Arrange
|
||||
var educations = new List<EducationEntry>
|
||||
{
|
||||
new() { Institution = "University of Cambridge" },
|
||||
new() { Institution = "Belford University" },
|
||||
new() { Institution = "Unknown Foreign University" }
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.VerifyAll(educations);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(3);
|
||||
results[0].Status.Should().Be("Recognised");
|
||||
results[1].Status.Should().Be("DiplomaMill");
|
||||
results[2].Status.Should().Be("Unknown");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyAll_OverlappingEducation_NotesOverlap()
|
||||
{
|
||||
// Arrange
|
||||
var educations = new List<EducationEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2018, 9, 1),
|
||||
EndDate = new DateOnly(2021, 6, 1)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Institution = "University of Bath",
|
||||
StartDate = new DateOnly(2020, 9, 1),
|
||||
EndDate = new DateOnly(2023, 6, 1)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.VerifyAll(educations);
|
||||
|
||||
// Assert
|
||||
results[0].DatePlausibilityNotes.Should().Contain("Overlaps with");
|
||||
results[1].DatePlausibilityNotes.Should().Contain("Overlaps with");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyAll_EmploymentBeforeGraduation_ChecksTimeline()
|
||||
{
|
||||
// Arrange
|
||||
var educations = new List<EducationEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2018, 9, 1),
|
||||
EndDate = new DateOnly(2021, 6, 1)
|
||||
}
|
||||
};
|
||||
|
||||
var employment = new List<EmploymentEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
CompanyName = "Tech Corp",
|
||||
JobTitle = "Senior Developer",
|
||||
StartDate = new DateOnly(2018, 1, 1) // Started before education started
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.VerifyAll(educations, employment);
|
||||
|
||||
// Assert
|
||||
results[0].DatesArePlausible.Should().BeFalse();
|
||||
results[0].DatePlausibilityNotes.Should().Contain("months before claimed graduation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyAll_InternshipBeforeGraduation_AllowsTimeline()
|
||||
{
|
||||
// Arrange
|
||||
var educations = new List<EducationEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
StartDate = new DateOnly(2018, 9, 1),
|
||||
EndDate = new DateOnly(2021, 6, 1)
|
||||
}
|
||||
};
|
||||
|
||||
var employment = new List<EmploymentEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
CompanyName = "Tech Corp",
|
||||
JobTitle = "Software Intern",
|
||||
StartDate = new DateOnly(2019, 6, 1)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = _sut.VerifyAll(educations, employment);
|
||||
|
||||
// Assert
|
||||
// Should be plausible because it's an internship
|
||||
results[0].DatesArePlausible.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data Preservation
|
||||
|
||||
[Fact]
|
||||
public void Verify_PreservesAllClaimedData()
|
||||
{
|
||||
// Arrange
|
||||
var education = new EducationEntry
|
||||
{
|
||||
Institution = "University of Bristol",
|
||||
Qualification = "BSc Computer Science",
|
||||
Subject = "Computer Science",
|
||||
Grade = "First Class Honours",
|
||||
StartDate = new DateOnly(2018, 9, 1),
|
||||
EndDate = new DateOnly(2021, 6, 1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _sut.Verify(education);
|
||||
|
||||
// Assert
|
||||
result.ClaimedInstitution.Should().Be("University of Bristol");
|
||||
result.ClaimedQualification.Should().Be("BSc Computer Science");
|
||||
result.ClaimedSubject.Should().Be("Computer Science");
|
||||
result.ClaimedStartDate.Should().Be(new DateOnly(2018, 9, 1));
|
||||
result.ClaimedEndDate.Should().Be(new DateOnly(2021, 6, 1));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user