Files
jobtrackingapp/JobTrackerApi.Tests/JobApplicationsApplicationPackageTests.cs

278 lines
11 KiB
C#

using System.Security.Claims;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace JobTrackerApi.Tests;
public sealed class JobApplicationsApplicationPackageTests
{
[Fact]
public async Task Save_application_drafts_replaces_notes_instead_of_appending()
{
await using var db = CreateDb();
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",
Notes = "Old notes"
};
db.JobApplications.Add(job);
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);
Assert.IsType<NoContentResult>(result);
var saved = await db.JobApplications.FirstAsync();
Assert.Equal("Updated notes block", saved.Notes);
}
[Fact]
public async Task Generate_application_package_uses_imported_correspondence_and_recruiter_context()
{
await using var db = CreateDb();
var company = new Company
{
Name = "Acme",
RecruiterName = "Maria Recruiter",
RecruiterEmail = "maria@acme.test",
OwnerUserId = "user-1"
};
db.Companies.Add(company);
db.Users.Add(new ApplicationUser
{
Id = "user-1",
UserName = "user@example.test",
Email = "user@example.test",
ProfileCvText = "Built .NET APIs and led backend delivery.",
ProfileCvStructureJson = "[]"
});
await db.SaveChangesAsync();
var job = new JobApplication
{
JobTitle = "Backend Developer",
CompanyId = company.Id,
OwnerUserId = "user-1",
Description = "Need .NET, APIs, and async collaboration with recruiters.",
Notes = "Priority role",
ShortSummary = "Acme backend hiring"
};
db.JobApplications.Add(job);
await db.SaveChangesAsync();
db.Correspondences.Add(new Correspondence
{
JobApplicationId = job.Id,
From = "Company",
Subject = "Backend Developer interview",
ExternalThreadId = "thread-1",
ExternalFrom = "Maria Recruiter <maria@acme.test>",
ExternalTo = "user@example.test",
Content = "We want someone who can own .NET APIs and communicate clearly with stakeholders.",
Date = DateTime.UtcNow.AddDays(-1)
});
await db.SaveChangesAsync();
var summarizer = new Mock<ISummarizerService>();
summarizer
.Setup(service => service.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.ReturnsAsync((string instruction, string context, int _, int __) =>
{
if (instruction.Contains("List up to 4 concrete application-package signals", StringComparison.OrdinalIgnoreCase))
{
return "Use recruiter language about owning .NET APIs.\nMention the interview timeline from imported correspondence.";
}
if (instruction.Contains("cover letter", StringComparison.OrdinalIgnoreCase))
{
return context.Contains("Imported correspondence context:", StringComparison.OrdinalIgnoreCase)
&& context.Contains("Maria Recruiter", StringComparison.OrdinalIgnoreCase)
? "Cover letter tailored with recruiter context and imported correspondence."
: "Generic cover letter.";
}
if (instruction.Contains("recruiter intro message", StringComparison.OrdinalIgnoreCase))
{
return context.Contains("Recruiter email: maria@acme.test", StringComparison.OrdinalIgnoreCase)
? "Recruiter message that uses Maria's context."
: "Generic recruiter message.";
}
if (instruction.Contains("application answer", StringComparison.OrdinalIgnoreCase))
{
return "Application answer grounded in the imported thread.";
}
if (instruction.Contains("Rewrite the candidate CV", StringComparison.OrdinalIgnoreCase))
{
return "Tailored CV that highlights .NET API ownership for Acme.";
}
return "Variant draft";
});
var controller = CreateController(db, summarizer.Object, "user-1");
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);
Assert.Contains("Tailored CV", payload.TailoredCvText);
Assert.Equal("Cover letter tailored with recruiter context and imported correspondence.", payload.CoverLetterDraft);
Assert.Equal("Recruiter message that uses Maria's context.", payload.RecruiterMessageDraft);
Assert.Contains(payload.KeyPoints, item => item.Contains("interview timeline", StringComparison.OrdinalIgnoreCase));
Assert.Contains(payload.KeyPoints, item => item.Contains("recruiter language", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task Generate_application_package_passes_typed_structured_cv_context_to_summarizer()
{
await using var db = CreateDb();
var company = new Company
{
Name = "Acme",
OwnerUserId = "user-1"
};
db.Companies.Add(company);
db.Users.Add(new ApplicationUser
{
Id = "user-1",
UserName = "user@example.test",
Email = "user@example.test",
ProfileCvText = "Built APIs and shipped backend work.",
ProfileCvStructureJson = """
{
"version": "1",
"contact": {
"fullName": "Demo User",
"headline": "Backend Developer",
"email": "user@example.test",
"location": "Oslo"
},
"summary": ["Backend-focused developer with strong API delivery experience."],
"jobs": [
{
"title": "System Developer",
"company": "Acme Consulting",
"location": "Oslo",
"start": "2020",
"end": "2024",
"isCurrent": false,
"bullets": ["Owned .NET API delivery across multiple services."],
"skills": [".NET", "SQL", "APIs"]
}
],
"education": [],
"skills": [".NET", "SQL", "APIs"],
"languages": [{ "name": "English", "level": "Native" }],
"interests": [],
"otherSections": []
}
"""
});
await db.SaveChangesAsync();
var job = new JobApplication
{
JobTitle = "Backend Developer",
CompanyId = company.Id,
OwnerUserId = "user-1",
Description = "Need .NET API ownership and strong SQL skills."
};
db.JobApplications.Add(job);
await db.SaveChangesAsync();
string? capturedContext = null;
var summarizer = new Mock<ISummarizerService>();
summarizer
.Setup(service => service.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.ReturnsAsync((string instruction, string context, int _, int __) =>
{
if (instruction.Contains("Rewrite the candidate CV", StringComparison.OrdinalIgnoreCase))
{
capturedContext = context;
return "Tailored CV";
}
if (instruction.Contains("List up to 4 concrete application-package signals", StringComparison.OrdinalIgnoreCase))
{
return "Lead with .NET API ownership.";
}
return "Draft";
});
var controller = CreateController(db, summarizer.Object, "user-1");
var result = await controller.GenerateApplicationPackage(job.Id, null, null, null, CancellationToken.None);
Assert.IsType<OkObjectResult>(result.Result);
Assert.NotNull(capturedContext);
Assert.Contains("Structured CV:", capturedContext);
Assert.Contains("Name: Demo User", capturedContext);
Assert.Contains("Skills:\n.NET, SQL, APIs", capturedContext);
Assert.Contains("Work Experience:", capturedContext);
Assert.Contains("Owned .NET API delivery across multiple services.", capturedContext);
}
private static JobApplicationsController CreateController(JobTrackerContext db, ISummarizerService summarizer, string userId)
{
var controller = new JobApplicationsController(db, summarizer, Mock.Of<IAppEmailSender>(), CreateUserManager().Object, NullLogger<JobApplicationsController>.Instance);
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()
{
var options = new DbContextOptionsBuilder<JobTrackerContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var currentUser = new Mock<ICurrentUserService>();
currentUser.SetupGet(service => service.UserId).Returns("user-1");
return new JobTrackerContext(options, currentUser.Object);
}
private static Mock<UserManager<ApplicationUser>> CreateUserManager()
{
var store = new Mock<IUserStore<ApplicationUser>>();
return new Mock<UserManager<ApplicationUser>>(
store.Object,
Options.Create(new IdentityOptions()),
new PasswordHasher<ApplicationUser>(),
Array.Empty<IUserValidator<ApplicationUser>>(),
Array.Empty<IPasswordValidator<ApplicationUser>>(),
new UpperInvariantLookupNormalizer(),
new IdentityErrorDescriber(),
null!,
new NullLogger<UserManager<ApplicationUser>>());
}
}