2026-01-20 16:45:43 +01:00
|
|
|
using FluentAssertions;
|
2026-01-22 20:47:55 +00:00
|
|
|
using RealCV.Application.Models;
|
|
|
|
|
using RealCV.Infrastructure.Services;
|
2026-01-20 16:45:43 +01:00
|
|
|
|
2026-01-22 20:47:55 +00:00
|
|
|
namespace RealCV.Tests.Services;
|
2026-01-20 16:45:43 +01:00
|
|
|
|
|
|
|
|
public sealed class EducationVerifierServiceTests
|
|
|
|
|
{
|
|
|
|
|
private readonly EducationVerifierService _sut = new();
|
|
|
|
|
|
2026-01-25 04:52:12 +00:00
|
|
|
#region Unaccredited Institution Detection
|
2026-01-20 16:45:43 +01:00
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("Belford University")]
|
|
|
|
|
[InlineData("Ashwood University")]
|
|
|
|
|
[InlineData("Rochville University")]
|
|
|
|
|
[InlineData("St Regis University")]
|
2026-01-25 04:52:12 +00:00
|
|
|
public void Verify_UnaccreditedInstitution_ReturnsUnaccredited(string institution)
|
2026-01-20 16:45:43 +01:00
|
|
|
{
|
|
|
|
|
// 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
|
2026-01-25 04:52:12 +00:00
|
|
|
result.Status.Should().Be("Unaccredited");
|
|
|
|
|
result.IsUnaccredited.Should().BeTrue();
|
2026-01-20 16:45:43 +01:00
|
|
|
result.IsSuspicious.Should().BeTrue();
|
|
|
|
|
result.IsVerified.Should().BeFalse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-01-25 04:52:12 +00:00
|
|
|
public void Verify_UnaccreditedInstitution_IncludesVerificationNotes()
|
2026-01-20 16:45:43 +01:00
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var education = new EducationEntry
|
|
|
|
|
{
|
|
|
|
|
Institution = "Belford University",
|
|
|
|
|
Qualification = "MBA"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = _sut.Verify(education);
|
|
|
|
|
|
|
|
|
|
// Assert
|
2026-01-25 04:52:12 +00:00
|
|
|
result.VerificationNotes.Should().Contain("QAA/HESA register");
|
2026-01-20 16:45:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#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();
|
2026-01-25 04:52:12 +00:00
|
|
|
result.IsUnaccredited.Should().BeFalse();
|
2026-01-20 16:45:43 +01:00
|
|
|
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();
|
2026-01-25 04:52:12 +00:00
|
|
|
result.IsUnaccredited.Should().BeFalse();
|
2026-01-20 16:45:43 +01:00
|
|
|
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();
|
2026-01-25 04:52:12 +00:00
|
|
|
result.IsUnaccredited.Should().BeFalse();
|
2026-01-20 16:45:43 +01:00
|
|
|
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");
|
2026-01-25 04:52:12 +00:00
|
|
|
results[1].Status.Should().Be("Unaccredited");
|
2026-01-20 16:45:43 +01:00
|
|
|
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
|
|
|
|
|
}
|