chore(M001/S01): auto-commit after complete-slice
This commit is contained in:
@@ -24,7 +24,7 @@ public sealed class AuthAndSystemControllerTests
|
||||
|
||||
var controller = new AuthController(BuildConfig(), userManager.Object, Mock.Of<ITokenService>(), Mock.Of<IAppEmailSender>(), Mock.Of<IGoogleTokenValidator>());
|
||||
|
||||
var result = await controller.UpdateProfile(new AuthController.UpdateProfileRequest(" new@example.com ", " newuser ", " Ada ", " Lovelace ", " Ada L. "));
|
||||
var result = await controller.UpdateProfile(new AuthController.UpdateProfileRequest(" new@example.com ", " newuser ", " Ada ", " Lovelace ", " Ada L. ", null, null));
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
Assert.Equal("new@example.com", user.Email);
|
||||
|
||||
@@ -315,6 +315,164 @@ public sealed class GmailControllerTests
|
||||
Assert.All(storedMessages, message => Assert.Equal("thread-1", message.ExternalThreadId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_linked_threads_imports_new_messages_for_known_thread_ids()
|
||||
{
|
||||
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"
|
||||
};
|
||||
db.JobApplications.Add(job);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.Correspondences.Add(new Correspondence
|
||||
{
|
||||
JobApplicationId = job.Id,
|
||||
From = "Company",
|
||||
Subject = "Initial recruiter note",
|
||||
ExternalMessageId = "msg-1",
|
||||
ExternalThreadId = "thread-1",
|
||||
ExternalFrom = "Maria Recruiter <maria@acme.test>",
|
||||
ExternalTo = "user@example.test",
|
||||
Content = "Existing import"
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var gmail = new Mock<IGmailOAuthService>();
|
||||
gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
|
||||
gmail.Setup(service => service.ListThreadMessagesAsync("user-1", "thread-1", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new[]
|
||||
{
|
||||
new GmailMessageSummary("msg-1", "thread-1", "Initial recruiter note", "Maria Recruiter <maria@acme.test>", "user@example.test", DateTimeOffset.UtcNow.AddDays(-2), "Old message"),
|
||||
new GmailMessageSummary("msg-2", "thread-1", "Follow-up reply", "user@example.test", "Maria Recruiter <maria@acme.test>", DateTimeOffset.UtcNow, "New reply")
|
||||
});
|
||||
gmail.Setup(service => service.GetMessageAsync("user-1", "msg-2", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GmailMessageDetail(
|
||||
"msg-2",
|
||||
"thread-1",
|
||||
"Follow-up reply",
|
||||
"user@example.test",
|
||||
"Maria Recruiter <maria@acme.test>",
|
||||
DateTimeOffset.UtcNow,
|
||||
"New reply",
|
||||
"Reply body",
|
||||
null));
|
||||
|
||||
var controller = CreateController(db, gmail.Object, "user-1");
|
||||
var result = await controller.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(job.Id), CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||
var payload = Assert.IsType<GmailController.GmailThreadRefreshResultDto>(ok.Value);
|
||||
Assert.Equal(job.Id, payload.JobApplicationId);
|
||||
Assert.Equal(1, payload.ThreadsChecked);
|
||||
Assert.Equal(1, payload.Imported);
|
||||
Assert.Equal(1, payload.Skipped);
|
||||
Assert.True(payload.HasLinkedThreads);
|
||||
var thread = Assert.Single(payload.Threads);
|
||||
Assert.Equal("thread-1", thread.ThreadId);
|
||||
Assert.Equal("imported-new-messages", thread.Status);
|
||||
Assert.Equal(2, thread.TotalMessages);
|
||||
|
||||
var stored = await db.Correspondences.Where(message => message.JobApplicationId == job.Id).OrderBy(message => message.ExternalMessageId).ToListAsync();
|
||||
Assert.Equal(2, stored.Count);
|
||||
Assert.Equal("msg-2", stored[1].ExternalMessageId);
|
||||
Assert.Equal("thread-1", stored[1].ExternalThreadId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_linked_threads_reports_empty_and_disconnected_states()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
|
||||
db.Companies.Add(company);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var linkedJob = new JobApplication { JobTitle = "Backend Developer", CompanyId = company.Id, OwnerUserId = "user-1" };
|
||||
var emptyJob = new JobApplication { JobTitle = "Backend Developer II", CompanyId = company.Id, OwnerUserId = "user-1" };
|
||||
db.JobApplications.AddRange(linkedJob, emptyJob);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.Correspondences.Add(new Correspondence
|
||||
{
|
||||
JobApplicationId = linkedJob.Id,
|
||||
From = "Company",
|
||||
Subject = "Initial recruiter note",
|
||||
ExternalMessageId = "msg-1",
|
||||
ExternalThreadId = "thread-1",
|
||||
Content = "Existing import"
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var disconnectedGmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
|
||||
disconnectedGmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>())).ReturnsAsync((GmailConnection?)null);
|
||||
var disconnectedController = CreateController(db, disconnectedGmail.Object, "user-1");
|
||||
var disconnectedResult = await disconnectedController.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(linkedJob.Id), CancellationToken.None);
|
||||
var conflict = Assert.IsType<ConflictObjectResult>(disconnectedResult.Result);
|
||||
Assert.Equal("Connect Gmail before refreshing linked threads.", conflict.Value);
|
||||
|
||||
var gmail = new Mock<IGmailOAuthService>();
|
||||
gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
|
||||
var controller = CreateController(db, gmail.Object, "user-1");
|
||||
var emptyResult = await controller.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(emptyJob.Id), CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(emptyResult.Result);
|
||||
var payload = Assert.IsType<GmailController.GmailThreadRefreshResultDto>(ok.Value);
|
||||
Assert.Equal(0, payload.ThreadsChecked);
|
||||
Assert.Equal(0, payload.Imported);
|
||||
Assert.Equal(0, payload.Skipped);
|
||||
Assert.False(payload.HasLinkedThreads);
|
||||
Assert.Empty(payload.Threads);
|
||||
gmail.Verify(service => service.ListThreadMessagesAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_linked_threads_rejects_invalid_job_id()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var gmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
|
||||
var controller = CreateController(db, gmail.Object, "user-1");
|
||||
|
||||
var result = await controller.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(0), CancellationToken.None);
|
||||
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
|
||||
Assert.Equal("Valid jobApplicationId is required.", badRequest.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_linked_threads_respects_owned_job_scope()
|
||||
{
|
||||
await using var db = CreateDb();
|
||||
var company = new Company { Name = "OtherCo", OwnerUserId = "user-2" };
|
||||
db.Companies.Add(company);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var foreignJob = new JobApplication
|
||||
{
|
||||
JobTitle = "Hidden job",
|
||||
CompanyId = company.Id,
|
||||
OwnerUserId = "user-2"
|
||||
};
|
||||
db.JobApplications.Add(foreignJob);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var gmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
|
||||
var controller = CreateController(db, gmail.Object, "user-1");
|
||||
var result = await controller.RefreshLinkedThreads(new GmailController.RefreshLinkedThreadsRequest(foreignJob.Id), CancellationToken.None);
|
||||
|
||||
var notFound = Assert.IsType<NotFoundObjectResult>(result.Result);
|
||||
Assert.Equal("Job application not found.", notFound.Value);
|
||||
gmail.Verify(service => service.GetConnectionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Job_candidates_respects_owned_job_scope()
|
||||
{
|
||||
|
||||
@@ -26,7 +26,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 "), CancellationToken.None);
|
||||
var result = await controller.SaveApplicationDrafts(job.Id, new JobApplicationsController.SaveApplicationDraftsRequest(" Cover letter body ", " Notes body ", null), CancellationToken.None);
|
||||
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
var saved = await db.JobApplications.FirstAsync();
|
||||
@@ -48,7 +48,7 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = CreateController(db, "user-1");
|
||||
var result = await controller.GenerateApplicationPackage(job.Id, null, CancellationToken.None);
|
||||
var result = await controller.GenerateApplicationPackage(job.Id, null, null, null, CancellationToken.None);
|
||||
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
|
||||
Assert.Contains("Profile page", badRequest.Value?.ToString());
|
||||
@@ -58,8 +58,9 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
{
|
||||
var summarizer = new Mock<ISummarizerService>();
|
||||
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
||||
var users = CreateUserManager();
|
||||
|
||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>());
|
||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>(), users.Object);
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
@@ -73,6 +74,21 @@ public sealed class JobApplicationsEndpointBehaviorTests
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static Mock<Microsoft.AspNetCore.Identity.UserManager<ApplicationUser>> CreateUserManager()
|
||||
{
|
||||
var store = new Mock<Microsoft.AspNetCore.Identity.IUserStore<ApplicationUser>>();
|
||||
return new Mock<Microsoft.AspNetCore.Identity.UserManager<ApplicationUser>>(
|
||||
store.Object,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!);
|
||||
}
|
||||
|
||||
private static JobTrackerContext CreateDb()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||||
|
||||
@@ -37,8 +37,9 @@ public sealed class JobApplicationsMariaDraftTests
|
||||
{
|
||||
var summarizer = new Mock<ISummarizerService>();
|
||||
summarizer.Setup(x => x.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync("generated text");
|
||||
var users = CreateUserManager();
|
||||
|
||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>());
|
||||
var controller = new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>(), users.Object);
|
||||
controller.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext
|
||||
@@ -49,6 +50,21 @@ public sealed class JobApplicationsMariaDraftTests
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static Mock<Microsoft.AspNetCore.Identity.UserManager<ApplicationUser>> CreateUserManager()
|
||||
{
|
||||
var store = new Mock<Microsoft.AspNetCore.Identity.IUserStore<ApplicationUser>>();
|
||||
return new Mock<Microsoft.AspNetCore.Identity.UserManager<ApplicationUser>>(
|
||||
store.Object,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!,
|
||||
null!);
|
||||
}
|
||||
|
||||
private static JobTrackerContext CreateDb()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class ProductionConfigTests
|
||||
[Fact]
|
||||
public void Summarizer_metrics_include_runtime_device_details()
|
||||
{
|
||||
var ctor = typeof(SummarizerMetrics).GetConstructors().Single();
|
||||
var ctor = typeof(AiServiceMetrics).GetConstructors().Single();
|
||||
var parameterNames = ctor.GetParameters().Select(x => x.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Assert.Contains("device", parameterNames);
|
||||
|
||||
@@ -22,8 +22,9 @@ public sealed class ProfileCvControllerTests
|
||||
var user = new ApplicationUser();
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
|
||||
var controller = new ProfileCvController(userManager.Object)
|
||||
var controller = new ProfileCvController(userManager.Object, aiService.Object)
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
|
||||
};
|
||||
@@ -32,7 +33,7 @@ public sealed class ProfileCvControllerTests
|
||||
var result = await controller.Upload(file);
|
||||
|
||||
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
|
||||
Assert.Contains("supported", StringComparison.OrdinalIgnoreCase, badRequest.Value?.ToString());
|
||||
Assert.True((badRequest.Value?.ToString() ?? string.Empty).Contains("supported", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -42,8 +43,12 @@ public sealed class ProfileCvControllerTests
|
||||
var userManager = CreateUserManager();
|
||||
userManager.Setup(x => x.GetUserAsync(It.IsAny<ClaimsPrincipal>())).ReturnsAsync(user);
|
||||
userManager.Setup(x => x.UpdateAsync(user)).ReturnsAsync(IdentityResult.Success);
|
||||
var aiService = new Mock<ISummarizerService>();
|
||||
aiService
|
||||
.Setup(x => x.ExtractTextAsync(It.IsAny<Stream>(), It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new AiTextExtractionResult("# CV\nBuilt APIs and UIs", false, "text/markdown", null, 22, "resume.md"));
|
||||
|
||||
var controller = new ProfileCvController(userManager.Object)
|
||||
var controller = new ProfileCvController(userManager.Object, aiService.Object)
|
||||
{
|
||||
ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user