b4fd5e2f96
Backlog item 4 (Wave 3, first sub-item). HasResume/HasCoverLetter/HasPortfolio/ HasOtherAttachment were manually-editable checkboxes in EditJobDialog, completely independent of whether a file was actually attached -- classic drift: mark 'resume ready' by hand, later delete the resume attachment, flag stays stuck true forever. User confirmed (asked directly, since removing the manual-override capability is a product decision, not purely technical): make them fully computed from Attachments, no manual override. - AttachmentsController.RecomputeAttachmentFlagsAsync: the single place these four fields get written now, called after every attachment mutation (upload, delete, Purpose change) that could affect them. Deliberately kept as persisted columns (not [NotMapped] computed properties reading the Attachments navigation collection) -- ~15 query sites build JobApplication DTOs without .Include(Attachments), so a live-computed property would silently return false everywhere instead of throwing, the worst kind of bug. Recomputing at the one write funnel avoids touching any read path. - Removed HasResume/etc from CreateJobApplicationRequest/ UpdateJobApplicationRequest -- no longer client-settable. - EditJobDialog: removed the manual checkboxes, kept the (now genuinely accurate) read-only status chips. - AddJobModal: stopped sending has*-flags at job-creation time; the follow-up attachment upload call now sets them correctly via the same recompute path. Caught a real bug while testing this: the Purpose-change path recomputed before saving the Purpose change, so a fresh query missed the pending edit and the flags never updated. Fixed by committing the mutation before recomputing. 3 new backend tests (purpose-change sets flag, delete clears flag, non-primary purpose counts as "other"). 172/172 backend, 25/25 frontend suites (57 tests) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
4.4 KiB
C#
112 lines
4.4 KiB
C#
using JobTrackerApi.Controllers;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Tests.TestSupport;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
// JobApplication.HasResume/HasCoverLetter/HasPortfolio/HasOtherAttachment are derived from
|
|
// Attachment rows (backlog Wave 3), not manually settable. These tests exercise the single
|
|
// place they're written: AttachmentsController's Purpose-change and Delete paths.
|
|
public sealed class AttachmentFlagsRecomputeTests
|
|
{
|
|
[Fact]
|
|
public async Task Changing_purpose_to_resume_sets_HasResume()
|
|
{
|
|
await using var db = TestHostFactory.CreateInMemoryDb();
|
|
var (job, attachment) = await SeedJobWithAttachmentAsync(db, purpose: "other");
|
|
var controller = CreateController(db);
|
|
|
|
var result = await controller.Rename(attachment.Id, new AttachmentsController.UpdateAttachmentRequest(null, "resume", null), CancellationToken.None);
|
|
|
|
Assert.IsType<NoContentResult>(result);
|
|
var updated = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.True(updated.HasResume);
|
|
Assert.True(updated.HasOtherAttachment == false);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Deleting_the_only_resume_attachment_clears_HasResume()
|
|
{
|
|
await using var db = TestHostFactory.CreateInMemoryDb();
|
|
var (job, attachment) = await SeedJobWithAttachmentAsync(db, purpose: "resume");
|
|
var controller = CreateController(db);
|
|
|
|
var result = await controller.Delete(attachment.Id, CancellationToken.None);
|
|
|
|
Assert.IsType<NoContentResult>(result);
|
|
var updated = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.False(updated.HasResume);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Attachment_with_case_study_purpose_counts_as_other()
|
|
{
|
|
await using var db = TestHostFactory.CreateInMemoryDb();
|
|
var (job, attachment) = await SeedJobWithAttachmentAsync(db, purpose: "resume");
|
|
var controller = CreateController(db);
|
|
|
|
await controller.Rename(attachment.Id, new AttachmentsController.UpdateAttachmentRequest(null, "case-study", null), CancellationToken.None);
|
|
|
|
var updated = await db.JobApplications.SingleAsync(j => j.Id == job.Id);
|
|
Assert.False(updated.HasResume);
|
|
Assert.True(updated.HasOtherAttachment);
|
|
}
|
|
|
|
private static async Task<(JobApplication Job, Attachment Attachment)> SeedJobWithAttachmentAsync(JobTrackerContext db, string purpose)
|
|
{
|
|
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" };
|
|
db.JobApplications.Add(job);
|
|
await db.SaveChangesAsync();
|
|
|
|
var attachment = new Attachment
|
|
{
|
|
JobApplicationId = job.Id,
|
|
FileName = "file.pdf",
|
|
FilePath = Path.Combine(Path.GetTempPath(), $"jobtracker-attachment-test-{Guid.NewGuid():N}.pdf"),
|
|
FileType = "application/pdf",
|
|
FileSize = 100,
|
|
Purpose = purpose,
|
|
};
|
|
db.Attachments.Add(attachment);
|
|
await db.SaveChangesAsync();
|
|
|
|
job.HasResume = purpose == "resume";
|
|
job.HasOtherAttachment = purpose is not ("resume" or "cover-letter" or "portfolio");
|
|
await db.SaveChangesAsync();
|
|
|
|
return (job, attachment);
|
|
}
|
|
|
|
private static AttachmentsController CreateController(JobTrackerContext db)
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), $"jobtracker-attachments-tests-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempRoot);
|
|
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?> { ["Data:Root"] = tempRoot })
|
|
.Build();
|
|
|
|
var env = new Mock<IHostEnvironment>();
|
|
env.SetupGet(x => x.ContentRootPath).Returns(tempRoot);
|
|
var paths = new AppPaths(config, env.Object);
|
|
|
|
return new AttachmentsController(paths, db)
|
|
{
|
|
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
|
|
};
|
|
}
|
|
}
|