using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; namespace JobTrackerApi.Tests; // Interview prep persistence (career-workspace-implementation-roadmap.md Phase F5): the AI call // used to re-run on every tab open. These tests lock in that a note is generated once, reused on // subsequent reads, and only regenerated when the attachment context changes or a refresh is // explicitly requested. public sealed class InterviewPrepPersistenceTests { [Fact] public async Task GetInterviewPrep_persists_and_reuses_the_generated_note() { await using var db = TestHostFactory.CreateInMemoryDb(); var job = await SeedJobAsync(db); var summarizer = new Mock(); summarizer.SetupSequence(x => x.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("First generated brief.") .ReturnsAsync("Second generated brief."); var controller = CreateController(db, summarizer.Object, "user-1"); var first = await controller.GetInterviewPrep(job.Id, null, false, CancellationToken.None); Assert.Equal("First generated brief.", GetDto(first).Summary); var second = await controller.GetInterviewPrep(job.Id, null, false, CancellationToken.None); Assert.Equal("First generated brief.", GetDto(second).Summary); summarizer.Verify(x => x.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); var stored = Assert.Single(db.InterviewPrepNotes.IgnoreQueryFilters().Where(x => x.JobApplicationId == job.Id)); Assert.Equal("First generated brief.", stored.Summary); } [Fact] public async Task GetInterviewPrep_regenerates_when_refresh_is_requested() { await using var db = TestHostFactory.CreateInMemoryDb(); var job = await SeedJobAsync(db); var summarizer = new Mock(); summarizer.SetupSequence(x => x.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("First generated brief.") .ReturnsAsync("Refreshed brief."); var controller = CreateController(db, summarizer.Object, "user-1"); await controller.GetInterviewPrep(job.Id, null, false, CancellationToken.None); var refreshed = await controller.GetInterviewPrep(job.Id, null, true, CancellationToken.None); Assert.Equal("Refreshed brief.", GetDto(refreshed).Summary); summarizer.Verify(x => x.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); var stored = Assert.Single(db.InterviewPrepNotes.IgnoreQueryFilters().Where(x => x.JobApplicationId == job.Id)); Assert.Equal("Refreshed brief.", stored.Summary); } [Fact] public async Task GetInterviewPrep_regenerates_when_attachment_selection_changes() { await using var db = TestHostFactory.CreateInMemoryDb(); var job = await SeedJobAsync(db); var summarizer = new Mock(); summarizer.SetupSequence(x => x.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("No-attachment brief.") .ReturnsAsync("With-attachment brief."); var controller = CreateController(db, summarizer.Object, "user-1"); await controller.GetInterviewPrep(job.Id, null, false, CancellationToken.None); var withAttachment = await controller.GetInterviewPrep(job.Id, "7", false, CancellationToken.None); Assert.Equal("With-attachment brief.", GetDto(withAttachment).Summary); summarizer.Verify(x => x.SummarizeSectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); } private static InterviewPrepDto GetDto(ActionResult result) => (InterviewPrepDto)Assert.IsType(result.Result).Value!; private static async Task SeedJobAsync(JobTrackerContext db) { 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", Description = "Needs .NET and SQL experience.", }; db.JobApplications.Add(job); await db.SaveChangesAsync(); return job; } private static JobApplicationsController CreateController(JobTrackerContext db, ISummarizerService summarizer, string userId) { var controller = new JobApplicationsController( db, summarizer, TestHostFactory.CreateUserManager(null).Object, Mock.Of(), Mock.Of()); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")) } }; return controller; } }