Refactor backend project and tighten CV test coverage

This commit is contained in:
2026-04-01 10:42:55 +02:00
parent 44000f96f2
commit 18d1de45cb
9 changed files with 246 additions and 19 deletions
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@@ -14,14 +14,12 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.2" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.14" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\JobTrackerApi\JobTrackerApi.csproj" />
<ProjectReference Include="..\JobTrackerBackend\JobTrackerBackend.csproj" />
</ItemGroup>
</Project>
@@ -580,6 +580,81 @@ public sealed class ProfileCvControllerTests
Assert.Equal("Connor Babbington", structured.Contact.FullName);
}
[Fact]
public async Task Parse_uses_classifier_fallback_when_plain_text_has_no_real_sections()
{
var source = "Senior Platform Engineer at Atlas Systems\nOslo\n2019 - Present\nBuilt event-driven APIs and migration tooling.\n\nPython\nSQL\nAzure";
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
var userManager = CreateUserManager();
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
var aiService = new Mock<ISummarizerService>();
aiService
.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), source, 3200, 900))
.ReturnsAsync("not-json");
var classifier = new Mock<ICvAiClassifier>();
classifier
.Setup(x => x.ClassifyBlockAsync(It.Is<string>(block => block.Contains("Atlas Systems", StringComparison.Ordinal)), It.IsAny<CancellationToken>()))
.ReturnsAsync(new CvBlockClassificationResult("Work Experience", 0.93, "job block", "Senior Platform Engineer", "Atlas Systems", "Oslo", "2019", "Present", new List<string> { "Built event-driven APIs and migration tooling." }));
classifier
.Setup(x => x.ClassifyBlockAsync(It.Is<string>(block => block.Contains("Python", StringComparison.Ordinal)), It.IsAny<CancellationToken>()))
.ReturnsAsync(new CvBlockClassificationResult("Skills", 0.88, "skills block", null, null, null, null, null, new List<string>()));
await using var db = CreateDb();
var paths = CreatePaths();
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
var ok = Assert.IsType<OkObjectResult>(result.Result);
var json = JsonSerializer.Serialize(ok.Value);
Assert.Contains("Senior Platform Engineer", json);
Assert.Contains("Atlas Systems", json);
var structured = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
var matchedJob = structured.Jobs.FirstOrDefault(job => job.Title == "Senior Platform Engineer");
Assert.NotNull(matchedJob);
Assert.Contains("Atlas Systems", matchedJob!.Company ?? string.Empty, StringComparison.Ordinal);
Assert.Contains("Python", structured.Skills);
Assert.Contains("SQL", structured.Skills);
classifier.Verify(x => x.ClassifyBlockAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
}
[Fact]
public async Task Parse_keeps_general_fallback_when_classifier_returns_nothing()
{
var source = "Independent consultant building internal platforms for public-sector clients across data, delivery, and migration work.";
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
var userManager = CreateUserManager();
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
var aiService = new Mock<ISummarizerService>();
aiService
.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), source, 3200, 900))
.ReturnsAsync("not-json");
var classifier = new Mock<ICvAiClassifier>();
classifier
.Setup(x => x.ClassifyBlockAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((CvBlockClassificationResult?)null);
await using var db = CreateDb();
var paths = CreatePaths();
var controller = CreateController(userManager.Object, aiService.Object, db, paths, classifier.Object);
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
var ok = Assert.IsType<OkObjectResult>(result.Result);
var json = JsonSerializer.Serialize(ok.Value);
Assert.Contains("Professional Summary", json);
var structured = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
Assert.Contains(structured.Summary, item => item.Contains("Independent consultant", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(user.ProfileCvStructureJson);
classifier.Verify(x => x.ClassifyBlockAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.AtLeastOnce());
}
[Fact]
public async Task Upload_accepts_markdown_cv_and_saves_text()
{
@@ -623,9 +698,9 @@ public sealed class ProfileCvControllerTests
Assert.Equal("Connor Babbington", StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson).Contact.FullName);
}
private static ProfileCvController CreateController(UserManager<ApplicationUser> userManager, ISummarizerService aiService, JobTrackerContext db, AppPaths paths)
private static ProfileCvController CreateController(UserManager<ApplicationUser> userManager, ISummarizerService aiService, JobTrackerContext db, AppPaths paths, ICvAiClassifier? cvAiClassifier = null)
{
return new ProfileCvController(userManager, aiService, db, paths)
return new ProfileCvController(userManager, aiService, db, paths, cvAiClassifier ?? NoOpCvAiClassifier.Instance)
{
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
};