using JobTrackerApi.Services; using Xunit; namespace JobTrackerApi.Tests; public sealed class JobCvMatchServiceTests { private readonly JobCvMatchService _service = new(); private static Dictionary Sections(params (string Name, string Text)[] items) => items.ToDictionary(i => i.Name, i => i.Text, StringComparer.OrdinalIgnoreCase); public static IEnumerable QualityFixtures() { yield return new object[] { "Norwegian", "Senior systemutvikler", "Vi ser etter deg som har erfaring med C# og ASP.NET Core. Du vil designe skalerbare distribuerte systemer. Gode samarbeidsevner er en fordel.", new[] { "C#", "ASP.NET Core", "designe skalerbare distribuerte systemer" }, new[] { "med", "til", "for", "som", "erfaring", "ser" }, }; yield return new object[] { "English", "Cloud platform engineer", "We need a candidate with experience in AWS and Terraform. You will lead incident response and operate distributed systems.", new[] { "AWS", "Terraform", "incident response", "operate distributed systems" }, new[] { "the", "with", "experience", "candidate", "will" }, }; yield return new object[] { "Mixed Norwegian and English", "DevOps-utvikler", "Du vil jobbe med Node.js og CI/CD. Work closely with Azure DevOps and cross-functional product teams.", new[] { "Node.js", "CI/CD", "Azure DevOps", "Collaboration" }, new[] { "og", "med", "with", "teams" }, }; yield return new object[] { "Short", "Data developer", "Python and SQL.", new[] { "Python", "SQL" }, new[] { "and" }, }; yield return new object[] { "Noisy HTML", "Frontend developer", "
Build accessible web applications with React and TypeScript.
", new[] { "React", "TypeScript", "Build accessible web applications" }, new[] { "home", "jobs", "login", "cookie", "settings", "privacy", "terms", "trackingcookie" }, }; yield return new object[] { "Technology heavy", "Platform developer", "C++, C#, .NET, ASP.NET Core, Node.js, CI/CD, Azure DevOps and Kubernetes.", new[] { "C++", "C#", ".NET", "ASP.NET Core", "Node.js", "CI/CD", "Azure DevOps", "Kubernetes" }, new[] { "and" }, }; yield return new object[] { "Repeated recruitment filler", "Software engineer", "Exciting opportunity for a passionate candidate. Great opportunity, strong experience required. We offer an exciting dynamic environment. Apply now. Build services using domain-driven design and Docker.", new[] { "domain-driven design", "Docker" }, new[] { "exciting", "opportunity", "passionate", "candidate", "experience", "environment", "apply" }, }; } [Theory] [MemberData(nameof(QualityFixtures))] public void Quality_fixtures_keep_useful_terms_and_suppress_noise( string name, string title, string description, string[] expected, string[] excluded) { var result = _service.Evaluate(title, description, Sections(("Skills", "synthetic profile text"))); var terms = result.MatchedKeywords.Concat(result.MissingKeywords).ToList(); foreach (var term in expected) Assert.True(terms.Contains(term, StringComparer.OrdinalIgnoreCase), $"{name}: expected '{term}' in [{string.Join(", ", terms)}]"); foreach (var term in excluded) Assert.False(terms.Contains(term, StringComparer.OrdinalIgnoreCase), $"{name}: did not expect '{term}' in [{string.Join(", ", terms)}]"); } [Fact] public void Strong_overlap_scores_high_and_lists_matched_keywords() { var result = _service.Evaluate( jobTitle: "Senior C# Backend Developer", jobText: "We need a backend engineer with strong C#, .NET, SQL and Docker experience building REST APIs.", cvSections: Sections( ("Skills", "C# .NET SQL Docker Kubernetes"), ("Experience", "Built REST APIs in C# and .NET with SQL Server and Docker."))); Assert.True(result.Score >= 75, $"expected strong score, got {result.Score}"); Assert.Equal("Strong", result.Band); Assert.Contains("C#", result.MatchedKeywords); Assert.Contains(".NET", result.MatchedKeywords); Assert.True(result.HasEnoughSignal); } [Fact] public void No_overlap_scores_low_and_surfaces_missing_keywords() { var result = _service.Evaluate( jobTitle: "Kubernetes Platform Engineer", jobText: "Deep Kubernetes, AWS, and Docker platform experience required. Terraform and CI/CD pipelines.", cvSections: Sections( ("Skills", "Graphic design, Adobe Photoshop, Illustrator, copywriting"), ("Experience", "Ran marketing campaigns and brand design work."))); Assert.True(result.Score < 50, $"expected low score, got {result.Score}"); Assert.Equal("Low", result.Band); Assert.Contains("Kubernetes", result.MissingKeywords); Assert.Contains("AWS", result.MissingKeywords); } [Fact] public void Is_deterministic_for_identical_inputs() { var a = _service.Evaluate("Data Engineer", "Python, SQL, Spark, ETL pipelines and AWS.", Sections(("Skills", "Python SQL AWS"))); var b = _service.Evaluate("Data Engineer", "Python, SQL, Spark, ETL pipelines and AWS.", Sections(("Skills", "Python SQL AWS"))); Assert.Equal(a.Score, b.Score); Assert.Equal(a.MatchedKeywords, b.MatchedKeywords); Assert.Equal(a.MissingKeywords, b.MissingKeywords); } [Fact] public void Word_boundary_prevents_false_substring_matches() { // "go" (the language) must not match inside "goals"/"ago". var result = _service.Evaluate( jobTitle: "Go Developer", jobText: "Go programming language, goroutines, concurrency.", cvSections: Sections(("Experience", "Achieved company goals two years ago in a great environment."))); Assert.DoesNotContain("go", result.MatchedKeywords, StringComparer.OrdinalIgnoreCase); } [Fact] public void Section_coverage_reports_where_matches_are_concentrated() { var result = _service.Evaluate( jobTitle: "React Frontend Engineer", jobText: "Build UIs with React, TypeScript and JavaScript. Strong testing culture.", cvSections: Sections( ("Skills", "React TypeScript JavaScript"), ("Experience", "Wrote documentation and managed budgets."))); var skills = Assert.Single(result.SectionCoverage, s => s.Section == "Skills"); var experience = Assert.Single(result.SectionCoverage, s => s.Section == "Experience"); Assert.True(skills.Matched > experience.Matched); } [Fact] public void Empty_cv_reports_no_signal() { var result = _service.Evaluate("Anything", "Some role text with several words here.", Sections()); Assert.False(result.HasEnoughSignal); Assert.Equal("Unknown", result.Band); Assert.Equal(0, result.MatchedCount); } [Fact] public void Curated_tag_matches_synonym_spelling_in_cv() { // Job posting says "Kubernetes"; CV only says "K8s" -- same skill, different spelling. var result = _service.Evaluate( jobTitle: "Platform Engineer", jobText: "Deep Kubernetes experience required for our platform team.", cvSections: Sections(("Skills", "K8s, Terraform, Helm"))); Assert.Contains("Kubernetes", result.MatchedKeywords); Assert.DoesNotContain("Kubernetes", result.MissingKeywords); } [Fact] public void Title_keywords_are_weighted_and_missing_ones_rank_first() { // The title term "kubernetes" is absent from the CV; it should lead the missing list // because title terms carry the title bonus weight. var result = _service.Evaluate( jobTitle: "Kubernetes Specialist", jobText: "Kubernetes orchestration. Some familiarity with logging and monitoring dashboards.", cvSections: Sections(("Skills", "logging monitoring dashboards"))); Assert.Equal("Kubernetes", result.MissingKeywords.First()); } }