refactor(api): extract JobApplications DTOs and helpers, fix N+1 aggregation
- Move inline DTOs to JobApplicationDtos.cs, pure static helpers to JobApplicationHelpers.cs - GetStats aggregates server-side (COUNT/GROUP BY) instead of loading the full table - Cache RuleSettings via IMemoryCache, keyed per-user (RulesEngine.GetSettings falls back to per-user UserRuleSettings overrides, so a single global cache key would leak settings across users) - Add missing AsNoTracking() to read-only GET endpoints (GetAll, GetById, GetBoard, GetReminders, GetStatusSuggestion, GetMatchScore, GetCandidateFit, GetFocusPlan, GetInterviewPrep, GetReadiness) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ public sealed class JobApplicationsApplicationPackageTests
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1");
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(null, "Updated notes block", null), CancellationToken.None);
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new SaveApplicationDraftsRequest(null, "Updated notes block", null), CancellationToken.None);
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
var saved = await db.JobApplications.FirstAsync();
|
||||
@@ -135,7 +135,7 @@ public sealed class JobApplicationsApplicationPackageTests
|
||||
var result = await controller.GenerateApplicationPackage(job.Id, null, null, null, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<JobApplicationsController.GenerateApplicationPackageDto>(ok.Value);
|
||||
var payload = Assert.IsType<GenerateApplicationPackageDto>(ok.Value);
|
||||
|
||||
Assert.Contains("Tailored CV", payload.TailoredCvText);
|
||||
Assert.Equal("Cover letter tailored with recruiter context and imported correspondence.", payload.CoverLetterDraft);
|
||||
@@ -261,7 +261,7 @@ public sealed class JobApplicationsApplicationPackageTests
|
||||
var result = await controller.GetTailoredCvDraft(job.Id, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<JobApplicationsController.TailoredCvDraftDto>(ok.Value);
|
||||
var payload = Assert.IsType<TailoredCvDraftDto>(ok.Value);
|
||||
Assert.True(payload.IsLegacyFallback);
|
||||
Assert.Equal("legacy-text", payload.TemplateId);
|
||||
Assert.Contains("Existing tailored CV text", payload.RenderedText);
|
||||
@@ -337,14 +337,14 @@ public sealed class JobApplicationsApplicationPackageTests
|
||||
var controller = CreateController(db, summarizer.Object, "user-1");
|
||||
var generateResult = await controller.GenerateTailoredCvDraft(job.Id, "ats", CancellationToken.None);
|
||||
var generateOk = Assert.IsType<OkObjectResult>(generateResult.Result);
|
||||
var generated = Assert.IsType<JobApplicationsController.TailoredCvDraftDto>(generateOk.Value);
|
||||
var generated = Assert.IsType<TailoredCvDraftDto>(generateOk.Value);
|
||||
|
||||
Assert.False(generated.IsLegacyFallback);
|
||||
Assert.Equal(7, generated.CanonicalProfileVersion);
|
||||
Assert.Equal("Senior Backend Engineer", generated.Headline);
|
||||
Assert.Contains("Led backend API delivery.", generated.RenderedText);
|
||||
|
||||
var saveResult = await controller.SaveTailoredCvDraft(job.Id, new JobApplicationsController.SaveTailoredCvDraftRequest(
|
||||
var saveResult = await controller.SaveTailoredCvDraft(job.Id, new SaveTailoredCvDraftRequest(
|
||||
generated.TemplateId,
|
||||
"Principal Backend Engineer",
|
||||
new List<string> { "Own backend delivery for critical APIs." },
|
||||
@@ -395,7 +395,7 @@ public sealed class JobApplicationsApplicationPackageTests
|
||||
var renderer = new TestCvTemplateRenderer();
|
||||
var exporter = new TestCvPdfExporter();
|
||||
var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1", renderer, exporter);
|
||||
var request = new JobApplicationsController.TailoredCvRenderRequest(
|
||||
var request = new TailoredCvRenderRequest(
|
||||
"ats-minimal",
|
||||
"Backend Engineer",
|
||||
new List<string> { "Built APIs" },
|
||||
@@ -409,7 +409,7 @@ public sealed class JobApplicationsApplicationPackageTests
|
||||
|
||||
var previewResult = await controller.PreviewTailoredCv(job.Id, request, CancellationToken.None);
|
||||
var ok = Assert.IsType<OkObjectResult>(previewResult.Result);
|
||||
var preview = Assert.IsType<JobApplicationsController.TailoredCvPreviewDto>(ok.Value);
|
||||
var preview = Assert.IsType<TailoredCvPreviewDto>(ok.Value);
|
||||
Assert.Equal("ats-minimal", preview.TemplateId);
|
||||
Assert.Equal("preview.pdf", preview.SuggestedFileName);
|
||||
Assert.Equal("data:image/png;base64,abc123", renderer.LastPhotoDataUrl);
|
||||
|
||||
@@ -9,7 +9,7 @@ public sealed class JobApplicationsControllerTests
|
||||
[Fact]
|
||||
public void Application_package_record_exposes_expected_fields()
|
||||
{
|
||||
var type = typeof(JobApplicationsController).GetNestedType("GenerateApplicationPackageDto", BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var type = typeof(GenerateApplicationPackageDto);
|
||||
Assert.NotNull(type);
|
||||
|
||||
var props = type!.GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(x => x.Name).ToHashSet();
|
||||
@@ -23,7 +23,7 @@ public sealed class JobApplicationsControllerTests
|
||||
[Fact]
|
||||
public void Save_application_drafts_request_supports_cover_letter_and_notes()
|
||||
{
|
||||
var type = typeof(JobApplicationsController).GetNestedType("SaveApplicationDraftsRequest", BindingFlags.Public | BindingFlags.NonPublic);
|
||||
var type = typeof(SaveApplicationDraftsRequest);
|
||||
Assert.NotNull(type);
|
||||
|
||||
var ctor = type!.GetConstructors().Single();
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(" Cover letter body ", " Notes body ", null), CancellationToken.None);
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new SaveApplicationDraftsRequest(" Cover letter body ", " Notes body ", null), CancellationToken.None);
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
var saved = await db.JobApplications.FirstAsync();
|
||||
@@ -83,7 +83,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
var result = await controller.GetStatusSuggestion(job.Id, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var dto = Assert.IsType<JobApplicationsController.StatusSuggestionDto>(ok.Value);
|
||||
var dto = Assert.IsType<StatusSuggestionDto>(ok.Value);
|
||||
Assert.True(dto.HasSuggestion);
|
||||
Assert.Equal("Rejected", dto.SuggestedStatus);
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
var result = await controller.GetStatusSuggestion(job.Id, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var dto = Assert.IsType<JobApplicationsController.StatusSuggestionDto>(ok.Value);
|
||||
var dto = Assert.IsType<StatusSuggestionDto>(ok.Value);
|
||||
Assert.False(dto.HasSuggestion);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
var result = await controller.GetMatchScore(job.Id, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var dto = Assert.IsType<JobApplicationsController.MatchScoreDto>(ok.Value);
|
||||
var dto = Assert.IsType<MatchScoreDto>(ok.Value);
|
||||
Assert.True(dto.HasEnoughSignal);
|
||||
Assert.True(dto.Score >= 75, $"expected strong score, got {dto.Score}");
|
||||
Assert.Contains("C#", dto.MatchedKeywords);
|
||||
@@ -181,7 +181,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var request = new JobApplicationsController.CreateJobApplicationRequest(
|
||||
var request = new CreateJobApplicationRequest(
|
||||
JobTitle: "Backend Dev",
|
||||
CompanyId: company.Id,
|
||||
Status: null,
|
||||
@@ -237,7 +237,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var request = new JobApplicationsController.UpdateJobApplicationRequest(
|
||||
var request = new UpdateJobApplicationRequest(
|
||||
JobTitle: "Backend Dev",
|
||||
CompanyId: company.Id,
|
||||
Status: "Applied",
|
||||
|
||||
@@ -92,7 +92,7 @@ public sealed class JobApplicationsFollowUpDraftTests
|
||||
var result = await controller.GetFollowUpDraft(job.Id, "waiting-update", null, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<JobApplicationsController.FollowUpDraftDto>(ok.Value);
|
||||
var payload = Assert.IsType<FollowUpDraftDto>(ok.Value);
|
||||
|
||||
Assert.Equal("Re: Backend Developer application update", payload.Subject);
|
||||
Assert.Contains("Maria", payload.Body);
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class JobApplicationsMariaDraftTests
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(null, null, " Recruiter hello "), CancellationToken.None);
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new SaveApplicationDraftsRequest(null, null, " Recruiter hello "), CancellationToken.None);
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
var saved = await db.JobApplications.FirstAsync();
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class JobApplicationsWorkflowSignalsTests
|
||||
var result = await controller.GetReadiness(job.Id, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<JobApplicationsController.ReadinessDto>(ok.Value);
|
||||
var payload = Assert.IsType<ReadinessDto>(ok.Value);
|
||||
|
||||
Assert.Equal("package-work", payload.WorkflowSignal.ActionKey);
|
||||
Assert.True(payload.WorkflowSignal.HasPackageGap);
|
||||
@@ -92,7 +92,7 @@ public sealed class JobApplicationsWorkflowSignalsTests
|
||||
var result = await controller.GetReminders(14, CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<List<JobApplicationsController.JobApplicationDto>>(ok.Value);
|
||||
var payload = Assert.IsType<List<JobApplicationDto>>(ok.Value);
|
||||
|
||||
var packageReminder = Assert.Single(payload, item => item.Id == packageGapJob.Id);
|
||||
Assert.Equal("package-work", packageReminder.WorkflowSignal.ActionKey);
|
||||
|
||||
Reference in New Issue
Block a user