fix(email): gate draft attempt rotation
CI and Deploy / test (pull_request) Successful in 4m23s
CI and Deploy / deploy (pull_request) Has been skipped

Rotate persisted delivery identity only after the matching owner attempt is definitively failed. Keep stale, foreign, pending, sent, and uncertain drafts non-retryable.
This commit is contained in:
cesnimda
2026-08-10 10:21:37 +02:00
parent 114e3b66ba
commit 29de2632f6
4 changed files with 123 additions and 3 deletions
@@ -105,6 +105,42 @@ public sealed class EmailDraftsControllerTests
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);
db.EmailSendAttempts.Add(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,
});
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);
}
private static EmailDraftsController Controller(JobTrackerContext db, string userId)
{
var controller = new EmailDraftsController(