188 lines
7.7 KiB
C#
188 lines
7.7 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));
|
|
}
|
|
|
|
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>>());
|
|
}
|
|
}
|