refactor, security updates, cv extraction upgrades
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
@@ -134,6 +135,7 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser { Id = "user-1", CurrentCvProfileVersion = 1 };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.FindByIdAsync("user-1")).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
@@ -176,7 +178,12 @@ public sealed class ProfileCvControllerTests
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
||||
var result = await controller.Reprocess();
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
var accepted = Assert.IsType<AcceptedResult>(result);
|
||||
var queuedRun = await db.CvExtractionRuns.SingleAsync();
|
||||
Assert.Equal("queued", queuedRun.Status);
|
||||
|
||||
await controller.ProcessQueuedRunAsync(queuedRun.Id, CancellationToken.None);
|
||||
|
||||
var run = await db.CvExtractionRuns.SingleAsync();
|
||||
Assert.Equal("reprocess", run.Trigger);
|
||||
Assert.Equal("applied", run.Status);
|
||||
@@ -276,6 +283,55 @@ public sealed class ProfileCvControllerTests
|
||||
Assert.Contains(structured.Sections, section => section.Name == "Education");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Upload_uses_ai_normalizer_fallback_when_flattened_text_stays_low_structure()
|
||||
{
|
||||
var rawExtraction = "connor.babbington@cesnimda.co.uk cesnimda.co.uk +47 41 33 44 70 E X P E R I E N C E S Y S T E M D E V E L O P E R 2015 - 2023 Developed and maintained multiple full-stack applications using C#, Python, Ruby on Rails, SQL, and JavaScript. + Warwickshire County Council, UK";
|
||||
|
||||
var user = new ApplicationUser { Id = "user-1" };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
.Setup(x => x.ExtractTextAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AiTextExtractionResult(rawExtraction, false, "application/pdf", 1, rawExtraction.Length, "Resume.en.pdf"));
|
||||
aiService
|
||||
.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Reconstruct this CV text extracted from a PDF", StringComparison.Ordinal)), rawExtraction, 2800, 900))
|
||||
.ReturnsAsync(string.Empty);
|
||||
aiService
|
||||
.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
var normalizer = new Mock<ICvAiNormalizer>();
|
||||
normalizer
|
||||
.Setup(x => x.NormalizeAsync(It.Is<string>(text => text.Contains("Warwickshire County Council", StringComparison.Ordinal)), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CvNormalizationResult(
|
||||
0.91,
|
||||
"Recovered structured sections from flattened OCR text",
|
||||
"# Contact\nConnor Babbington\nconnor.babbington@cesnimda.co.uk\n+47 41 33 44 70\ncesnimda.co.uk\n\n# Professional Summary\nMid-level system developer with eight years of experience in UK local government.\n\n# Work Experience\nSystem Developer\nWarwickshire County Council, UK\n2015 - 2023\n- Developed and maintained multiple full-stack applications using C#, Python, Ruby on Rails, SQL, and JavaScript.\n\n# Skills\nC#\nPython\nRuby on Rails\nSQL\nJavaScript"));
|
||||
|
||||
await using var db = CreateDb();
|
||||
var paths = CreatePaths();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths, null, normalizer.Object);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes("fake pdf bytes");
|
||||
var file = new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", "Resume.en.pdf")
|
||||
{
|
||||
Headers = new HeaderDictionary(),
|
||||
ContentType = "application/pdf"
|
||||
};
|
||||
|
||||
var result = await controller.Upload(file);
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
normalizer.Verify(x => x.NormalizeAsync(It.Is<string>(text => text.Contains("Warwickshire County Council", StringComparison.Ordinal)), It.IsAny<CancellationToken>()), Times.Once);
|
||||
var structured = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal("Connor Babbington", structured.Contact.FullName);
|
||||
Assert.Contains("# Skills", user.ProfileCvText ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Warwickshire County Council", user.ProfileCvText ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Upload_populates_structured_fields_from_flattened_cv_when_ai_json_is_invalid()
|
||||
{
|
||||
@@ -790,9 +846,248 @@ public sealed class ProfileCvControllerTests
|
||||
Assert.Equal("Connor Babbington", StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson).Contact.FullName);
|
||||
}
|
||||
|
||||
private static ProfileCvController CreateController(UserManager<ApplicationUser> userManager, ISummarizerService aiService, JobTrackerContext db, AppPaths paths, ICvAiClassifier? cvAiClassifier = null)
|
||||
[Fact]
|
||||
public void Normalized_markdown_parse_preserves_real_estate_job_and_language_levels()
|
||||
{
|
||||
return new ProfileCvController(userManager, aiService, db, paths, null, cvAiClassifier ?? NoOpCvAiClassifier.Instance)
|
||||
var normalized = "# Contact\nAvery Cooper\n(415) 223-4344\nhttps://www.linkedin.com/in/avery-cooper/\nhttps://www.realtor.com/realestateagents/avery-copper/\nSan Francisco\n\n# Professional Summary\nDynamic real estate professional with 12 years of experience in residential and commercial property.\n\n# Work Experience\nReal Estate Agent\nEleanor Lane Agency, White Plains\nJuly 2017 - Present\n- Managed all aspects of the sales process from preparation to close, achieving a 25% increase in closed deals compared to previous periods.\n- Successfully negotiated favorable terms for clients in over 50 real estate transactions, consistently securing above-market value.\n\nReal Estate Assistant\nHathaway Properties, New Rochelle\nOctober 2012 - June 2017\n- Managed administrative tasks in a fast-paced real estate office, ensuring smooth daily operations.\n- Supported Realtors and Brokers by coordinating marketing materials, client communications, and office transactions.\n\n# Skills\n- Contract Management\n- Retail Market Analysis\n- Property Valuation\n- Client Relationship Management\n- Digital Marketing\n- Attention to Detail\n\n# Languages\n- English (Native)\n- Spanish - C2";
|
||||
|
||||
var actual = ParseNormalizedMarkdown(normalized);
|
||||
|
||||
Assert.Equal("Avery Cooper", actual.Contact.FullName);
|
||||
Assert.Equal("San Francisco", actual.Contact.Location);
|
||||
Assert.NotEmpty(actual.Jobs);
|
||||
Assert.Equal("Real Estate Agent", actual.Jobs[0].Title);
|
||||
Assert.Equal("Eleanor Lane Agency, White Plains", actual.Jobs[0].Company);
|
||||
Assert.True(actual.Jobs[0].Bullets.Count >= 2);
|
||||
Assert.Contains("Contract Management", actual.Skills);
|
||||
Assert.Contains(actual.Languages, item => string.Equals(item.Name, "Spanish", StringComparison.OrdinalIgnoreCase) && string.Equals(item.Level, "C2", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalized_markdown_parse_preserves_web_developer_bullets_and_skills()
|
||||
{
|
||||
var normalized = "# Contact\nChristoper Morgan\nchristoper.m@gmail.com\n+44 (0)20 7666 8555\n\n# Professional Summary\nSenior Web Developer specializing in front end development. Experienced with all stages of the development cycle for dynamic web projects.\n\n# Work Experience\nWeb Developer\nLuna Web Design, New York\n09/2015 - 05/2019\n- Cooperate with designers to create clean interfaces and simple, intuitive interactions and experiences.\n- Develop project concepts and maintain optimal workflow.\n- Work with senior developer to manage large, complex design projects for corporate clients.\n- Complete detailed programming and development tasks for front end public and internal websites as well as challenging back-end server code.\n- Carry out quality assurance tests to discover errors and optimize usability.\n\n# Skills\n- JavaScript\n- HTML5\n- PHP OOP\n- CSS\n- SQL\n- MySQL\n\n# Languages\n- Spanish - C2\n- Chinese - A1\n- German - A2";
|
||||
|
||||
var actual = ParseNormalizedMarkdown(normalized);
|
||||
|
||||
Assert.Equal("Christoper Morgan", actual.Contact.FullName);
|
||||
Assert.NotEmpty(actual.Jobs);
|
||||
Assert.Equal("Web Developer", actual.Jobs[0].Title);
|
||||
Assert.Equal("Luna Web Design, New York", actual.Jobs[0].Company);
|
||||
Assert.True(actual.Jobs[0].Bullets.Count >= 5);
|
||||
Assert.Contains("JavaScript", actual.Skills);
|
||||
Assert.Contains("MySQL", actual.Skills);
|
||||
Assert.Contains(actual.Languages, item => string.Equals(item.Name, "Chinese", StringComparison.OrdinalIgnoreCase) && string.Equals(item.Level, "A1", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(actual.Languages, item => string.Equals(item.Name, "German", StringComparison.OrdinalIgnoreCase) && string.Equals(item.Level, "A2", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Parse_uses_forced_ai_normalizer_output_when_enabled()
|
||||
{
|
||||
var previous = Environment.GetEnvironmentVariable("CV_FORCE_AI_NORMALIZER");
|
||||
Environment.SetEnvironmentVariable("CV_FORCE_AI_NORMALIZER", "true");
|
||||
try
|
||||
{
|
||||
var source = "Avery CooperReal Estate Agent\nSan Francisco(415) 223-4344\nDynamic real estate professional with 12 years of experience in residential and commercial property.";
|
||||
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
var normalizer = new Mock<ICvAiNormalizer>();
|
||||
normalizer
|
||||
.Setup(x => x.NormalizeAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CvNormalizationResult(
|
||||
0.88,
|
||||
"forced test",
|
||||
"# Contact\nAvery Cooper\n(415) 223-4344\nhttps://www.linkedin.com/in/avery-cooper/\nhttps://www.realtor.com/realestateagents/avery-copper/\nSan Francisco\n\n# Professional Summary\nDynamic real estate professional with 12 years of experience in residential and commercial property.\n\n# Work Experience\nReal Estate Agent\nEleanor Lane Agency, White Plains\nJuly 2017 - Present\n- Managed all aspects of the sales process from preparation to close.\n\n# Skills\n- Contract Management\n- Property Valuation\n\n# Languages\n- English (Native)\n- Spanish - C2"));
|
||||
|
||||
await using var db = CreateDb();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths(), null, normalizer.Object);
|
||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
||||
Assert.IsType<OkObjectResult>(result.Result);
|
||||
|
||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal("Avery Cooper", actual.Contact.FullName);
|
||||
Assert.Equal("San Francisco", actual.Contact.Location);
|
||||
Assert.NotEmpty(actual.Jobs);
|
||||
Assert.Contains("Real Estate Agent", actual.Jobs[0].Title ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Contract Management", actual.Skills);
|
||||
Assert.Contains(actual.Languages, item => string.Equals(item.Name, "Spanish", StringComparison.OrdinalIgnoreCase) && string.Equals(item.Level, "C2", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("CV_FORCE_AI_NORMALIZER", previous);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approved_fixture_regression_for_cv_txt_keeps_core_fields_stable()
|
||||
{
|
||||
var approvedPath = "/home/pi/cvs/approved-jsons/cv-txt.json";
|
||||
var rawPath = "/home/pi/cvs/cv.txt";
|
||||
if (!System.IO.File.Exists(approvedPath) || !System.IO.File.Exists(rawPath)) return;
|
||||
|
||||
var approved = StructuredCvProfileJson.Deserialize(await System.IO.File.ReadAllTextAsync(approvedPath));
|
||||
var rawSource = await System.IO.File.ReadAllTextAsync(rawPath);
|
||||
var user = new ApplicationUser { Id = "user-1", ProfileCvText = rawSource };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
await using var db = CreateDb();
|
||||
var paths = CreatePaths();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, paths);
|
||||
|
||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(rawSource));
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
Assert.NotNull(ok.Value);
|
||||
|
||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal(approved.Contact.FullName, actual.Contact.FullName);
|
||||
Assert.Equal(approved.Contact.Location, actual.Contact.Location);
|
||||
Assert.True(actual.Skills.Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approved_fixture_regression_for_new_resume_docx_keeps_contact_and_role_core_fields_stable()
|
||||
{
|
||||
var approvedPath = "/home/pi/cvs/approved-jsons/new-resume-001-docx.json";
|
||||
if (!System.IO.File.Exists(approvedPath)) return;
|
||||
|
||||
var approved = StructuredCvProfileJson.Deserialize(await System.IO.File.ReadAllTextAsync(approvedPath));
|
||||
var source = "Christoper Morgan\nPhone: +49 800 600 600\nE-Mail: christoper.morgan@gmail.com\nLinkedin: linkedin.com/christopher.morgan\n\nSkill Highlights\nProject management\nStrong decision maker\nComplex problem solver\nCreative design\nInnovative\nService-focused\n\n09/2015 to 05/2019\nWeb Developer\nLuna Web Design, New York\nCooperate with designers to create clean interfaces and simple, intuitive interactions and experiences.\nDevelop project concepts and maintain optimal workflow.\nWork with senior developer to manage large, complex design projects for corporate clients.\nComplete detailed programming and development tasks for front end public and internal websites as well as challenging back-end server code.\nCarry out quality assurance tests to discover errors and optimize usability.\n\n2014 to 2019\nBachelor Of Science: Computer Information Systems\nColumbia University, NY\n\nLanguages\nSpanish C2\nChinese C2\n\nSkills\nJavaScript\nSQL";
|
||||
|
||||
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
await using var db = CreateDb();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
||||
Assert.IsType<OkObjectResult>(result.Result);
|
||||
|
||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal(approved.Contact.FullName, actual.Contact.FullName);
|
||||
Assert.Equal(approved.Contact.Email, actual.Contact.Email);
|
||||
Assert.NotEmpty(actual.Jobs);
|
||||
Assert.Contains("Web Developer", actual.Jobs[0].Title ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("JavaScript", actual.Skills);
|
||||
Assert.Contains("SQL", actual.Skills);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approved_fixture_regression_for_coolfreecv_resume_keeps_summary_and_bullets_stable()
|
||||
{
|
||||
var approvedPath = "/home/pi/cvs/approved-jsons/coolfreecv-resume-en-03-n-docx.json";
|
||||
if (!System.IO.File.Exists(approvedPath)) return;
|
||||
|
||||
var approved = StructuredCvProfileJson.Deserialize(await System.IO.File.ReadAllTextAsync(approvedPath));
|
||||
var source = "Christoper Morgan\nchristoper.m@gmail.com\n+44 (0)20 7666 8555\n\nSenior Web Developer specializing in front end development. Experienced with all stages of the development cycle for dynamic web projects. Well-versed in numerous programming languages including HTML5, PHP OOP, JavaScript, CSS, MySQL. Strong background in project management and customer relations.\n\nWeb Developer - 09/2015 to 05/2019\nLuna Web Design, New York\nCooperate with designers to create clean interfaces and simple, intuitive interactions and experiences.\nDevelop project concepts and maintain optimal workflow.\nWork with senior developer to manage large, complex design projects for corporate clients.\nComplete detailed programming and development tasks for front end public and internal websites as well as challenging back-end server code.\nCarry out quality assurance tests to discover errors and optimize usability.\n\nBachelor Of Science: Computer Information Systems - 2014\nColumbia University, NY\n\nSkills\nJavaScript, HTML5, PHP OOP, CSS, SQL, MySQL\nProject management\nStrong decision maker\nComplex problem solver\nCreative design\nInnovative\nService-focused\n\nLanguages\nSpanish C2\nChinese A1\nGerman A2";
|
||||
|
||||
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
await using var db = CreateDb();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
||||
Assert.IsType<OkObjectResult>(result.Result);
|
||||
|
||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal(approved.Contact.FullName, actual.Contact.FullName);
|
||||
Assert.Equal(approved.Contact.Email, actual.Contact.Email);
|
||||
Assert.NotEmpty(actual.Summary);
|
||||
Assert.Contains("Senior Web Developer", actual.Summary[0], StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotEmpty(actual.Jobs);
|
||||
Assert.Contains("Web Developer", actual.Jobs[0].Title ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("JavaScript", actual.Skills);
|
||||
Assert.Contains("MySQL", actual.Skills);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deterministic_parse_handles_flat_resume_contact_and_first_job()
|
||||
{
|
||||
var source = "Christoper Morgan\nchristoper.m@gmail.com\n+44 (0)20 7666 8555\nSenior Web Developer specializing in front end development. Experienced with all stages of the development cycle for dynamic web projects.\n\nWeb Developer - 09/2015 to 05/2019\nLuna Web Design, New York\nCooperate with designers to create clean interfaces and simple, intuitive interactions and experiences.\nDevelop project concepts and maintain optimal workflow.\nWork with senior developer to manage large, complex design projects for corporate clients.\nComplete detailed programming and development tasks for front end public and internal websites as well as challenging back-end server code.\nCarry out quality assurance tests to discover errors and optimize usability.\n\nSkills\nJavaScript, HTML5, PHP OOP, CSS, SQL, MySQL";
|
||||
|
||||
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
await using var db = CreateDb();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
||||
Assert.IsType<OkObjectResult>(result.Result);
|
||||
|
||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal("Christoper Morgan", actual.Contact.FullName);
|
||||
Assert.Equal("christoper.m@gmail.com", actual.Contact.Email);
|
||||
Assert.Equal("+44 (0)20 7666 8555", actual.Contact.Phone);
|
||||
Assert.NotEmpty(actual.Jobs);
|
||||
Assert.Contains("Web Developer", actual.Jobs[0].Title ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("JavaScript", actual.Skills);
|
||||
Assert.Contains("SQL", actual.Skills);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deterministic_parse_handles_real_estate_contact_summary_and_jobs()
|
||||
{
|
||||
var source = "Avery Cooper Real Estate Agent\n(415) 223-4344\nSan Francisco\nhttps://www.linkedin.com/in/avery-cooper\nhttps://www.realtor.com/realestateagents/avery-copper/\n\nDynamic real estate professional with 12 years of experience in residential and commercial property. Proven track record in developing strong client relationships, closing over 50 successful deals, and providing exceptional real estate experiences.\n\nReal Estate Agent at Eleanor Lane Agency\nWhite Plains\n2017 - Present\nManaged all aspects of the sales process from preparation to close, achieving a 25% increase in closed deals compared to previous periods.\nSuccessfully negotiated favorable terms for clients in over 50 real estate transactions, consistently securing above-market value.\n\nReal Estate Assistant at Hathaway Properties\nNew Rochelle\n2012 - 2017\nManaged administrative tasks in a fast-paced real estate office, ensuring smooth daily operations.\nSupported Realtors and Brokers by coordinating marketing materials, client communications, and office transactions.\n\nSkills\nContract Management, Retail Market Analysis, Property Valuation, Client Relationship Management, Digital Marketing, Attention to Detail";
|
||||
|
||||
var user = new ApplicationUser { Id = "user-1", ProfileCvText = source };
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService.Setup(x => x.SummarizeSectionAsync(It.Is<string>(instruction => instruction.Contains("Extract this CV into structured JSON", StringComparison.Ordinal)), It.IsAny<string>(), 3200, 900))
|
||||
.ReturnsAsync("not-json");
|
||||
|
||||
await using var db = CreateDb();
|
||||
var controller = CreateController(userManager.Object, aiService.Object, db, CreatePaths());
|
||||
var result = await controller.Parse(new ProfileCvController.ParseCvRequest(source));
|
||||
Assert.IsType<OkObjectResult>(result.Result);
|
||||
|
||||
var actual = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
Assert.Equal("Avery Cooper", actual.Contact.FullName);
|
||||
Assert.Equal("San Francisco", actual.Contact.Location);
|
||||
Assert.Contains("realtor.com", actual.Contact.Website ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotEmpty(actual.Summary);
|
||||
Assert.True(actual.Jobs.Count >= 2);
|
||||
Assert.Contains("Contract Management", actual.Skills);
|
||||
Assert.Contains("Attention to Detail", actual.Skills);
|
||||
}
|
||||
|
||||
private static StructuredCvProfile ParseNormalizedMarkdown(string normalized)
|
||||
{
|
||||
var method = typeof(ProfileCvController).GetMethod("BuildStructuredCvFromNormalizedMarkdown", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
Assert.NotNull(method);
|
||||
var result = method!.Invoke(null, new object[] { normalized });
|
||||
Assert.NotNull(result);
|
||||
return StructuredCvProfileJson.Normalize((StructuredCvProfile)result!);
|
||||
}
|
||||
|
||||
private static ProfileCvController CreateController(UserManager<ApplicationUser> userManager, ISummarizerService aiService, JobTrackerContext db, AppPaths paths, ICvAiClassifier? cvAiClassifier = null, ICvAiNormalizer? cvAiNormalizer = null)
|
||||
{
|
||||
return new ProfileCvController(userManager, aiService, db, paths, null, cvAiClassifier ?? NoOpCvAiClassifier.Instance, cvAiNormalizer ?? NoOpCvAiNormalizer.Instance)
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user