246 lines
9.4 KiB
C#
246 lines
9.4 KiB
C#
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
public sealed class ApplicationWorkspaceTests
|
|
{
|
|
private static (JobTrackerContext db, ApplicationWorkspaceService svc) New(string userId)
|
|
{
|
|
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
|
|
var currentUser = new Mock<ICurrentUserService>();
|
|
currentUser.SetupGet(s => s.UserId).Returns(userId);
|
|
var db = new JobTrackerContext(options, currentUser.Object);
|
|
return (db, new ApplicationWorkspaceService(db, new ApplicationChecklistService(db)));
|
|
}
|
|
|
|
// Milestone 2: the next action comes from the checklist, and every preparation item outranks the
|
|
// later categories. Seeding the parts a test is not asserting on keeps the assertion about one rule.
|
|
private static async Task CompletePreparationAsync(JobTrackerContext db, string owner, int jobId, params string[] except)
|
|
{
|
|
db.CareerProfiles.Add(new CareerProfile
|
|
{
|
|
OwnerUserId = owner,
|
|
Experiences = { new CareerExperience { OwnerUserId = owner, Title = "Dev", Company = "Acme" } },
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var svc = new ApplicationChecklistService(db);
|
|
var checklist = await svc.GetAsync(owner, jobId, default);
|
|
foreach (var item in checklist!.Items.Where(i =>
|
|
i.Category == ChecklistCategories.Preparation &&
|
|
i.Status == ChecklistStatuses.Pending &&
|
|
!except.Contains(i.SystemKey)))
|
|
{
|
|
await svc.UpdateAsync(owner, jobId, item.Id, new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);
|
|
}
|
|
}
|
|
|
|
private static async Task<JobApplication> SeedAsync(JobTrackerContext db, string owner, Action<JobApplication>? tweak = null)
|
|
{
|
|
var company = new Company { OwnerUserId = owner, Name = "Acme" };
|
|
db.Companies.Add(company);
|
|
await db.SaveChangesAsync();
|
|
var job = new JobApplication
|
|
{
|
|
OwnerUserId = owner,
|
|
CompanyId = company.Id,
|
|
JobTitle = "Backend Developer",
|
|
Status = "Applied",
|
|
Location = "Oslo",
|
|
Description = "Needs .NET and SQL.",
|
|
DateApplied = DateTime.UtcNow.AddDays(-3),
|
|
};
|
|
tweak?.Invoke(job);
|
|
db.JobApplications.Add(job);
|
|
await db.SaveChangesAsync();
|
|
return job;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_aggregates_the_application_without_copying_career_data()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1");
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.NotNull(o);
|
|
Assert.Equal("Backend Developer", o!.JobTitle);
|
|
Assert.Equal("Acme", o.Company);
|
|
Assert.Equal("Oslo", o.Location);
|
|
Assert.Equal("Needs .NET and SQL.", o.Description);
|
|
Assert.Equal(job.SavedAt, o.SavedAt);
|
|
Assert.True(o.HasJobDescription);
|
|
Assert.Null(o.Cv.VariantId); // no variant attached yet
|
|
Assert.False(o.HasCoverLetter);
|
|
Assert.Equal(0, o.DocumentCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_separates_application_answers_from_human_notes()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1", j =>
|
|
{
|
|
j.Notes = "Ask about the platform team.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nI enjoy solving customer problems.\n<<<END_APPLICATION_ANSWER_DRAFT>>>";
|
|
j.RecruiterMessageDraft = "Hello Maria";
|
|
});
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.Equal("Ask about the platform team.", o!.Notes);
|
|
Assert.Equal("I enjoy solving customer problems.", o.ApplicationAnswerDraft);
|
|
Assert.Equal("Hello Maria", o.RecruiterMessageDraft);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_surfaces_the_attached_cv_variant()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1");
|
|
db.CvVariants.Add(new CvVariant
|
|
{
|
|
OwnerUserId = "user-1",
|
|
JobApplicationId = job.Id,
|
|
PublicSlug = Guid.NewGuid().ToString("N"),
|
|
Name = "Backend CV",
|
|
SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings { ThemeId = "nordic" }),
|
|
UpdatedAtUtc = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.Equal("Backend CV", o!.Cv.VariantName);
|
|
Assert.Equal("nordic", o.Cv.ThemeId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_counts_documents_and_ai_history()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1");
|
|
db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "cert.pdf", FilePath = "/tmp/a.pdf", FileType = "PDF" });
|
|
db.AiInteractions.Add(new AiInteraction
|
|
{
|
|
OwnerUserId = "user-1", JobApplicationId = job.Id, Module = "job-analysis",
|
|
Title = "Job analysis", Provider = "p", ResultJson = "{}", CreatedAtUtc = DateTimeOffset.UtcNow,
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.Equal(1, o!.DocumentCount);
|
|
Assert.Equal(1, o.AiInteractionCount);
|
|
Assert.NotNull(o.LastAiAtUtc);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_returns_recent_activity_newest_first()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1");
|
|
db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "Created", At = DateTime.Now.AddDays(-2) });
|
|
db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "StatusChanged", NewValue = "Applied", At = DateTime.Now });
|
|
await db.SaveChangesAsync();
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.Equal(new[] { "StatusChanged", "Created" }, o!.RecentActivity.Select(a => a.Type));
|
|
}
|
|
|
|
// --- next recommended action: "the user should never ask what to do next" ---
|
|
|
|
[Fact]
|
|
public async Task Next_step_asks_for_the_advert_when_there_is_no_description()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1", j => j.Description = null);
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.Equal("review-job-details", o!.NextStep!.Key);
|
|
Assert.Equal("job-details", o.NextStep.Section);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Next_step_walks_cv_then_cover_letter_then_documents()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1");
|
|
await CompletePreparationAsync(db, "user-1", job.Id,
|
|
"prepare-cv", "create-cover-letter", "attach-supporting-documents");
|
|
|
|
Assert.Equal("prepare-cv", (await svc.GetOverviewAsync("user-1", job.Id, default))!.NextStep!.Key);
|
|
|
|
job.TailoredCvText = "tailored";
|
|
await db.SaveChangesAsync();
|
|
Assert.Equal("create-cover-letter", (await svc.GetOverviewAsync("user-1", job.Id, default))!.NextStep!.Key);
|
|
|
|
job.CoverLetterText = "Dear team";
|
|
await db.SaveChangesAsync();
|
|
Assert.Equal("attach-supporting-documents", (await svc.GetOverviewAsync("user-1", job.Id, default))!.NextStep!.Key);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Next_step_reaches_interview_prep_once_preparation_is_done()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1", j =>
|
|
{
|
|
j.Status = "Interview";
|
|
j.TailoredCvText = "tailored";
|
|
j.CoverLetterText = "letter";
|
|
j.FollowUpAt = DateTime.UtcNow.AddDays(3);
|
|
j.NextAction = "Confirm the interview slot";
|
|
});
|
|
db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "c.pdf", FilePath = "/tmp/c.pdf", FileType = "PDF" });
|
|
await db.SaveChangesAsync();
|
|
await CompletePreparationAsync(db, "user-1", job.Id);
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.Equal("prepare-interview-notes", o!.NextStep!.Key);
|
|
Assert.Equal("interview", o.NextStep.Section);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_reports_checklist_progress()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var job = await SeedAsync(db, "user-1");
|
|
|
|
var o = await svc.GetOverviewAsync("user-1", job.Id, default);
|
|
|
|
Assert.NotNull(o!.ChecklistProgress);
|
|
Assert.True(o.ChecklistProgress!.Total > 0);
|
|
// The advert is present, so that item auto-completed from the same signal readiness uses.
|
|
Assert.True(o.ChecklistProgress.Completed > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Another_users_application_is_not_visible()
|
|
{
|
|
var (db, svc) = New("user-1");
|
|
await using var _ = db;
|
|
var other = await SeedAsync(db, "user-2");
|
|
|
|
Assert.Null(await svc.GetOverviewAsync("user-1", other.Id, default));
|
|
}
|
|
}
|