2b57d65715
Phase 3, frontend. /career now reads and writes the master profile through the
relational source of truth instead of the legacy blob path.
- CareerProfilePage loads GET /career/profile (structured profile from the
relational children + cvText + completeness) and saves PUT /career/profile
({ profile, cvText }). This keeps the relational store authoritative — the
previous PUT /auth/profile blob write left it stale after first load.
- Added a "Profile completeness" overview (percent bar + missing sections) at the
top of /career, from the server scorecard.
- PUT /career/profile now accepts { profile, cvText } so the single /career save
covers both the structured profile and the raw imported text; GET returns cvText.
Tests: career-save asserts the /career/profile payload; new completeness-overview
test; controller tests updated for the request wrapper. 75/76 frontend pass (the
1 failure is the unrelated pre-existing settings-view suite); prod build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.8 KiB
C#
100 lines
3.8 KiB
C#
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<ClaimsPrincipal>())).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(new CareerProfileSaveRequest(Sample(), null), CancellationToken.None);
|
|
Assert.IsType<OkObjectResult>(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<CareerProfileDto>(Assert.IsType<OkObjectResult>(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(new CareerProfileSaveRequest(Sample(), null), CancellationToken.None); // has personal + experience + skills
|
|
|
|
var get = await controller.Get(CancellationToken.None);
|
|
var dto = Assert.IsType<CareerProfileDto>(Assert.IsType<OkObjectResult>(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(new CareerProfileSaveRequest(huge, null), CancellationToken.None);
|
|
|
|
Assert.IsType<BadRequestObjectResult>(put.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Put_accepts_an_empty_work_in_progress_profile()
|
|
{
|
|
var (controller, _, _) = Build();
|
|
|
|
var put = await controller.Put(new CareerProfileSaveRequest(new StructuredCvProfile(), null), CancellationToken.None);
|
|
|
|
// Completeness, not validation, is what flags an incomplete profile — an empty profile saves.
|
|
Assert.IsType<OkObjectResult>(put.Result);
|
|
}
|
|
}
|