package resenddelivery import ( "bytes" "context" "log/slog" "testing" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/service/acceptgenericdelivery" "github.com/stretchr/testify/require" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func TestServiceExecuteRejectsNonTerminalStatus(t *testing.T) { t.Parallel() tests := []deliverydomain.Status{ deliverydomain.StatusAccepted, deliverydomain.StatusQueued, deliverydomain.StatusRendered, deliverydomain.StatusSending, } for _, status := range tests { status := status t.Run(string(status), func(t *testing.T) { t.Parallel() record := validOriginalDelivery() record.Status = status record.SentAt = nil record.FailedAt = nil record.DeadLetteredAt = nil record.SuppressedAt = nil require.NoError(t, record.Validate()) store := &stubStore{delivery: &record} service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: &stubIDGenerator{ids: []common.DeliveryID{"clone-1"}}, Clock: stubClock{now: fixedNow()}, }) _, err := service.Execute(context.Background(), Input{DeliveryID: record.DeliveryID}) require.ErrorIs(t, err, ErrNotAllowed) }) } } func TestServiceExecuteCreatesLinkedClone(t *testing.T) { t.Parallel() original := validOriginalDelivery() originalCopy := original payload := validPayload(original.DeliveryID) store := &stubStore{ delivery: &original, payload: &payload, } service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: &stubIDGenerator{ids: []common.DeliveryID{"clone-123"}}, Clock: stubClock{now: fixedNow()}, }) result, err := service.Execute(context.Background(), Input{DeliveryID: original.DeliveryID}) require.NoError(t, err) require.Equal(t, Result{DeliveryID: common.DeliveryID("clone-123")}, result) require.Len(t, store.createInputs, 1) createInput := store.createInputs[0] require.Equal(t, common.DeliveryID("clone-123"), createInput.Delivery.DeliveryID) require.Equal(t, original.DeliveryID, createInput.Delivery.ResendParentDeliveryID) require.Equal(t, deliverydomain.SourceOperatorResend, createInput.Delivery.Source) require.Equal(t, common.IdempotencyKey("operator:resend:"+original.DeliveryID.String()), createInput.Delivery.IdempotencyKey) require.Equal(t, deliverydomain.StatusQueued, createInput.Delivery.Status) require.Equal(t, 1, createInput.Delivery.AttemptCount) require.Empty(t, createInput.Delivery.LastAttemptStatus) require.Nil(t, createInput.Delivery.SentAt) require.Nil(t, createInput.Delivery.FailedAt) require.Equal(t, attempt.StatusScheduled, createInput.FirstAttempt.Status) require.Equal(t, 1, createInput.FirstAttempt.AttemptNo) require.NotNil(t, createInput.DeliveryPayload) require.Equal(t, common.DeliveryID("clone-123"), createInput.DeliveryPayload.DeliveryID) require.Equal(t, payload.Attachments, createInput.DeliveryPayload.Attachments) require.Equal(t, originalCopy, original) } func TestServiceExecuteLogsCloneCreationAndCreatesSpan(t *testing.T) { t.Parallel() original := validOriginalDelivery() payload := validPayload(original.DeliveryID) loggerBuffer := &bytes.Buffer{} recorder := tracetest.NewSpanRecorder() tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) telemetry := &stubTelemetry{} store := &stubStore{ delivery: &original, payload: &payload, } service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: &stubIDGenerator{ids: []common.DeliveryID{"clone-456"}}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, TracerProvider: tracerProvider, Logger: slog.New(slog.NewJSONHandler(loggerBuffer, nil)), }) _, err := service.Execute(context.Background(), Input{DeliveryID: original.DeliveryID}) require.NoError(t, err) require.Equal(t, []string{"operator_resend:queued"}, telemetry.statuses) require.Contains(t, loggerBuffer.String(), "\"delivery_id\":\"clone-456\"") require.Contains(t, loggerBuffer.String(), "\"source\":\"operator_resend\"") require.Contains(t, loggerBuffer.String(), "\"template_id\":\"game.turn.ready\"") require.Contains(t, loggerBuffer.String(), "\"otel_trace_id\":") require.True(t, hasResendSpanNamed(recorder.Ended(), "mail.resend_delivery")) } type stubStore struct { delivery *deliverydomain.Delivery payload *acceptgenericdelivery.DeliveryPayload createInputs []CreateResendInput } func (store *stubStore) GetDelivery(context.Context, common.DeliveryID) (deliverydomain.Delivery, bool, error) { if store.delivery == nil { return deliverydomain.Delivery{}, false, nil } return *store.delivery, true, nil } func (store *stubStore) GetDeliveryPayload(context.Context, common.DeliveryID) (acceptgenericdelivery.DeliveryPayload, bool, error) { if store.payload == nil { return acceptgenericdelivery.DeliveryPayload{}, false, nil } return *store.payload, true, nil } func (store *stubStore) CreateResend(_ context.Context, input CreateResendInput) error { store.createInputs = append(store.createInputs, input) return nil } type stubIDGenerator struct { ids []common.DeliveryID } func (generator *stubIDGenerator) NewDeliveryID() (common.DeliveryID, error) { if len(generator.ids) == 0 { return "", nil } next := generator.ids[0] generator.ids = generator.ids[1:] return next, nil } type stubClock struct { now time.Time } func (clock stubClock) Now() time.Time { return clock.now } type stubTelemetry struct { statuses []string } func (telemetry *stubTelemetry) RecordDeliveryStatusTransition(_ context.Context, status string, source string) { telemetry.statuses = append(telemetry.statuses, source+":"+status) } func newTestService(t *testing.T, cfg Config) *Service { t.Helper() service, err := New(cfg) require.NoError(t, err) return service } func fixedNow() time.Time { return time.Unix(1_775_122_100, 0).UTC() } func validOriginalDelivery() deliverydomain.Delivery { createdAt := time.Unix(1_775_121_700, 0).UTC() updatedAt := createdAt.Add(time.Minute) sentAt := updatedAt record := deliverydomain.Delivery{ DeliveryID: common.DeliveryID("delivery-original"), Source: deliverydomain.SourceNotification, PayloadMode: deliverydomain.PayloadModeTemplate, TemplateID: common.TemplateID("game.turn.ready"), Envelope: deliverydomain.Envelope{ To: []common.Email{common.Email("pilot@example.com")}, Cc: []common.Email{common.Email("copilot@example.com")}, Bcc: []common.Email{common.Email("ops@example.com")}, ReplyTo: []common.Email{common.Email("noreply@example.com")}, }, Content: deliverydomain.Content{ Subject: "Turn ready", TextBody: "Your next turn is ready", }, Attachments: []common.AttachmentMetadata{ {Filename: "instructions.txt", ContentType: "text/plain; charset=utf-8", SizeBytes: 7}, }, Locale: common.Locale("en"), TemplateVariables: map[string]any{"turn": 7}, LocaleFallbackUsed: true, IdempotencyKey: common.IdempotencyKey("notification:delivery-original"), Status: deliverydomain.StatusSent, AttemptCount: 2, LastAttemptStatus: attempt.StatusProviderAccepted, ProviderSummary: "provider=smtp result=accepted", CreatedAt: createdAt, UpdatedAt: updatedAt, SentAt: &sentAt, } if err := record.Validate(); err != nil { panic(err) } return record } func validPayload(deliveryID common.DeliveryID) acceptgenericdelivery.DeliveryPayload { payload := acceptgenericdelivery.DeliveryPayload{ DeliveryID: deliveryID, Attachments: []acceptgenericdelivery.AttachmentPayload{ { Filename: "instructions.txt", ContentType: "text/plain; charset=utf-8", ContentBase64: "cmVhZCBtZQ==", SizeBytes: 7, }, }, } if err := payload.Validate(); err != nil { panic(err) } return payload } var _ Store = (*stubStore)(nil) var _ DeliveryIDGenerator = (*stubIDGenerator)(nil) var _ Clock = stubClock{} var _ Telemetry = (*stubTelemetry)(nil) func hasResendSpanNamed(spans []sdktrace.ReadOnlySpan, name string) bool { for _, span := range spans { if span.Name() == name { return true } } return false }