feat(cv): extend templates and extraction

This commit is contained in:
cesnimda
2026-08-27 15:27:48 +02:00
parent cc76f86482
commit ccd0af908c
15 changed files with 586 additions and 54 deletions
@@ -1,6 +1,8 @@
using System.Reflection;
using System.IO.Compression;
using JobTrackerApi.Controllers;
using JobTrackerApi.Models;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace JobTrackerApi.Tests;
@@ -137,10 +139,150 @@ public sealed class CvExtractionCoverageTests
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
2020Present
- 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
2020Present
- Bygget pålitelige tjenester
Utdanning
Bachelor i informatikk
Eksempelhøgskolen
20172020
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("""
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>
<w:p><w:r><w:t>Technical Skills</w:t></w:r></w:p>
<w:tbl>
<w:tr><w:tc><w:p><w:r><w:t>Backend</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>C#, .NET</w:t></w:r></w:p></w:tc></w:tr>
<w:tr><w:tc><w:p><w:r><w:t>DevOps</w:t></w:r></w:p></w:tc><w:tc><w:p><w:r><w:t>Docker, Linux</w:t></w:r></w:p></w:tc></w:tr>
</w:tbl>
<w:p><w:pPr><w:pStyle w:val="CVRole" /></w:pPr><w:r><w:t>Example Engineer</w:t></w:r></w:p>
<w:p><w:pPr><w:pStyle w:val="CVBullet" /></w:pPr><w:r><w:t>Built reliable services</w:t></w:r></w:p>
</w:body></w:document>
""");
}
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<Task<string>>(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<StructuredCvProfile>(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<StructuredCvProfile>(method.Invoke(null, new object[] { text, text }));
}
}