147 lines
5.8 KiB
C#
147 lines
5.8 KiB
C#
using System.Reflection;
|
||
using JobTrackerApi.Controllers;
|
||
using JobTrackerApi.Models;
|
||
using Xunit;
|
||
|
||
namespace JobTrackerApi.Tests;
|
||
|
||
// Phase 2.1-b — CV extraction coverage. The structured model and StructuredCvProfileJson.FromSections
|
||
// already map Projects, Certifications and Languages headings into the profile; the gap was upstream
|
||
// (the AI /cv/normalize prompt never emitted those headings, so the sections were dropped). These
|
||
// tests lock the C# side so that once the normalized markdown carries # Projects / # Certifications /
|
||
// # Languages, they reach the structured profile — and so a future change can't silently regress it.
|
||
//
|
||
// Content shapes mirror what the (fixed) normalizer produces for the benchmark CV
|
||
// (Connor Babbington): a Projects section, and languages stated as "Name: Level".
|
||
public sealed class CvExtractionCoverageTests
|
||
{
|
||
private static StructuredCvSection Section(string name, string content) =>
|
||
new() { Name = name, Content = content };
|
||
|
||
[Fact]
|
||
public void FromSections_maps_a_Projects_heading_into_structured_projects()
|
||
{
|
||
var profile = StructuredCvProfileJson.FromSections(new[]
|
||
{
|
||
Section("Projects",
|
||
"JobTrack\nFull-stack job-application tracker (React, ASP.NET Core, SQLite, Docker).\n\n" +
|
||
"InboxIntel\nGmail analytics and safe bulk-cleanup tool in .NET 8 with PostgreSQL."),
|
||
});
|
||
|
||
Assert.Equal(2, profile.Projects.Count);
|
||
Assert.Contains(profile.Projects, p => p.Name == "JobTrack");
|
||
Assert.Contains(profile.Projects, p => p.Name == "InboxIntel");
|
||
}
|
||
|
||
[Fact]
|
||
public void FromSections_maps_a_Certifications_heading_into_structured_certifications()
|
||
{
|
||
var profile = StructuredCvProfileJson.FromSections(new[]
|
||
{
|
||
Section("Certifications",
|
||
"Extended Diploma NVQ Level 3 in ICT\n\nAZ-900 Azure Fundamentals"),
|
||
});
|
||
|
||
Assert.NotEmpty(profile.Certifications);
|
||
Assert.Contains(profile.Certifications, c => (c.Name ?? "").Contains("NVQ", System.StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
[Fact]
|
||
public void FromSections_maps_a_Languages_heading_with_levels()
|
||
{
|
||
var profile = StructuredCvProfileJson.FromSections(new[]
|
||
{
|
||
Section("Languages", "English: Native\nNorwegian: B1"),
|
||
});
|
||
|
||
Assert.Equal(2, profile.Languages.Count);
|
||
Assert.Contains(profile.Languages, l => l.Name == "English" && (l.Level ?? "").Contains("Native"));
|
||
Assert.Contains(profile.Languages, l => l.Name == "Norwegian" && (l.Level ?? "").Contains("B1"));
|
||
}
|
||
|
||
// The benchmark CV has all four rich sections; confirm they coexist without one clobbering another.
|
||
[Fact]
|
||
public void FromSections_populates_projects_certifications_and_languages_together()
|
||
{
|
||
var profile = StructuredCvProfileJson.FromSections(new[]
|
||
{
|
||
Section("Skills", "C#\n.NET\nDocker"),
|
||
Section("Projects", "JobTrack\nJob-application tracker."),
|
||
Section("Certifications", "NVQ Level 3 in ICT"),
|
||
Section("Languages", "English: Native\nNorwegian: B1"),
|
||
});
|
||
|
||
Assert.NotEmpty(profile.Skills);
|
||
Assert.Single(profile.Projects);
|
||
Assert.NotEmpty(profile.Certifications);
|
||
Assert.Equal(2, profile.Languages.Count);
|
||
}
|
||
[Fact]
|
||
public void Normalized_markdown_strips_skill_group_labels()
|
||
{
|
||
var profile = InvokeProfileBuilder("""
|
||
# Skills
|
||
Development: C#
|
||
DevOps & Infrastructure: Docker
|
||
Practices: CI/CD
|
||
""");
|
||
|
||
Assert.Equal(new[] { "C#", "CI/CD", "Docker" }, profile.Skills);
|
||
Assert.DoesNotContain(profile.Skills, skill => skill.StartsWith("Development", System.StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
[Fact]
|
||
public void Normalized_markdown_separates_glued_dates_from_job_title()
|
||
{
|
||
var profile = InvokeProfileBuilder("""
|
||
# Work Experience
|
||
2015–2023System Developer
|
||
Warwickshire County Council, UK
|
||
- Built APIs
|
||
""");
|
||
|
||
var job = Assert.Single(profile.Jobs);
|
||
Assert.Equal("2015", job.Start);
|
||
Assert.Equal("2023", job.End);
|
||
Assert.Contains("System Developer", job.Title ?? string.Empty);
|
||
}
|
||
|
||
[Fact]
|
||
public void Extraction_repairs_common_utf8_as_latin1_mojibake()
|
||
{
|
||
var method = typeof(ProfileCvController).GetMethod("RepairKnownMojibake", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||
var repaired = Assert.IsType<string>(method.Invoke(null, new object[] { "Tønsberg 2015–2023" }));
|
||
|
||
Assert.Equal("Tønsberg 2015–2023", repaired);
|
||
}
|
||
|
||
[Fact]
|
||
public void Earlier_part_time_roles_become_separate_experience_entries()
|
||
{
|
||
var profile = InvokeProfileBuilder("""
|
||
# Work Experience
|
||
System Developer
|
||
Warwickshire County Council
|
||
2015–2023
|
||
- Built APIs
|
||
|
||
Earlier roles (part-time)
|
||
- Sales Assistant — Royal Vapes | 2013–2015
|
||
- Labourer — The Hodcarrier | 2014–2016
|
||
- Lifeguard — Nuffield Health | 2012–2014
|
||
""");
|
||
|
||
Assert.Contains(profile.Jobs, job => job.Title == "Sales Assistant" && job.Company == "Royal Vapes");
|
||
Assert.Contains(profile.Jobs, job => job.Title == "Labourer" && job.Company == "The Hodcarrier");
|
||
Assert.Contains(profile.Jobs, job => job.Title == "Lifeguard" && job.Company == "Nuffield Health");
|
||
Assert.Equal(4, profile.Jobs.Count);
|
||
}
|
||
|
||
private static StructuredCvProfile InvokeProfileBuilder(string markdown)
|
||
{
|
||
var method = typeof(ProfileCvController).GetMethod("BuildStructuredCvFromNormalizedMarkdown", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||
return Assert.IsType<StructuredCvProfile>(method.Invoke(null, new object[] { markdown }));
|
||
}
|
||
|
||
}
|