992f89e619
Recover the F1 Career Profile foundation + AI-workspace persistence from the unmerged feature/career-workspace branch, so Phase 2 builds on the documented, tested target state instead of re-deriving it. Foundation only — CV Builder commits (variants, ATS badge, rewrite diff) stay deferred per "do not build CV Builder yet". See docs/career-workspace-branch-assessment.md. Squashed from 3 branch commits (235e291,5916f09,00a035e), resolved against main + Phase 0: - CareerProfile + CareerProfileVersion (append-only history), dual-written from every profile save path via CareerProfileService. ApplicationUser. ProfileCvStructureJson stays authoritative; the tables mirror it. Stable item IDs assigned to jobs/education/certifications/projects (the prerequisite for future variant lineage). CvDateNormalizer for free-text -> YYYY-MM. - InterviewPrepNote + AiWorkspaceNote: cache AI interview prep / candidate fit / focus plan keyed by an attachment-context signature, so they stop regenerating (and re-spending the provider) on every open. Conflict resolutions (union, favouring current code + Phase 0): - JobTrackerContext / StartupInitializationExtensions: kept Phase 0's tables and reconciler blocks, added the career/interview/ai-note tables (both SQLite and MySQL dialects). - ProfileCvController: dropped the branch's in-file DTO records (main defines them in ProfileCvDtos.cs) and the LayoutFamily/AtsRating template fields (deferred ATS-badge work), keeping main's 7-arg CvTemplateDescriptor. - JobApplicationsController: kept the branch's cache-check, restored main's AsNoTracking on the read-only user load. Tables ship empty (verified dev); nothing to migrate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.7 KiB
C#
102 lines
3.7 KiB
C#
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
public sealed class CareerProfileServiceTests
|
|
{
|
|
private static JobTrackerContext NewContext(string userId)
|
|
{
|
|
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var currentUser = new Mock<ICurrentUserService>();
|
|
currentUser.SetupGet(service => service.UserId).Returns(userId);
|
|
return new JobTrackerContext(options, currentUser.Object);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveVersionAsync_assigns_stable_ids_to_items_missing_one()
|
|
{
|
|
await using var db = NewContext("user-1");
|
|
var service = new CareerProfileService(db);
|
|
var profile = new StructuredCvProfile
|
|
{
|
|
Jobs = { new StructuredCvJob { Title = "Engineer", Company = "Acme" } },
|
|
};
|
|
|
|
var saved = await service.SaveVersionAsync("user-1", profile, "upload", default);
|
|
|
|
Assert.False(string.IsNullOrWhiteSpace(saved.Jobs[0].Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveVersionAsync_preserves_existing_ids_across_saves()
|
|
{
|
|
await using var db = NewContext("user-1");
|
|
var service = new CareerProfileService(db);
|
|
var profile = new StructuredCvProfile
|
|
{
|
|
Jobs = { new StructuredCvJob { Title = "Engineer", Company = "Acme" } },
|
|
};
|
|
await service.SaveVersionAsync("user-1", profile, "upload", default);
|
|
var firstId = profile.Jobs[0].Id;
|
|
|
|
await service.SaveVersionAsync("user-1", profile, "rebuild", default);
|
|
|
|
Assert.Equal(firstId, profile.Jobs[0].Id);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("January 2020", "2020-01")]
|
|
[InlineData("Mar 2019", "2019-03")]
|
|
[InlineData("03/2019", "2019-03")]
|
|
[InlineData("2019-03", "2019-03")]
|
|
[InlineData("Present", null)]
|
|
[InlineData("2020", null)]
|
|
[InlineData(null, null)]
|
|
public void CvDateNormalizer_parses_common_formats_without_guessing(string? input, string? expected)
|
|
{
|
|
Assert.Equal(expected, CvDateNormalizer.TryParseYearMonth(input));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveVersionAsync_persists_current_snapshot_and_append_only_history()
|
|
{
|
|
await using var db = NewContext("user-1");
|
|
var service = new CareerProfileService(db);
|
|
var profile = new StructuredCvProfile { Summary = { "First version" } };
|
|
|
|
await service.SaveVersionAsync("user-1", profile, "upload", default);
|
|
await service.SaveVersionAsync("user-1", profile, "improve", default);
|
|
|
|
var snapshots = await db.CareerProfiles.IgnoreQueryFilters().Where(x => x.OwnerUserId == "user-1").ToListAsync();
|
|
var history = await db.CareerProfileVersions.IgnoreQueryFilters().Where(x => x.OwnerUserId == "user-1").ToListAsync();
|
|
|
|
Assert.Single(snapshots);
|
|
Assert.Equal(2, snapshots[0].Version);
|
|
Assert.Equal(2, history.Count);
|
|
Assert.Equal(new[] { "upload", "improve" }, history.OrderBy(x => x.Version).Select(x => x.Source));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveVersionAsync_does_not_normalize_end_date_when_job_is_current()
|
|
{
|
|
await using var db = NewContext("user-1");
|
|
var service = new CareerProfileService(db);
|
|
var profile = new StructuredCvProfile
|
|
{
|
|
Jobs = { new StructuredCvJob { Title = "Engineer", Start = "Jan 2020", End = "Present", IsCurrent = true } },
|
|
};
|
|
|
|
var saved = await service.SaveVersionAsync("user-1", profile, "upload", default);
|
|
|
|
Assert.Equal("2020-01", saved.Jobs[0].StartDate);
|
|
Assert.Null(saved.Jobs[0].EndDate);
|
|
}
|
|
}
|