feat(email): add tenant draft persistence
Add owner-filtered, job-cascading private draft storage with reversible SQLite and MariaDB migration paths. No API or UI exposes draft content yet.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class EmailDraftPersistenceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Drafts_are_owner_filtered_and_follow_the_owned_job_lifecycle()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options;
|
||||
|
||||
int userOneJobId;
|
||||
int userTwoJobId;
|
||||
await using (var seed = Context(options, null))
|
||||
{
|
||||
await seed.Database.EnsureCreatedAsync();
|
||||
var companyOne = new Company { Name = "One", OwnerUserId = "user-1" };
|
||||
var companyTwo = new Company { Name = "Two", OwnerUserId = "user-2" };
|
||||
seed.Companies.AddRange(companyOne, companyTwo);
|
||||
await seed.SaveChangesAsync();
|
||||
var jobOne = new JobApplication { JobTitle = "Role one", CompanyId = companyOne.Id, OwnerUserId = "user-1" };
|
||||
var jobTwo = new JobApplication { JobTitle = "Role two", CompanyId = companyTwo.Id, OwnerUserId = "user-2" };
|
||||
seed.JobApplications.AddRange(jobOne, jobTwo);
|
||||
await seed.SaveChangesAsync();
|
||||
userOneJobId = jobOne.Id;
|
||||
userTwoJobId = jobTwo.Id;
|
||||
seed.EmailDrafts.AddRange(
|
||||
Draft("user-1", jobOne.Id, "one@example.test"),
|
||||
Draft("user-2", jobTwo.Id, "two@example.test"));
|
||||
await seed.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await using (var userOne = Context(options, "user-1"))
|
||||
{
|
||||
var visible = Assert.Single(await userOne.EmailDrafts.AsNoTracking().ToListAsync());
|
||||
Assert.Equal(userOneJobId, visible.JobApplicationId);
|
||||
Assert.Equal("one@example.test", visible.To);
|
||||
var job = await userOne.JobApplications.SingleAsync(item => item.Id == userOneJobId);
|
||||
userOne.JobApplications.Remove(job);
|
||||
await userOne.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await using var verify = Context(options, null);
|
||||
var remaining = Assert.Single(await verify.EmailDrafts.IgnoreQueryFilters().AsNoTracking().ToListAsync());
|
||||
Assert.Equal("user-2", remaining.OwnerUserId);
|
||||
Assert.Equal(userTwoJobId, remaining.JobApplicationId);
|
||||
}
|
||||
|
||||
private static EmailDraft Draft(string ownerUserId, int jobApplicationId, string recipient) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OwnerUserId = ownerUserId,
|
||||
JobApplicationId = jobApplicationId,
|
||||
Provider = "gmail",
|
||||
To = recipient,
|
||||
Subject = "Synthetic draft",
|
||||
BodyText = "Synthetic private draft body.",
|
||||
ThreadId = "thread-1",
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
private static JobTrackerContext Context(DbContextOptions<JobTrackerContext> options, string? userId)
|
||||
{
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
currentUser.SetupGet(service => service.UserId).Returns(userId);
|
||||
return new JobTrackerContext(options, currentUser.Object);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user