fix(email): preserve draft send identity
Persist and export each draft's idempotency UUID so refresh and edits cannot create a fresh delivery identity. Add reversible provider-specific migration SQL.
This commit is contained in:
@@ -154,6 +154,7 @@ public sealed class BackgroundWorkerTenantTests
|
||||
var draft = Assert.Single(rootElement.GetProperty("EmailDrafts").EnumerateArray());
|
||||
Assert.Equal(owner == "user-1" ? "one@example.test" : "two@example.test", draft.GetProperty("To").GetString());
|
||||
Assert.Equal(owner == "user-1" ? "Private draft one" : "Private draft two", draft.GetProperty("BodyText").GetString());
|
||||
Assert.True(Guid.TryParse(draft.GetProperty("ClientRequestId").GetString(), out _));
|
||||
Assert.Equal(1, draft.GetProperty("Revision").GetInt64());
|
||||
}
|
||||
owners.Sort(StringComparer.Ordinal);
|
||||
@@ -176,6 +177,7 @@ public sealed class BackgroundWorkerTenantTests
|
||||
Subject = "Synthetic export subject",
|
||||
BodyText = body,
|
||||
ThreadId = "synthetic-thread",
|
||||
ClientRequestId = Guid.NewGuid().ToString("D"),
|
||||
CreatedAtUtc = DateTime.UtcNow.AddMinutes(-5),
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
@@ -62,6 +62,7 @@ public sealed class BackupControllerTests
|
||||
Subject = "Synthetic export subject",
|
||||
BodyText = "Synthetic readable draft body.",
|
||||
ThreadId = "synthetic-thread",
|
||||
ClientRequestId = "00000000-0000-4000-8000-000000000123",
|
||||
Revision = 3,
|
||||
CreatedAtUtc = DateTime.UtcNow.AddMinutes(-5),
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
@@ -75,6 +76,7 @@ public sealed class BackupControllerTests
|
||||
To = "other@example.test",
|
||||
Subject = "Other tenant subject",
|
||||
BodyText = "Other tenant private body.",
|
||||
ClientRequestId = "00000000-0000-4000-8000-000000000456",
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
});
|
||||
@@ -102,6 +104,7 @@ public sealed class BackupControllerTests
|
||||
Assert.Equal("Synthetic export subject", draft.GetProperty("Subject").GetString());
|
||||
Assert.Equal("Synthetic readable draft body.", draft.GetProperty("BodyText").GetString());
|
||||
Assert.Equal("synthetic-thread", draft.GetProperty("ThreadId").GetString());
|
||||
Assert.Equal("00000000-0000-4000-8000-000000000123", draft.GetProperty("ClientRequestId").GetString());
|
||||
Assert.Equal(3, draft.GetProperty("Revision").GetInt64());
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ public sealed class EmailDraftPersistenceTests
|
||||
Subject = "Synthetic draft",
|
||||
BodyText = "Synthetic private draft body.",
|
||||
ThreadId = "thread-1",
|
||||
ClientRequestId = Guid.NewGuid().ToString("D"),
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ public sealed class EmailDraftsControllerTests
|
||||
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);
|
||||
@@ -63,6 +64,9 @@ public sealed class EmailDraftsControllerTests
|
||||
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]
|
||||
@@ -72,11 +76,13 @@ public sealed class EmailDraftsControllerTests
|
||||
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);
|
||||
@@ -190,6 +196,7 @@ public sealed class EmailDraftsControllerTests
|
||||
To = recipient,
|
||||
Subject = "Synthetic",
|
||||
BodyText = "Synthetic private draft.",
|
||||
ClientRequestId = Guid.NewGuid().ToString("D"),
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user