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); [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 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()); } }