587 lines
19 KiB
Go
587 lines
19 KiB
Go
package mailstore
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/mail/internal/domain/attempt"
|
|
"galaxy/mail/internal/domain/common"
|
|
deliverydomain "galaxy/mail/internal/domain/delivery"
|
|
"galaxy/mail/internal/domain/idempotency"
|
|
"galaxy/mail/internal/domain/malformedcommand"
|
|
"galaxy/mail/internal/service/acceptauthdelivery"
|
|
"galaxy/mail/internal/service/acceptgenericdelivery"
|
|
"galaxy/mail/internal/service/executeattempt"
|
|
"galaxy/mail/internal/service/listdeliveries"
|
|
"galaxy/mail/internal/service/renderdelivery"
|
|
"galaxy/mail/internal/service/resenddelivery"
|
|
)
|
|
|
|
const (
|
|
fixtureDeliveryID common.DeliveryID = "delivery-001"
|
|
fixtureKey common.IdempotencyKey = "key-001"
|
|
fixtureFingerprint = "sha256:abcdef"
|
|
fixtureRecipient common.Email = "user@example.com"
|
|
)
|
|
|
|
func fixtureNow() time.Time {
|
|
return time.Date(2026, time.April, 26, 12, 0, 0, 0, time.UTC)
|
|
}
|
|
|
|
func fixtureAuthDelivery(id common.DeliveryID, key common.IdempotencyKey, status deliverydomain.Status) deliverydomain.Delivery {
|
|
now := fixtureNow()
|
|
record := deliverydomain.Delivery{
|
|
DeliveryID: id,
|
|
Source: deliverydomain.SourceAuthSession,
|
|
PayloadMode: deliverydomain.PayloadModeRendered,
|
|
Envelope: deliverydomain.Envelope{To: []common.Email{fixtureRecipient}},
|
|
Content: deliverydomain.Content{Subject: "Login code", TextBody: "Your code is 123456"},
|
|
IdempotencyKey: key,
|
|
Status: status,
|
|
AttemptCount: 1,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if status == deliverydomain.StatusSuppressed {
|
|
record.AttemptCount = 0
|
|
record.SuppressedAt = &now
|
|
}
|
|
return record
|
|
}
|
|
|
|
func fixtureGenericDelivery(id common.DeliveryID, key common.IdempotencyKey) deliverydomain.Delivery {
|
|
now := fixtureNow()
|
|
return deliverydomain.Delivery{
|
|
DeliveryID: id,
|
|
Source: deliverydomain.SourceNotification,
|
|
PayloadMode: deliverydomain.PayloadModeTemplate,
|
|
TemplateID: common.TemplateID("generic-news"),
|
|
Locale: common.Locale("en"),
|
|
TemplateVariables: map[string]any{"name": "Alice"},
|
|
Envelope: deliverydomain.Envelope{To: []common.Email{fixtureRecipient}, ReplyTo: []common.Email{"reply@example.com"}},
|
|
Attachments: []common.AttachmentMetadata{{Filename: "f.txt", ContentType: "text/plain", SizeBytes: 5}},
|
|
IdempotencyKey: key,
|
|
Status: deliverydomain.StatusQueued,
|
|
AttemptCount: 1,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
}
|
|
|
|
func fixtureFirstAttempt(id common.DeliveryID, attemptNo int) attempt.Attempt {
|
|
now := fixtureNow().Add(time.Minute)
|
|
return attempt.Attempt{
|
|
DeliveryID: id,
|
|
AttemptNo: attemptNo,
|
|
Status: attempt.StatusScheduled,
|
|
ScheduledFor: now,
|
|
}
|
|
}
|
|
|
|
func fixtureIdempotency(source deliverydomain.Source, id common.DeliveryID, key common.IdempotencyKey) idempotency.Record {
|
|
now := fixtureNow()
|
|
return idempotency.Record{
|
|
Source: source,
|
|
IdempotencyKey: key,
|
|
DeliveryID: id,
|
|
RequestFingerprint: fixtureFingerprint,
|
|
CreatedAt: now,
|
|
ExpiresAt: now.Add(7 * 24 * time.Hour),
|
|
}
|
|
}
|
|
|
|
func TestPing(t *testing.T) {
|
|
store := newTestStore(t)
|
|
if err := store.Ping(context.Background()); err != nil {
|
|
t.Fatalf("ping: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthAcceptanceCreate_GetIdempotency_GetDelivery(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
delivery := fixtureAuthDelivery(fixtureDeliveryID, fixtureKey, deliverydomain.StatusQueued)
|
|
first := fixtureFirstAttempt(delivery.DeliveryID, 1)
|
|
idem := fixtureIdempotency(delivery.Source, delivery.DeliveryID, delivery.IdempotencyKey)
|
|
|
|
if err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: &first,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("create acceptance: %v", err)
|
|
}
|
|
|
|
got, ok, err := store.GetIdempotency(ctx, delivery.Source, delivery.IdempotencyKey)
|
|
if err != nil {
|
|
t.Fatalf("get idempotency: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("idempotency not found")
|
|
}
|
|
if got.DeliveryID != delivery.DeliveryID || got.RequestFingerprint != fixtureFingerprint {
|
|
t.Fatalf("idempotency mismatch: %+v", got)
|
|
}
|
|
|
|
loaded, ok, err := store.GetDelivery(ctx, delivery.DeliveryID)
|
|
if err != nil {
|
|
t.Fatalf("get delivery: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("delivery not found")
|
|
}
|
|
if loaded.DeliveryID != delivery.DeliveryID || loaded.Status != deliverydomain.StatusQueued {
|
|
t.Fatalf("delivery mismatch: %+v", loaded)
|
|
}
|
|
if !reflect.DeepEqual(loaded.Envelope.To, []common.Email{fixtureRecipient}) {
|
|
t.Fatalf("envelope.to mismatch: %+v", loaded.Envelope)
|
|
}
|
|
}
|
|
|
|
func TestAuthAcceptanceConflict(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
delivery := fixtureAuthDelivery(fixtureDeliveryID, fixtureKey, deliverydomain.StatusQueued)
|
|
first := fixtureFirstAttempt(delivery.DeliveryID, 1)
|
|
idem := fixtureIdempotency(delivery.Source, delivery.DeliveryID, delivery.IdempotencyKey)
|
|
|
|
if err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: &first,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("first create: %v", err)
|
|
}
|
|
|
|
dup := delivery
|
|
dup.DeliveryID = "delivery-002"
|
|
dupAttempt := fixtureFirstAttempt(dup.DeliveryID, 1)
|
|
dupIdem := idem
|
|
dupIdem.DeliveryID = dup.DeliveryID
|
|
|
|
err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: dup,
|
|
FirstAttempt: &dupAttempt,
|
|
Idempotency: dupIdem,
|
|
})
|
|
if !errors.Is(err, acceptauthdelivery.ErrConflict) {
|
|
t.Fatalf("expected acceptauthdelivery.ErrConflict, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGenericAcceptanceCreate_GetDeliveryPayload(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
delivery := fixtureGenericDelivery(fixtureDeliveryID, fixtureKey)
|
|
first := fixtureFirstAttempt(delivery.DeliveryID, 1)
|
|
idem := fixtureIdempotency(delivery.Source, delivery.DeliveryID, delivery.IdempotencyKey)
|
|
payload := &acceptgenericdelivery.DeliveryPayload{
|
|
DeliveryID: delivery.DeliveryID,
|
|
Attachments: []acceptgenericdelivery.AttachmentPayload{{
|
|
Filename: "f.txt",
|
|
ContentType: "text/plain",
|
|
ContentBase64: "aGVsbG8=", // "hello"
|
|
SizeBytes: 5,
|
|
}},
|
|
}
|
|
|
|
handle := store.GenericAcceptance()
|
|
if err := handle.CreateAcceptance(ctx, acceptgenericdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: first,
|
|
DeliveryPayload: payload,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("create generic acceptance: %v", err)
|
|
}
|
|
|
|
got, ok, err := store.GetDeliveryPayload(ctx, delivery.DeliveryID)
|
|
if err != nil {
|
|
t.Fatalf("get delivery payload: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("payload not found")
|
|
}
|
|
if got.DeliveryID != delivery.DeliveryID || len(got.Attachments) != 1 {
|
|
t.Fatalf("payload mismatch: %+v", got)
|
|
}
|
|
if got.Attachments[0].ContentBase64 != "aGVsbG8=" {
|
|
t.Fatalf("payload base64 mismatch: %+v", got.Attachments[0])
|
|
}
|
|
}
|
|
|
|
func TestSchedulerClaimAndCommit(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
delivery := fixtureAuthDelivery(fixtureDeliveryID, fixtureKey, deliverydomain.StatusQueued)
|
|
first := fixtureFirstAttempt(delivery.DeliveryID, 1)
|
|
idem := fixtureIdempotency(delivery.Source, delivery.DeliveryID, delivery.IdempotencyKey)
|
|
if err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: &first,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("create acceptance: %v", err)
|
|
}
|
|
|
|
scheduler := store.AttemptExecution()
|
|
now := first.ScheduledFor.Add(time.Second)
|
|
ids, err := scheduler.NextDueDeliveryIDs(ctx, now, 10)
|
|
if err != nil {
|
|
t.Fatalf("next due: %v", err)
|
|
}
|
|
if len(ids) != 1 || ids[0] != delivery.DeliveryID {
|
|
t.Fatalf("next due ids: %+v", ids)
|
|
}
|
|
|
|
claimed, ok, err := scheduler.ClaimDueAttempt(ctx, delivery.DeliveryID, now)
|
|
if err != nil {
|
|
t.Fatalf("claim due: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("claim due: not found")
|
|
}
|
|
if claimed.Delivery.Status != deliverydomain.StatusSending {
|
|
t.Fatalf("expected sending, got %q", claimed.Delivery.Status)
|
|
}
|
|
if claimed.Attempt.Status != attempt.StatusInProgress {
|
|
t.Fatalf("expected in_progress, got %q", claimed.Attempt.Status)
|
|
}
|
|
|
|
// After claim, the row should not be picked up again.
|
|
again, err := scheduler.NextDueDeliveryIDs(ctx, now.Add(time.Second), 10)
|
|
if err != nil {
|
|
t.Fatalf("next due (after claim): %v", err)
|
|
}
|
|
if len(again) != 0 {
|
|
t.Fatalf("expected zero due deliveries after claim, got %+v", again)
|
|
}
|
|
|
|
completed := claimed.Attempt
|
|
finishedAt := now.Add(time.Second)
|
|
completed.Status = attempt.StatusProviderAccepted
|
|
completed.FinishedAt = &finishedAt
|
|
completed.ProviderClassification = "accepted"
|
|
completed.ProviderSummary = "ok"
|
|
|
|
finalDelivery := claimed.Delivery
|
|
finalDelivery.Status = deliverydomain.StatusSent
|
|
finalDelivery.LastAttemptStatus = attempt.StatusProviderAccepted
|
|
finalDelivery.SentAt = &finishedAt
|
|
finalDelivery.UpdatedAt = finishedAt
|
|
finalDelivery.ProviderSummary = "ok"
|
|
|
|
if err := scheduler.Commit(ctx, executeattempt.CommitStateInput{
|
|
Delivery: finalDelivery,
|
|
Attempt: completed,
|
|
}); err != nil {
|
|
t.Fatalf("commit attempt: %v", err)
|
|
}
|
|
|
|
loaded, ok, err := store.GetDelivery(ctx, delivery.DeliveryID)
|
|
if err != nil || !ok {
|
|
t.Fatalf("get delivery after commit: ok=%v err=%v", ok, err)
|
|
}
|
|
if loaded.Status != deliverydomain.StatusSent {
|
|
t.Fatalf("expected sent, got %q", loaded.Status)
|
|
}
|
|
}
|
|
|
|
func TestRenderMarkRendered(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
delivery := fixtureGenericDelivery(fixtureDeliveryID, fixtureKey)
|
|
first := fixtureFirstAttempt(delivery.DeliveryID, 1)
|
|
idem := fixtureIdempotency(delivery.Source, delivery.DeliveryID, delivery.IdempotencyKey)
|
|
if err := store.GenericAcceptance().CreateAcceptance(ctx, acceptgenericdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: first,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("create acceptance: %v", err)
|
|
}
|
|
|
|
rendered := delivery
|
|
rendered.Status = deliverydomain.StatusRendered
|
|
rendered.Content = deliverydomain.Content{Subject: "Hello Alice", TextBody: "Hi"}
|
|
rendered.UpdatedAt = fixtureNow().Add(time.Second)
|
|
|
|
if err := store.RenderDelivery().MarkRendered(ctx, renderdelivery.MarkRenderedInput{Delivery: rendered}); err != nil {
|
|
t.Fatalf("mark rendered: %v", err)
|
|
}
|
|
|
|
loaded, ok, err := store.GetDelivery(ctx, delivery.DeliveryID)
|
|
if err != nil || !ok {
|
|
t.Fatalf("get delivery: ok=%v err=%v", ok, err)
|
|
}
|
|
if loaded.Status != deliverydomain.StatusRendered {
|
|
t.Fatalf("expected rendered, got %q", loaded.Status)
|
|
}
|
|
if loaded.Content.Subject != "Hello Alice" {
|
|
t.Fatalf("subject mismatch: %q", loaded.Content.Subject)
|
|
}
|
|
}
|
|
|
|
func TestListDeliveriesPaging(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
for i := range 3 {
|
|
key := common.IdempotencyKey([]byte{'k', '0' + byte(i)})
|
|
id := common.DeliveryID([]byte{'d', '0' + byte(i)})
|
|
delivery := fixtureAuthDelivery(id, key, deliverydomain.StatusQueued)
|
|
// Stagger created_at so listing order is deterministic.
|
|
delivery.CreatedAt = fixtureNow().Add(time.Duration(i) * time.Second)
|
|
delivery.UpdatedAt = delivery.CreatedAt
|
|
first := fixtureFirstAttempt(id, 1)
|
|
first.ScheduledFor = delivery.CreatedAt.Add(time.Minute)
|
|
idem := fixtureIdempotency(delivery.Source, id, key)
|
|
idem.CreatedAt = delivery.CreatedAt
|
|
idem.ExpiresAt = delivery.CreatedAt.Add(7 * 24 * time.Hour)
|
|
if err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: &first,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("create %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
page1, err := store.List(ctx, listdeliveries.Input{Limit: 2})
|
|
if err != nil {
|
|
t.Fatalf("list page 1: %v", err)
|
|
}
|
|
if len(page1.Items) != 2 || page1.NextCursor == nil {
|
|
t.Fatalf("page 1 unexpected: items=%d cursor=%v", len(page1.Items), page1.NextCursor)
|
|
}
|
|
if page1.Items[0].DeliveryID != "d2" || page1.Items[1].DeliveryID != "d1" {
|
|
t.Fatalf("page 1 ordering: %+v", []common.DeliveryID{page1.Items[0].DeliveryID, page1.Items[1].DeliveryID})
|
|
}
|
|
|
|
page2, err := store.List(ctx, listdeliveries.Input{Limit: 2, Cursor: page1.NextCursor})
|
|
if err != nil {
|
|
t.Fatalf("list page 2: %v", err)
|
|
}
|
|
if len(page2.Items) != 1 || page2.NextCursor != nil {
|
|
t.Fatalf("page 2 unexpected: items=%d cursor=%v", len(page2.Items), page2.NextCursor)
|
|
}
|
|
if page2.Items[0].DeliveryID != "d0" {
|
|
t.Fatalf("page 2 expected d0, got %s", page2.Items[0].DeliveryID)
|
|
}
|
|
}
|
|
|
|
func TestListAttemptsAndDeadLetter(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
delivery := fixtureAuthDelivery(fixtureDeliveryID, fixtureKey, deliverydomain.StatusQueued)
|
|
first := fixtureFirstAttempt(delivery.DeliveryID, 1)
|
|
idem := fixtureIdempotency(delivery.Source, delivery.DeliveryID, delivery.IdempotencyKey)
|
|
if err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: delivery,
|
|
FirstAttempt: &first,
|
|
Idempotency: idem,
|
|
}); err != nil {
|
|
t.Fatalf("create acceptance: %v", err)
|
|
}
|
|
|
|
// Claim and commit a transport_failed → next attempt scheduled (delivery
|
|
// stays queued); then claim attempt 2 and commit dead-letter.
|
|
scheduler := store.AttemptExecution()
|
|
now := first.ScheduledFor.Add(time.Second)
|
|
claimed1, ok, err := scheduler.ClaimDueAttempt(ctx, delivery.DeliveryID, now)
|
|
if err != nil || !ok {
|
|
t.Fatalf("claim attempt 1: ok=%v err=%v", ok, err)
|
|
}
|
|
|
|
finishedAt1 := now.Add(time.Second)
|
|
terminal1 := claimed1.Attempt
|
|
terminal1.Status = attempt.StatusTransportFailed
|
|
terminal1.FinishedAt = &finishedAt1
|
|
terminal1.ProviderClassification = "transport_failed"
|
|
|
|
nextAttempt := attempt.Attempt{
|
|
DeliveryID: delivery.DeliveryID,
|
|
AttemptNo: 2,
|
|
Status: attempt.StatusScheduled,
|
|
ScheduledFor: finishedAt1.Add(5 * time.Minute),
|
|
}
|
|
|
|
delivery2 := claimed1.Delivery
|
|
delivery2.Status = deliverydomain.StatusQueued
|
|
delivery2.LastAttemptStatus = attempt.StatusTransportFailed
|
|
delivery2.AttemptCount = 2
|
|
delivery2.UpdatedAt = finishedAt1
|
|
|
|
if err := scheduler.Commit(ctx, executeattempt.CommitStateInput{
|
|
Delivery: delivery2,
|
|
Attempt: terminal1,
|
|
NextAttempt: &nextAttempt,
|
|
}); err != nil {
|
|
t.Fatalf("commit attempt 1: %v", err)
|
|
}
|
|
|
|
// Claim attempt 2.
|
|
now2 := nextAttempt.ScheduledFor.Add(time.Second)
|
|
claimed2, ok, err := scheduler.ClaimDueAttempt(ctx, delivery.DeliveryID, now2)
|
|
if err != nil || !ok {
|
|
t.Fatalf("claim attempt 2: ok=%v err=%v", ok, err)
|
|
}
|
|
|
|
finishedAt2 := now2.Add(time.Second)
|
|
terminal2 := claimed2.Attempt
|
|
terminal2.Status = attempt.StatusTransportFailed
|
|
terminal2.FinishedAt = &finishedAt2
|
|
terminal2.ProviderClassification = "retry_exhausted"
|
|
|
|
dlEntry := &deliverydomain.DeadLetterEntry{
|
|
DeliveryID: delivery.DeliveryID,
|
|
FinalAttemptNo: 2,
|
|
FailureClassification: "retry_exhausted",
|
|
CreatedAt: finishedAt2,
|
|
}
|
|
|
|
delivery3 := claimed2.Delivery
|
|
delivery3.Status = deliverydomain.StatusDeadLetter
|
|
delivery3.LastAttemptStatus = attempt.StatusTransportFailed
|
|
delivery3.DeadLetteredAt = &finishedAt2
|
|
delivery3.UpdatedAt = finishedAt2
|
|
|
|
if err := scheduler.Commit(ctx, executeattempt.CommitStateInput{
|
|
Delivery: delivery3,
|
|
Attempt: terminal2,
|
|
DeadLetter: dlEntry,
|
|
}); err != nil {
|
|
t.Fatalf("commit attempt 2: %v", err)
|
|
}
|
|
|
|
loaded, ok, err := store.GetDelivery(ctx, delivery.DeliveryID)
|
|
if err != nil || !ok {
|
|
t.Fatalf("get delivery: ok=%v err=%v", ok, err)
|
|
}
|
|
if loaded.Status != deliverydomain.StatusDeadLetter {
|
|
t.Fatalf("expected dead_letter, got %q", loaded.Status)
|
|
}
|
|
|
|
dl, ok, err := store.GetDeadLetter(ctx, delivery.DeliveryID)
|
|
if err != nil || !ok {
|
|
t.Fatalf("get dead-letter: ok=%v err=%v", ok, err)
|
|
}
|
|
if dl.FailureClassification != "retry_exhausted" {
|
|
t.Fatalf("dead-letter mismatch: %+v", dl)
|
|
}
|
|
|
|
attempts, err := store.ListAttempts(ctx, delivery.DeliveryID, loaded.AttemptCount)
|
|
if err != nil {
|
|
t.Fatalf("list attempts: %v", err)
|
|
}
|
|
if len(attempts) != 2 {
|
|
t.Fatalf("expected 2 attempts, got %d", len(attempts))
|
|
}
|
|
if attempts[0].AttemptNo != 1 || attempts[1].AttemptNo != 2 {
|
|
t.Fatalf("attempt sequence: %+v", attempts)
|
|
}
|
|
}
|
|
|
|
func TestMalformedCommandRecord(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
entry := malformedcommand.Entry{
|
|
StreamEntryID: "1234-0",
|
|
DeliveryID: "delivery-x",
|
|
Source: "notification",
|
|
IdempotencyKey: "k",
|
|
FailureCode: malformedcommand.FailureCodeInvalidPayload,
|
|
FailureMessage: "missing required field",
|
|
RawFields: map[string]any{"raw": "value"},
|
|
RecordedAt: fixtureNow(),
|
|
}
|
|
if err := store.Record(ctx, entry); err != nil {
|
|
t.Fatalf("record malformed: %v", err)
|
|
}
|
|
// Idempotent re-record: same entry should not error.
|
|
if err := store.Record(ctx, entry); err != nil {
|
|
t.Fatalf("re-record malformed: %v", err)
|
|
}
|
|
|
|
got, ok, err := store.GetMalformedCommand(ctx, entry.StreamEntryID)
|
|
if err != nil || !ok {
|
|
t.Fatalf("get malformed: ok=%v err=%v", ok, err)
|
|
}
|
|
if got.FailureCode != malformedcommand.FailureCodeInvalidPayload {
|
|
t.Fatalf("failure code mismatch: %q", got.FailureCode)
|
|
}
|
|
}
|
|
|
|
func TestResendCreate(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
|
|
parent := fixtureAuthDelivery(fixtureDeliveryID, fixtureKey, deliverydomain.StatusQueued)
|
|
parentAttempt := fixtureFirstAttempt(parent.DeliveryID, 1)
|
|
parentIdem := fixtureIdempotency(parent.Source, parent.DeliveryID, parent.IdempotencyKey)
|
|
if err := store.CreateAcceptance(ctx, acceptauthdelivery.CreateAcceptanceInput{
|
|
Delivery: parent,
|
|
FirstAttempt: &parentAttempt,
|
|
Idempotency: parentIdem,
|
|
}); err != nil {
|
|
t.Fatalf("create parent: %v", err)
|
|
}
|
|
|
|
cloneID := common.DeliveryID("clone-001")
|
|
cloneIdempKey := common.IdempotencyKey("resend-clone-001")
|
|
now := fixtureNow().Add(time.Hour)
|
|
clone := deliverydomain.Delivery{
|
|
DeliveryID: cloneID,
|
|
ResendParentDeliveryID: parent.DeliveryID,
|
|
Source: deliverydomain.SourceOperatorResend,
|
|
PayloadMode: deliverydomain.PayloadModeRendered,
|
|
Envelope: parent.Envelope,
|
|
Content: parent.Content,
|
|
IdempotencyKey: cloneIdempKey,
|
|
Status: deliverydomain.StatusQueued,
|
|
AttemptCount: 1,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
cloneAttempt := attempt.Attempt{
|
|
DeliveryID: cloneID,
|
|
AttemptNo: 1,
|
|
Status: attempt.StatusScheduled,
|
|
ScheduledFor: now.Add(time.Minute),
|
|
}
|
|
|
|
if err := store.CreateResend(ctx, resenddelivery.CreateResendInput{
|
|
Delivery: clone,
|
|
FirstAttempt: cloneAttempt,
|
|
}); err != nil {
|
|
t.Fatalf("create resend: %v", err)
|
|
}
|
|
|
|
loaded, ok, err := store.GetDelivery(ctx, cloneID)
|
|
if err != nil || !ok {
|
|
t.Fatalf("get clone: ok=%v err=%v", ok, err)
|
|
}
|
|
if loaded.ResendParentDeliveryID != parent.DeliveryID {
|
|
t.Fatalf("expected resend parent %q, got %q", parent.DeliveryID, loaded.ResendParentDeliveryID)
|
|
}
|
|
|
|
// Resend deliveries do not surface as idempotency hits.
|
|
_, ok, err = store.GetIdempotency(ctx, deliverydomain.SourceOperatorResend, cloneIdempKey)
|
|
if err != nil {
|
|
t.Fatalf("get idempotency for resend: %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatal("resend delivery should not surface as idempotency hit")
|
|
}
|
|
}
|