455 lines
24 KiB
C#
455 lines
24 KiB
C#
using System.Reflection;
|
|
using System.Security.Claims;
|
|
using JobTrackerApi.Controllers;
|
|
using JobTrackerApi.Data;
|
|
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using JobTrackerApi.Services.EmailProviders;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace JobTrackerApi.Tests;
|
|
|
|
public sealed class EmailDraftsControllerTests
|
|
{
|
|
[Fact]
|
|
public void Draft_routes_require_local_authentication()
|
|
{
|
|
var authorization = Assert.Single(typeof(EmailDraftsController)
|
|
.GetCustomAttributes<AuthorizeAttribute>());
|
|
Assert.Equal("local", authorization.AuthenticationSchemes);
|
|
Assert.True(string.IsNullOrWhiteSpace(authorization.Policy));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_accepts_incomplete_owned_drafts_but_rejects_invalid_or_foreign_inputs()
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync();
|
|
await using var db = fixture.Context("user-1");
|
|
var controller = Controller(db, "user-1");
|
|
|
|
var createdResult = await controller.Create(
|
|
new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "GMAIL", "", "", "", null), default);
|
|
var created = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<CreatedAtActionResult>(createdResult.Result).Value);
|
|
Assert.Equal("gmail", created.Provider);
|
|
Assert.Equal(1, created.Revision);
|
|
Assert.Empty(created.To);
|
|
Assert.True(Guid.TryParse(created.ClientRequestId, out _));
|
|
|
|
Assert.IsType<BadRequestObjectResult>((await controller.Create(
|
|
new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "unknown", "", "", "", null), default)).Result);
|
|
Assert.IsType<BadRequestObjectResult>((await controller.Create(
|
|
new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "gmail", "not-an-address", "", "", null), default)).Result);
|
|
Assert.IsType<NotFoundResult>((await controller.Create(
|
|
new EmailDraftsController.CreateDraftRequest(fixture.UserTwoJobId, "gmail", "", "", "", null), default)).Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Direct_ids_and_job_lists_do_not_cross_tenants()
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
|
|
await using var db = fixture.Context("user-1");
|
|
var controller = Controller(db, "user-1");
|
|
|
|
Assert.IsType<NotFoundResult>((await controller.Get(fixture.UserTwoDraftId, default)).Result);
|
|
var foreignList = Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
|
|
Assert.IsType<OkObjectResult>((await controller.List(fixture.UserTwoJobId, default)).Result).Value);
|
|
Assert.Empty(foreignList);
|
|
var ownList = Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
|
|
Assert.IsType<OkObjectResult>((await controller.List(fixture.UserOneJobId, default)).Result).Value);
|
|
Assert.Equal(fixture.UserOneDraftId, Assert.Single(ownList).Id);
|
|
var allOwnDrafts = Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
|
|
Assert.IsType<OkObjectResult>((await controller.List(null, default)).Result).Value);
|
|
Assert.Equal(fixture.UserOneDraftId, Assert.Single(allOwnDrafts).Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Revision_conflicts_prevent_lost_updates_and_cross_tenant_deletes()
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
|
|
await using (var userOneDb = fixture.Context("user-1"))
|
|
{
|
|
var userOne = Controller(userOneDb, "user-1");
|
|
var originalRequestId = (await userOneDb.EmailDrafts.AsNoTracking().SingleAsync()).ClientRequestId;
|
|
var updatedResult = await userOne.Update(fixture.UserOneDraftId,
|
|
new EmailDraftsController.UpdateDraftRequest(1, "new@example.test", "Updated", "Updated body"), default);
|
|
var updated = Assert.IsType<EmailDraftsController.DraftDto>(Assert.IsType<OkObjectResult>(updatedResult.Result).Value);
|
|
Assert.Equal(2, updated.Revision);
|
|
Assert.Equal("Updated body", updated.BodyText);
|
|
Assert.Equal(originalRequestId, updated.ClientRequestId);
|
|
|
|
Assert.IsType<ConflictObjectResult>((await userOne.Update(fixture.UserOneDraftId,
|
|
new EmailDraftsController.UpdateDraftRequest(1, "stale@example.test", "Stale", "Stale body"), default)).Result);
|
|
Assert.IsType<ConflictObjectResult>(await userOne.Delete(fixture.UserOneDraftId, 1, default));
|
|
}
|
|
|
|
await using (var userTwoDb = fixture.Context("user-2"))
|
|
{
|
|
var userTwo = Controller(userTwoDb, "user-2");
|
|
Assert.IsType<NotFoundResult>((await userTwo.Update(fixture.UserOneDraftId,
|
|
new EmailDraftsController.UpdateDraftRequest(2, "other@example.test", "Other", "Other body"), default)).Result);
|
|
Assert.IsType<NotFoundResult>(await userTwo.Delete(fixture.UserOneDraftId, 2, default));
|
|
}
|
|
|
|
await using (var userOneDb = fixture.Context("user-1"))
|
|
Assert.IsType<NoContentResult>(await Controller(userOneDb, "user-1").Delete(fixture.UserOneDraftId, 2, default));
|
|
|
|
await using var verify = fixture.Context(null);
|
|
var remaining = Assert.Single(await verify.EmailDrafts.IgnoreQueryFilters().AsNoTracking().ToListAsync());
|
|
Assert.Equal("user-2", remaining.OwnerUserId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task New_attempt_identity_requires_the_matching_definitive_failure()
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
|
|
await using var db = fixture.Context("user-1");
|
|
var controller = Controller(db, "user-1");
|
|
var original = await db.EmailDrafts.AsNoTracking().SingleAsync();
|
|
|
|
var refused = await controller.NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(1), default);
|
|
Assert.IsType<ConflictObjectResult>(refused.Result);
|
|
var failedAttempt = new EmailSendAttempt
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = "user-1",
|
|
JobApplicationId = original.JobApplicationId,
|
|
Provider = original.Provider,
|
|
ClientRequestId = original.ClientRequestId,
|
|
PayloadHash = new string('a', 64),
|
|
Status = EmailSendStatuses.Failed,
|
|
FailureCategory = "provider_rejected",
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
CompletedAtUtc = DateTime.UtcNow,
|
|
};
|
|
db.EmailSendAttempts.Add(failedAttempt);
|
|
await db.SaveChangesAsync();
|
|
|
|
var rotatedResult = await controller.NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(1), default);
|
|
var rotated = Assert.IsType<EmailDraftsController.DraftDto>(Assert.IsType<OkObjectResult>(rotatedResult.Result).Value);
|
|
Assert.Equal(2, rotated.Revision);
|
|
Assert.NotEqual(original.ClientRequestId, rotated.ClientRequestId);
|
|
Assert.True(Guid.TryParse(rotated.ClientRequestId, out _));
|
|
|
|
Assert.IsType<ConflictObjectResult>((await controller.NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(1), default)).Result);
|
|
await using var otherDb = fixture.Context("user-2");
|
|
Assert.IsType<NotFoundResult>((await Controller(otherDb, "user-2").NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(2), default)).Result);
|
|
|
|
await using var verify = fixture.Context("user-1");
|
|
var persisted = await verify.EmailDrafts.AsNoTracking().SingleAsync();
|
|
AssertDraftContentUnchanged(original, persisted);
|
|
Assert.Equal(rotated.ClientRequestId, persisted.ClientRequestId);
|
|
Assert.Equal(2, persisted.Revision);
|
|
var readback = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<OkObjectResult>((await Controller(verify, "user-1").Get(original.Id, default)).Result).Value);
|
|
Assert.Equal(rotated, readback);
|
|
AssertAttemptUnchanged(failedAttempt, Assert.Single(await verify.EmailSendAttempts.AsNoTracking().ToListAsync()));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(EmailSendStatuses.Pending)]
|
|
[InlineData(EmailSendStatuses.Sending)]
|
|
[InlineData(EmailSendStatuses.Uncertain)]
|
|
[InlineData(EmailSendStatuses.Sent)]
|
|
public async Task Nonfailed_attempts_cannot_rotate_identity_or_change_draft_and_history(string status)
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
|
|
await using var db = fixture.Context("user-1");
|
|
var original = await db.EmailDrafts.AsNoTracking().SingleAsync();
|
|
var attempt = new EmailSendAttempt
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = original.OwnerUserId,
|
|
JobApplicationId = original.JobApplicationId,
|
|
Provider = original.Provider,
|
|
ClientRequestId = original.ClientRequestId,
|
|
PayloadHash = new string('a', 64),
|
|
Status = status,
|
|
ProviderMessageId = status == EmailSendStatuses.Sent ? "sent-message" : null,
|
|
FailureCategory = status == EmailSendStatuses.Uncertain ? "delivery_unknown" : null,
|
|
CreatedAtUtc = original.CreatedAtUtc,
|
|
StartedAtUtc = status == EmailSendStatuses.Pending ? null : original.UpdatedAtUtc,
|
|
CompletedAtUtc = status is EmailSendStatuses.Uncertain or EmailSendStatuses.Sent ? original.UpdatedAtUtc : null,
|
|
};
|
|
db.EmailSendAttempts.Add(attempt);
|
|
await db.SaveChangesAsync();
|
|
|
|
var result = await Controller(db, "user-1").NewAttempt(original.Id,
|
|
new EmailDraftsController.NewAttemptRequest(original.Revision), default);
|
|
Assert.IsType<ConflictObjectResult>(result.Result);
|
|
|
|
await using var verify = fixture.Context("user-1");
|
|
var persisted = await verify.EmailDrafts.AsNoTracking().SingleAsync();
|
|
AssertDraftContentUnchanged(original, persisted);
|
|
Assert.Equal(original.ClientRequestId, persisted.ClientRequestId);
|
|
Assert.Equal(original.Revision, persisted.Revision);
|
|
Assert.Equal(original.UpdatedAtUtc, persisted.UpdatedAtUtc);
|
|
AssertAttemptUnchanged(attempt, Assert.Single(await verify.EmailSendAttempts.AsNoTracking().ToListAsync()));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public async Task Failures_with_another_owner_or_request_key_cannot_rotate_or_change_either_tenant(bool wrongOwner)
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
|
|
await using var seed = fixture.Context(null);
|
|
var originals = await seed.EmailDrafts.IgnoreQueryFilters().AsNoTracking().OrderBy(draft => draft.OwnerUserId).ToListAsync();
|
|
var ownDraft = originals.Single(draft => draft.OwnerUserId == "user-1");
|
|
var foreignDraft = originals.Single(draft => draft.OwnerUserId == "user-2");
|
|
var attempt = new EmailSendAttempt
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = wrongOwner ? foreignDraft.OwnerUserId : ownDraft.OwnerUserId,
|
|
JobApplicationId = wrongOwner ? foreignDraft.JobApplicationId : ownDraft.JobApplicationId,
|
|
Provider = ownDraft.Provider,
|
|
ClientRequestId = wrongOwner ? ownDraft.ClientRequestId : foreignDraft.ClientRequestId,
|
|
PayloadHash = new string('b', 64),
|
|
Status = EmailSendStatuses.Failed,
|
|
FailureCategory = "provider_rejected",
|
|
CreatedAtUtc = ownDraft.CreatedAtUtc,
|
|
StartedAtUtc = ownDraft.UpdatedAtUtc,
|
|
CompletedAtUtc = ownDraft.UpdatedAtUtc,
|
|
};
|
|
seed.EmailSendAttempts.Add(attempt);
|
|
await seed.SaveChangesAsync();
|
|
|
|
await using var db = fixture.Context("user-1");
|
|
var result = await Controller(db, "user-1").NewAttempt(ownDraft.Id,
|
|
new EmailDraftsController.NewAttemptRequest(ownDraft.Revision), default);
|
|
Assert.IsType<ConflictObjectResult>(result.Result);
|
|
|
|
await using var verify = fixture.Context(null);
|
|
var persistedDrafts = await verify.EmailDrafts.IgnoreQueryFilters().AsNoTracking().OrderBy(draft => draft.OwnerUserId).ToListAsync();
|
|
Assert.Equal(originals.Count, persistedDrafts.Count);
|
|
foreach (var original in originals)
|
|
{
|
|
var persisted = persistedDrafts.Single(draft => draft.Id == original.Id);
|
|
AssertDraftContentUnchanged(original, persisted);
|
|
Assert.Equal(original.ClientRequestId, persisted.ClientRequestId);
|
|
Assert.Equal(original.Revision, persisted.Revision);
|
|
Assert.Equal(original.UpdatedAtUtc, persisted.UpdatedAtUtc);
|
|
}
|
|
AssertAttemptUnchanged(attempt, Assert.Single(await verify.EmailSendAttempts.IgnoreQueryFilters().AsNoTracking().ToListAsync()));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Draft_DTO_timestamps_keep_UTC_shape_and_exact_ticks_after_SQLite_roundtrips()
|
|
{
|
|
await using var fixture = await Fixture.CreateAsync();
|
|
await using var createDb = fixture.Context("user-1");
|
|
var created = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<CreatedAtActionResult>((await Controller(createDb, "user-1").Create(
|
|
new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "gmail", "one@example.test", "UTC subject", "UTC body", "utc-thread"), default)).Result).Value);
|
|
AssertUtcTimestamps(created);
|
|
|
|
await using var readDb = fixture.Context("user-1");
|
|
var beforeReads = await readDb.EmailDrafts.AsNoTracking().SingleAsync();
|
|
var reader = Controller(readDb, "user-1");
|
|
var retrieved = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<OkObjectResult>((await reader.Get(created.Id, default)).Result).Value);
|
|
var listed = Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
|
|
Assert.IsType<OkObjectResult>((await reader.List(fixture.UserOneJobId, default)).Result).Value));
|
|
|
|
await using var verifyReads = fixture.Context("user-1");
|
|
var afterReads = await verifyReads.EmailDrafts.AsNoTracking().SingleAsync();
|
|
AssertDraftContentUnchanged(beforeReads, afterReads);
|
|
Assert.Equal(beforeReads.ClientRequestId, afterReads.ClientRequestId);
|
|
Assert.Equal(beforeReads.Revision, afterReads.Revision);
|
|
Assert.Equal(beforeReads.UpdatedAtUtc, afterReads.UpdatedAtUtc);
|
|
Assert.Equal(beforeReads.CreatedAtUtc.Kind, afterReads.CreatedAtUtc.Kind);
|
|
Assert.Equal(beforeReads.UpdatedAtUtc.Kind, afterReads.UpdatedAtUtc.Kind);
|
|
Assert.Empty(await verifyReads.EmailSendAttempts.AsNoTracking().ToListAsync());
|
|
Assert.Equal(created, retrieved);
|
|
Assert.Equal(created, listed);
|
|
Assert.Equal(created.CreatedAtUtc.Ticks, retrieved.CreatedAtUtc.Ticks);
|
|
Assert.Equal(created.UpdatedAtUtc.Ticks, retrieved.UpdatedAtUtc.Ticks);
|
|
Assert.Equal(created.CreatedAtUtc.Ticks, listed.CreatedAtUtc.Ticks);
|
|
Assert.Equal(created.UpdatedAtUtc.Ticks, listed.UpdatedAtUtc.Ticks);
|
|
AssertUtcTimestamps(retrieved);
|
|
AssertUtcTimestamps(listed);
|
|
|
|
var updated = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<OkObjectResult>((await reader.Update(created.Id,
|
|
new EmailDraftsController.UpdateDraftRequest(created.Revision, "one@example.test", "Updated UTC subject", "Updated UTC body"), default)).Result).Value);
|
|
AssertUtcTimestamps(updated);
|
|
Assert.Equal(created.CreatedAtUtc.Ticks, updated.CreatedAtUtc.Ticks);
|
|
Assert.Equal(created.ClientRequestId, updated.ClientRequestId);
|
|
Assert.Equal(2, updated.Revision);
|
|
|
|
var failedAttempt = new EmailSendAttempt
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = "user-1",
|
|
JobApplicationId = updated.JobApplicationId,
|
|
Provider = updated.Provider,
|
|
ClientRequestId = updated.ClientRequestId,
|
|
PayloadHash = new string('c', 64),
|
|
Status = EmailSendStatuses.Failed,
|
|
FailureCategory = "provider_rejected",
|
|
CreatedAtUtc = updated.CreatedAtUtc,
|
|
CompletedAtUtc = updated.UpdatedAtUtc,
|
|
};
|
|
readDb.EmailSendAttempts.Add(failedAttempt);
|
|
await readDb.SaveChangesAsync();
|
|
var rotated = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<OkObjectResult>((await reader.NewAttempt(updated.Id,
|
|
new EmailDraftsController.NewAttemptRequest(updated.Revision), default)).Result).Value);
|
|
AssertUtcTimestamps(rotated);
|
|
Assert.Equal(updated.CreatedAtUtc.Ticks, rotated.CreatedAtUtc.Ticks);
|
|
Assert.NotEqual(updated.ClientRequestId, rotated.ClientRequestId);
|
|
Assert.Equal(3, rotated.Revision);
|
|
|
|
await using var verifyRotation = fixture.Context("user-1");
|
|
var finalReader = Controller(verifyRotation, "user-1");
|
|
var finalRetrieved = Assert.IsType<EmailDraftsController.DraftDto>(
|
|
Assert.IsType<OkObjectResult>((await finalReader.Get(rotated.Id, default)).Result).Value);
|
|
var finalListed = Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
|
|
Assert.IsType<OkObjectResult>((await finalReader.List(null, default)).Result).Value));
|
|
Assert.Equal(rotated, finalRetrieved);
|
|
Assert.Equal(rotated, finalListed);
|
|
AssertUtcTimestamps(finalRetrieved);
|
|
AssertUtcTimestamps(finalListed);
|
|
AssertAttemptUnchanged(failedAttempt, Assert.Single(await verifyRotation.EmailSendAttempts.AsNoTracking().ToListAsync()));
|
|
}
|
|
|
|
private static void AssertUtcTimestamps(EmailDraftsController.DraftDto draft)
|
|
{
|
|
Assert.Equal(DateTimeKind.Utc, draft.CreatedAtUtc.Kind);
|
|
Assert.Equal(DateTimeKind.Utc, draft.UpdatedAtUtc.Kind);
|
|
}
|
|
|
|
private static void AssertDraftContentUnchanged(EmailDraft expected, EmailDraft actual)
|
|
{
|
|
Assert.Equal(expected.Id, actual.Id);
|
|
Assert.Equal(expected.OwnerUserId, actual.OwnerUserId);
|
|
Assert.Equal(expected.JobApplicationId, actual.JobApplicationId);
|
|
Assert.Equal(expected.Provider, actual.Provider);
|
|
Assert.Equal(expected.To, actual.To);
|
|
Assert.Equal(expected.Subject, actual.Subject);
|
|
Assert.Equal(expected.BodyText, actual.BodyText);
|
|
Assert.Equal(expected.ThreadId, actual.ThreadId);
|
|
Assert.Equal(expected.CreatedAtUtc, actual.CreatedAtUtc);
|
|
}
|
|
|
|
private static void AssertAttemptUnchanged(EmailSendAttempt expected, EmailSendAttempt actual)
|
|
{
|
|
Assert.Equal(expected.Id, actual.Id);
|
|
Assert.Equal(expected.OwnerUserId, actual.OwnerUserId);
|
|
Assert.Equal(expected.JobApplicationId, actual.JobApplicationId);
|
|
Assert.Equal(expected.Provider, actual.Provider);
|
|
Assert.Equal(expected.ClientRequestId, actual.ClientRequestId);
|
|
Assert.Equal(expected.PayloadHash, actual.PayloadHash);
|
|
Assert.Equal(expected.Status, actual.Status);
|
|
Assert.Equal(expected.ProviderMessageId, actual.ProviderMessageId);
|
|
Assert.Equal(expected.FailureCategory, actual.FailureCategory);
|
|
Assert.Equal(expected.CreatedAtUtc, actual.CreatedAtUtc);
|
|
Assert.Equal(expected.StartedAtUtc, actual.StartedAtUtc);
|
|
Assert.Equal(expected.CompletedAtUtc, actual.CompletedAtUtc);
|
|
}
|
|
|
|
private static EmailDraftsController Controller(JobTrackerContext db, string userId)
|
|
{
|
|
var controller = new EmailDraftsController(
|
|
db,
|
|
new EmailProviderRegistry(new[] { new FakeProvider() }),
|
|
TimeProvider.System);
|
|
controller.ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")),
|
|
},
|
|
};
|
|
return controller;
|
|
}
|
|
|
|
private sealed class FakeProvider : IEmailProvider
|
|
{
|
|
public string ProviderKey => "gmail";
|
|
public Task<EmailConnectionInfo?> GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken) => Task.FromResult<EmailConnectionInfo?>(null);
|
|
public Task<IReadOnlyList<EmailMessageSummary>> SearchAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
public Task<IReadOnlyList<EmailMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string threadId, CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
public Task<EmailMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
public Task<EmailDeliveryResult> SendAsync(string ownerUserId, EmailDeliveryRequest request, CancellationToken cancellationToken) => throw new NotSupportedException();
|
|
}
|
|
|
|
private sealed class Fixture : IAsyncDisposable
|
|
{
|
|
private readonly SqliteConnection _connection;
|
|
private readonly DbContextOptions<JobTrackerContext> _options;
|
|
|
|
private Fixture(SqliteConnection connection, DbContextOptions<JobTrackerContext> options)
|
|
{
|
|
_connection = connection;
|
|
_options = options;
|
|
}
|
|
|
|
public int UserOneJobId { get; private set; }
|
|
public int UserTwoJobId { get; private set; }
|
|
public Guid UserOneDraftId { get; private set; }
|
|
public Guid UserTwoDraftId { get; private set; }
|
|
|
|
public static async Task<Fixture> CreateAsync(bool seedDrafts = false)
|
|
{
|
|
var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options;
|
|
var fixture = new Fixture(connection, options);
|
|
await using var db = fixture.Context(null);
|
|
await db.Database.EnsureCreatedAsync();
|
|
var companyOne = new Company { Name = "One", OwnerUserId = "user-1" };
|
|
var companyTwo = new Company { Name = "Two", OwnerUserId = "user-2" };
|
|
db.Companies.AddRange(companyOne, companyTwo);
|
|
await db.SaveChangesAsync();
|
|
var jobOne = new JobApplication { JobTitle = "One", CompanyId = companyOne.Id, OwnerUserId = "user-1" };
|
|
var jobTwo = new JobApplication { JobTitle = "Two", CompanyId = companyTwo.Id, OwnerUserId = "user-2" };
|
|
db.JobApplications.AddRange(jobOne, jobTwo);
|
|
await db.SaveChangesAsync();
|
|
fixture.UserOneJobId = jobOne.Id;
|
|
fixture.UserTwoJobId = jobTwo.Id;
|
|
if (seedDrafts)
|
|
{
|
|
var one = Draft("user-1", jobOne.Id, "one@example.test");
|
|
var two = Draft("user-2", jobTwo.Id, "two@example.test");
|
|
db.EmailDrafts.AddRange(one, two);
|
|
await db.SaveChangesAsync();
|
|
fixture.UserOneDraftId = one.Id;
|
|
fixture.UserTwoDraftId = two.Id;
|
|
}
|
|
return fixture;
|
|
}
|
|
|
|
public JobTrackerContext Context(string? userId)
|
|
{
|
|
var currentUser = new Mock<ICurrentUserService>();
|
|
currentUser.SetupGet(service => service.UserId).Returns(userId);
|
|
return new JobTrackerContext(_options, currentUser.Object);
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => _connection.DisposeAsync();
|
|
|
|
private static EmailDraft Draft(string ownerUserId, int jobApplicationId, string recipient) => new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OwnerUserId = ownerUserId,
|
|
JobApplicationId = jobApplicationId,
|
|
Provider = "gmail",
|
|
To = recipient,
|
|
Subject = "Synthetic",
|
|
BodyText = "Synthetic private draft.",
|
|
ThreadId = "thread-1",
|
|
ClientRequestId = Guid.NewGuid().ToString("D"),
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
UpdatedAtUtc = DateTime.UtcNow,
|
|
};
|
|
}
|
|
}
|