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 Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class CareerProfileControllerTests { private static (CareerProfileController controller, JobTrackerContext db, ApplicationUser user) Build() { var db = TestHostFactory.CreateInMemoryDb(); var user = new ApplicationUser { Id = "user-1", Email = "ada@example.com", UserName = "ada" }; var users = TestHostFactory.CreateUserManager(); users.Setup(x => x.GetUserAsync(It.IsAny())).ReturnsAsync(user); users.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success); var controller = new CareerProfileController(users.Object, new CareerProfileService(db), db) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "user-1") }, "test")), }, }, }; return (controller, db, user); } private static StructuredCvProfile Sample() => new() { Contact = { FullName = "Ada Lovelace", Email = "ada@example.com" }, Jobs = { new StructuredCvJob { Title = "Engineer", Company = "Acme" } }, Skills = { "C#", "SQL" }, }; [Fact] public async Task Put_then_get_round_trips_the_profile_and_updates_the_projection() { var (controller, _, user) = Build(); var put = await controller.Put(Sample(), CancellationToken.None); Assert.IsType(put.Result); // The derived blob projection is kept in sync for legacy readers. Assert.Contains("Ada Lovelace", user.ProfileCvStructureJson); var get = await controller.Get(CancellationToken.None); var dto = Assert.IsType(Assert.IsType(get.Result).Value); Assert.Equal("Ada Lovelace", dto.Profile.Contact.FullName); Assert.Single(dto.Profile.Jobs); Assert.Equal(new[] { "C#", "SQL" }, dto.Profile.Skills); } [Fact] public async Task Get_reports_completeness_with_missing_sections() { var (controller, _, _) = Build(); await controller.Put(Sample(), CancellationToken.None); // has personal + experience + skills var get = await controller.Get(CancellationToken.None); var dto = Assert.IsType(Assert.IsType(get.Result).Value); Assert.InRange(dto.Completeness.Percent, 1, 99); Assert.Contains("Education", dto.Completeness.Missing); // not provided Assert.DoesNotContain("Experience", dto.Completeness.Missing); } [Fact] public async Task Put_rejects_an_over_limit_profile() { var (controller, _, _) = Build(); var huge = new StructuredCvProfile(); for (var i = 0; i < 500; i++) huge.Skills.Add($"skill-{i}"); var put = await controller.Put(huge, CancellationToken.None); Assert.IsType(put.Result); } [Fact] public async Task Put_accepts_an_empty_work_in_progress_profile() { var (controller, _, _) = Build(); var put = await controller.Put(new StructuredCvProfile(), CancellationToken.None); // Completeness, not validation, is what flags an incomplete profile — an empty profile saves. Assert.IsType(put.Result); } }