using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class JobApplicationsApplicationPackageTests { [Fact] public async Task Save_application_drafts_replaces_notes_instead_of_appending() { await using var db = CreateDb(); var company = new Company { Name = "Acme", OwnerUserId = "user-1" }; db.Companies.Add(company); await db.SaveChangesAsync(); var job = new JobApplication { JobTitle = "Backend Developer", CompanyId = company.Id, OwnerUserId = "user-1", Notes = "Old notes" }; db.JobApplications.Add(job); await db.SaveChangesAsync(); var controller = CreateController(db, Mock.Of(), "user-1"); var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(null, "Updated notes block", null), CancellationToken.None); Assert.IsType(result); var saved = await db.JobApplications.FirstAsync(); Assert.Equal("Updated notes block", saved.Notes); } [Fact] public async Task Generate_application_package_uses_imported_correspondence_and_recruiter_context() { await using var db = CreateDb(); var company = new Company { Name = "Acme", RecruiterName = "Maria Recruiter", RecruiterEmail = "maria@acme.test", OwnerUserId = "user-1" }; db.Companies.Add(company); db.Users.Add(new ApplicationUser { Id = "user-1", UserName = "user@example.test", Email = "user@example.test", ProfileCvText = "Built .NET APIs and led backend delivery.", ProfileCvStructureJson = "[]" }); await db.SaveChangesAsync(); var job = new JobApplication { JobTitle = "Backend Developer", CompanyId = company.Id, OwnerUserId = "user-1", Description = "Need .NET, APIs, and async collaboration with recruiters.", Notes = "Priority role", ShortSummary = "Acme backend hiring" }; db.JobApplications.Add(job); await db.SaveChangesAsync(); db.Correspondences.Add(new Correspondence { JobApplicationId = job.Id, From = "Company", Subject = "Backend Developer interview", ExternalThreadId = "thread-1", ExternalFrom = "Maria Recruiter ", ExternalTo = "user@example.test", Content = "We want someone who can own .NET APIs and communicate clearly with stakeholders.", Date = DateTime.UtcNow.AddDays(-1) }); await db.SaveChangesAsync(); var summarizer = new Mock(); summarizer .Setup(service => service.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string instruction, string context, int _, int __) => { if (instruction.Contains("List up to 4 concrete application-package signals", StringComparison.OrdinalIgnoreCase)) { return "Use recruiter language about owning .NET APIs.\nMention the interview timeline from imported correspondence."; } if (instruction.Contains("cover letter", StringComparison.OrdinalIgnoreCase)) { return context.Contains("Imported correspondence context:", StringComparison.OrdinalIgnoreCase) && context.Contains("Maria Recruiter", StringComparison.OrdinalIgnoreCase) ? "Cover letter tailored with recruiter context and imported correspondence." : "Generic cover letter."; } if (instruction.Contains("recruiter intro message", StringComparison.OrdinalIgnoreCase)) { return context.Contains("Recruiter email: maria@acme.test", StringComparison.OrdinalIgnoreCase) ? "Recruiter message that uses Maria's context." : "Generic recruiter message."; } if (instruction.Contains("application answer", StringComparison.OrdinalIgnoreCase)) { return "Application answer grounded in the imported thread."; } if (instruction.Contains("Rewrite the candidate CV", StringComparison.OrdinalIgnoreCase)) { return "Tailored CV that highlights .NET API ownership for Acme."; } return "Variant draft"; }); var controller = CreateController(db, summarizer.Object, "user-1"); var result = await controller.GenerateApplicationPackage(job.Id, null, null, null, CancellationToken.None); var ok = Assert.IsType(result.Result); var payload = Assert.IsType(ok.Value); Assert.Contains("Tailored CV", payload.TailoredCvText); Assert.Equal("Cover letter tailored with recruiter context and imported correspondence.", payload.CoverLetterDraft); Assert.Equal("Recruiter message that uses Maria's context.", payload.RecruiterMessageDraft); Assert.Contains(payload.KeyPoints, item => item.Contains("interview timeline", StringComparison.OrdinalIgnoreCase)); Assert.Contains(payload.KeyPoints, item => item.Contains("recruiter language", StringComparison.OrdinalIgnoreCase)); } private static JobApplicationsController CreateController(JobTrackerContext db, ISummarizerService summarizer, string userId) { var controller = new JobApplicationsController(db, summarizer, Mock.Of(), CreateUserManager().Object, NullLogger.Instance); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")) } }; return controller; } private static JobTrackerContext CreateDb() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var currentUser = new Mock(); currentUser.SetupGet(service => service.UserId).Returns("user-1"); return new JobTrackerContext(options, currentUser.Object); } private static Mock> CreateUserManager() { var store = new Mock>(); return new Mock>( store.Object, Options.Create(new IdentityOptions()), new PasswordHasher(), Array.Empty>(), Array.Empty>(), new UpperInvariantLookupNormalizer(), new IdentityErrorDescriber(), null!, new NullLogger>()); } }