using System.Reflection; using System.IO.Compression; using JobTrackerApi.Controllers; using JobTrackerApi.Models; using Microsoft.AspNetCore.Http; 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(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); } [Fact] public void Numbered_editorial_headings_are_canonicalized_without_personal_fixture_data() { var profile = InvokeHeuristicBuilder(""" 01 — PROFESSIONAL SUMMARY Backend engineer focused on reliable services and clear delivery. 02 — CORE COMPETENCIES C# Docker 03 — PROFESSIONAL EXPERIENCE Software Engineer Example Council 2020–Present - Built maintainable APIs """); Assert.NotEmpty(profile.Summary); Assert.Contains("C#", profile.Skills); Assert.Contains(profile.Jobs, job => job.Title == "Software Engineer"); } [Fact] public void Unknown_markdown_sections_are_preserved_for_import_review() { var profile = InvokeHeuristicBuilder(""" # Professional Summary Engineer with broad delivery experience. # Community Leadership Mentored early-career developers Organised a monthly meetup """); var custom = Assert.Single(profile.OtherSections, section => section.Title == "Community Leadership"); Assert.Contains(custom.Items, item => item.Contains("Mentored", System.StringComparison.Ordinal)); } [Fact] public void Contact_detection_uses_the_location_segment_and_rejects_section_headings() { var profile = InvokeHeuristicBuilder(""" Alex Example Platform Engineer Tønsberg, Norway · +47 412 34 567 · alex.example@example.test portfolio.example.dev · linkedin.com/in/alex-example Core Competencies Backend Development · System Design """); Assert.Equal("Tønsberg, Norway", profile.Contact.Location); Assert.Equal("portfolio.example.dev", profile.Contact.Website); } [Fact] public void Norwegian_section_headings_are_canonicalized() { var profile = InvokeHeuristicBuilder(""" Sammendrag Utvikler med erfaring fra robuste tjenester. Teknisk kompetanse C# Docker Arbeidserfaring Systemutvikler Eksempelkommune 2020–Present - Bygget pålitelige tjenester Utdanning Bachelor i informatikk Eksempelhøgskolen 2017–2020 Språk Norsk: B2 English: Native """); Assert.NotEmpty(profile.Summary); Assert.Contains("C#", profile.Skills); Assert.NotEmpty(profile.Jobs); Assert.NotEmpty(profile.Education); Assert.Equal(2, profile.Languages.Count); } [Fact] public void Norwegian_inline_language_pairs_keep_their_own_proficiency() { var profile = InvokeHeuristicBuilder(""" Språk Engelsk — morsmål — Norsk — A2/B1, under aktiv utvikling mot B2 """); Assert.Contains(profile.Languages, language => language.Name == "English" && language.Level == "Native"); Assert.Contains(profile.Languages, language => language.Name == "Norwegian" && language.Level == "A2/B1"); } [Fact] public async Task Docx_fallback_preserves_paragraph_and_table_boundaries() { await using var package = new MemoryStream(); using (var archive = new ZipArchive(package, ZipArchiveMode.Create, leaveOpen: true)) { var entry = archive.CreateEntry("word/document.xml"); await using var stream = entry.Open(); await using var writer = new StreamWriter(stream); await writer.WriteAsync(""" Technical Skills BackendC#, .NET DevOpsDocker, Linux Example Engineer Built reliable services """); } package.Position = 0; var upload = new FormFile(package, 0, package.Length, "file", "sanitized.docx"); var method = typeof(ProfileCvController).GetMethod("ExtractTextAsync", BindingFlags.NonPublic | BindingFlags.Static)!; var task = Assert.IsAssignableFrom>(method.Invoke(null, new object[] { upload, ".docx" })); var extracted = await task; Assert.Contains("Technical Skills\n", extracted); Assert.Contains("Backend | C#, .NET", extracted); Assert.Contains("DevOps | Docker, Linux", extracted); Assert.Contains("\n\nExample Engineer\n- Built reliable services", extracted); } private static StructuredCvProfile InvokeProfileBuilder(string markdown) { var method = typeof(ProfileCvController).GetMethod("BuildStructuredCvFromNormalizedMarkdown", BindingFlags.NonPublic | BindingFlags.Static)!; return Assert.IsType(method.Invoke(null, new object[] { markdown })); } private static StructuredCvProfile InvokeHeuristicBuilder(string text) { var method = typeof(ProfileCvController).GetMethod("BuildHeuristicStructuredCv", BindingFlags.NonPublic | BindingFlags.Static)!; return Assert.IsType(method.Invoke(null, new object[] { text, text })); } }