8fe39031ea
Keep follow-up draft generation but remove the direct application SMTP delivery boundary. Route users to the provider-aware Job email flow and return 410 for legacy API callers.
134 lines
5.4 KiB
C#
134 lines
5.4 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 Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
/// <summary>
|
|
/// Moving a job backwards into a pre-application stage clears DateApplied so analytics stay
|
|
/// accurate. These tests guard the other half of that bargain: the application activity must be
|
|
/// preserved as history rather than destroyed.
|
|
/// </summary>
|
|
public sealed class JobApplicationsAppliedDateHistoryTests
|
|
{
|
|
private static readonly DateTime AppliedOn = new(2026, 3, 1, 9, 30, 0, DateTimeKind.Utc);
|
|
|
|
[Fact]
|
|
public async Task Moving_back_to_a_prospect_stage_clears_the_date_but_records_it_as_history()
|
|
{
|
|
await using var db = CreateDb();
|
|
var job = await SeedAppliedJob(db);
|
|
var controller = CreateController(db, "user-1");
|
|
|
|
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Saved"), CancellationToken.None);
|
|
|
|
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.Equal("Saved", saved.Status);
|
|
Assert.Null(saved.DateApplied);
|
|
|
|
var cleared = await db.JobEvents.SingleAsync(e => e.Type == JobPipeline.AppliedDateClearedEvent);
|
|
// Round-tripped, so the original date is recoverable exactly — not just described in prose.
|
|
Assert.Equal(AppliedOn, DateTime.Parse(cleared.OldValue!, null, System.Globalization.DateTimeStyles.RoundtripKind));
|
|
|
|
// The status transition itself is still recorded separately.
|
|
Assert.Contains(await db.JobEvents.ToListAsync(), e => e.Type == "StatusChanged" && e.NewValue == "Saved");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Re_applying_after_a_backwards_move_stamps_a_fresh_date_and_leaves_history_intact()
|
|
{
|
|
await using var db = CreateDb();
|
|
var job = await SeedAppliedJob(db);
|
|
var controller = CreateController(db, "user-1");
|
|
|
|
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Saved"), CancellationToken.None);
|
|
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Applied"), CancellationToken.None);
|
|
|
|
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.Equal("Applied", saved.Status);
|
|
Assert.NotNull(saved.DateApplied);
|
|
Assert.NotEqual(AppliedOn, saved.DateApplied);
|
|
|
|
// The first application's date survives the round trip in history.
|
|
var cleared = await db.JobEvents.SingleAsync(e => e.Type == JobPipeline.AppliedDateClearedEvent);
|
|
Assert.Equal(AppliedOn, DateTime.Parse(cleared.OldValue!, null, System.Globalization.DateTimeStyles.RoundtripKind));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Moving_between_post_application_stages_records_no_cleared_date_event()
|
|
{
|
|
await using var db = CreateDb();
|
|
var job = await SeedAppliedJob(db);
|
|
var controller = CreateController(db, "user-1");
|
|
|
|
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Interview"), CancellationToken.None);
|
|
|
|
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.Equal(AppliedOn, saved.DateApplied);
|
|
Assert.DoesNotContain(await db.JobEvents.ToListAsync(), e => e.Type == JobPipeline.AppliedDateClearedEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Withdrawing_keeps_the_applied_date()
|
|
{
|
|
await using var db = CreateDb();
|
|
var job = await SeedAppliedJob(db);
|
|
var controller = CreateController(db, "user-1");
|
|
|
|
await controller.UpdateStatus(job.Id, new UpdateStatusRequest("Withdrawn"), CancellationToken.None);
|
|
|
|
// Withdrawn is a closed stage, not a pre-application one: you did apply, then pulled out.
|
|
var saved = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.Equal("Withdrawn", saved.Status);
|
|
Assert.Equal(AppliedOn, saved.DateApplied);
|
|
}
|
|
|
|
private static async Task<JobApplication> SeedAppliedJob(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",
|
|
Status = "Applied",
|
|
DateApplied = AppliedOn,
|
|
SavedAt = AppliedOn.AddDays(-2),
|
|
};
|
|
db.JobApplications.Add(job);
|
|
await db.SaveChangesAsync();
|
|
return job;
|
|
}
|
|
|
|
private static JobApplicationsController CreateController(JobTrackerContext db, string userId)
|
|
{
|
|
var controller = new JobApplicationsController(db, Mock.Of<ISummarizerService>(), TestHostFactory.CreateUserManager().Object);
|
|
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() => TestHostFactory.CreateInMemoryDb();
|
|
}
|