package acceptauthdelivery import ( "bytes" "context" "errors" "log/slog" "testing" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/domain/idempotency" "github.com/stretchr/testify/require" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" ) func TestServiceExecuteAcceptsQueuedDelivery(t *testing.T) { t.Parallel() store := &stubStore{} telemetry := &stubTelemetry{} service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: stubIDGenerator{ids: []common.DeliveryID{"delivery-queued"}}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, IdempotencyTTL: 7 * 24 * time.Hour, }) result, err := service.Execute(context.Background(), validInput()) require.NoError(t, err) require.Equal(t, Result{Outcome: OutcomeSent}, result) require.Len(t, store.createInputs, 1) require.NotNil(t, store.createInputs[0].FirstAttempt) require.Equal(t, deliverydomain.StatusQueued, store.createInputs[0].Delivery.Status) require.Equal(t, []string{"sent"}, telemetry.outcomes) require.Equal(t, 1, telemetry.accepted) require.Equal(t, []string{"authsession:queued"}, telemetry.statuses) } func TestServiceExecuteAcceptsSuppressedDelivery(t *testing.T) { t.Parallel() store := &stubStore{} telemetry := &stubTelemetry{} service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: stubIDGenerator{ids: []common.DeliveryID{"delivery-suppressed"}}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, IdempotencyTTL: 7 * 24 * time.Hour, SuppressOutbound: true, }) result, err := service.Execute(context.Background(), validInput()) require.NoError(t, err) require.Equal(t, Result{Outcome: OutcomeSuppressed}, result) require.Len(t, store.createInputs, 1) require.Nil(t, store.createInputs[0].FirstAttempt) require.Equal(t, deliverydomain.StatusSuppressed, store.createInputs[0].Delivery.Status) require.Equal(t, []string{"suppressed"}, telemetry.outcomes) require.Equal(t, 1, telemetry.accepted) require.Equal(t, []string{"authsession:suppressed"}, telemetry.statuses) } func TestServiceExecuteReturnsStableDuplicateResult(t *testing.T) { t.Parallel() input := validInput() fingerprint, err := input.Fingerprint() require.NoError(t, err) store := &stubStore{ idempotencyRecord: &idempotency.Record{ Source: deliverydomain.SourceAuthSession, IdempotencyKey: input.IdempotencyKey, DeliveryID: common.DeliveryID("delivery-existing"), RequestFingerprint: fingerprint, CreatedAt: fixedNow(), ExpiresAt: fixedNow().Add(7 * 24 * time.Hour), }, deliveryRecord: &deliverydomain.Delivery{ DeliveryID: common.DeliveryID("delivery-existing"), Source: deliverydomain.SourceAuthSession, PayloadMode: deliverydomain.PayloadModeTemplate, TemplateID: AuthTemplateID, Envelope: deliverydomain.Envelope{ To: []common.Email{input.Email}, }, Locale: input.Locale, TemplateVariables: map[string]any{ "code": input.Code, }, IdempotencyKey: input.IdempotencyKey, Status: deliverydomain.StatusSuppressed, CreatedAt: fixedNow(), UpdatedAt: fixedNow(), SuppressedAt: ptrTime(fixedNow()), }, } require.NoError(t, store.idempotencyRecord.Validate()) require.NoError(t, store.deliveryRecord.Validate()) telemetry := &stubTelemetry{} service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: stubIDGenerator{}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, IdempotencyTTL: 7 * 24 * time.Hour, }) result, err := service.Execute(context.Background(), input) require.NoError(t, err) require.Equal(t, Result{Outcome: OutcomeSuppressed}, result) require.Empty(t, store.createInputs) require.Equal(t, []string{"duplicate"}, telemetry.outcomes) } func TestServiceExecuteRejectsConflictingReplay(t *testing.T) { t.Parallel() input := validInput() store := &stubStore{ idempotencyRecord: &idempotency.Record{ Source: deliverydomain.SourceAuthSession, IdempotencyKey: input.IdempotencyKey, DeliveryID: common.DeliveryID("delivery-existing"), RequestFingerprint: "sha256:other", CreatedAt: fixedNow(), ExpiresAt: fixedNow().Add(7 * 24 * time.Hour), }, } require.NoError(t, store.idempotencyRecord.Validate()) telemetry := &stubTelemetry{} service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: stubIDGenerator{}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, IdempotencyTTL: 7 * 24 * time.Hour, }) _, err := service.Execute(context.Background(), input) require.Error(t, err) require.ErrorIs(t, err, ErrConflict) require.Equal(t, []string{"conflict"}, telemetry.outcomes) } func TestServiceExecuteReturnsServiceUnavailableOnCreateFailure(t *testing.T) { t.Parallel() telemetry := &stubTelemetry{} service := newTestService(t, Config{ Store: &stubStore{ createErr: errors.New("redis unavailable"), }, DeliveryIDGenerator: stubIDGenerator{ids: []common.DeliveryID{"delivery-queued"}}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, IdempotencyTTL: 7 * 24 * time.Hour, }) _, err := service.Execute(context.Background(), validInput()) require.Error(t, err) require.ErrorIs(t, err, ErrServiceUnavailable) require.Equal(t, []string{"service_unavailable"}, telemetry.outcomes) } func TestServiceExecuteLogsAcceptedDeliveryAndCreatesSpan(t *testing.T) { t.Parallel() store := &stubStore{} telemetry := &stubTelemetry{} loggerBuffer := &bytes.Buffer{} recorder := tracetest.NewSpanRecorder() tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) service := newTestService(t, Config{ Store: store, DeliveryIDGenerator: stubIDGenerator{ids: []common.DeliveryID{"delivery-queued"}}, Clock: stubClock{now: fixedNow()}, Telemetry: telemetry, TracerProvider: tracerProvider, Logger: slog.New(slog.NewJSONHandler(loggerBuffer, nil)), IdempotencyTTL: 7 * 24 * time.Hour, }) _, err := service.Execute(context.Background(), validInput()) require.NoError(t, err) require.Contains(t, loggerBuffer.String(), "\"delivery_id\":\"delivery-queued\"") require.Contains(t, loggerBuffer.String(), "\"source\":\"authsession\"") require.Contains(t, loggerBuffer.String(), "\"template_id\":\"auth.login_code\"") require.Contains(t, loggerBuffer.String(), "\"otel_trace_id\":") require.True(t, hasSpanNamed(recorder.Ended(), "mail.accept_auth_delivery")) } func TestInputFingerprintStableForEquivalentInput(t *testing.T) { t.Parallel() first := validInput() second := validInput() firstFingerprint, err := first.Fingerprint() require.NoError(t, err) secondFingerprint, err := second.Fingerprint() require.NoError(t, err) require.Equal(t, firstFingerprint, secondFingerprint) } type stubStore struct { createInputs []CreateAcceptanceInput createErr error idempotencyRecord *idempotency.Record deliveryRecord *deliverydomain.Delivery } func (store *stubStore) CreateAcceptance(_ context.Context, input CreateAcceptanceInput) error { store.createInputs = append(store.createInputs, input) return store.createErr } func (store *stubStore) GetIdempotency(_ context.Context, _ deliverydomain.Source, _ common.IdempotencyKey) (idempotency.Record, bool, error) { if store.idempotencyRecord == nil { return idempotency.Record{}, false, nil } return *store.idempotencyRecord, true, nil } func (store *stubStore) GetDelivery(_ context.Context, _ common.DeliveryID) (deliverydomain.Delivery, bool, error) { if store.deliveryRecord == nil { return deliverydomain.Delivery{}, false, nil } return *store.deliveryRecord, true, nil } type stubIDGenerator struct { ids []common.DeliveryID } func (generator stubIDGenerator) NewDeliveryID() (common.DeliveryID, error) { if len(generator.ids) == 0 { return "", errors.New("no delivery ids left") } return generator.ids[0], nil } type stubClock struct { now time.Time } func (clock stubClock) Now() time.Time { return clock.now } type stubTelemetry struct { outcomes []string accepted int statuses []string } func (telemetry *stubTelemetry) RecordAuthDeliveryOutcome(_ context.Context, outcome string) { telemetry.outcomes = append(telemetry.outcomes, outcome) } func (telemetry *stubTelemetry) RecordAcceptedAuthDelivery(context.Context) { telemetry.accepted++ } 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 validInput() Input { locale, err := common.ParseLocale("en") if err != nil { panic(err) } return Input{ IdempotencyKey: common.IdempotencyKey("challenge-123"), Email: common.Email("pilot@example.com"), Code: "123456", Locale: locale, } } func fixedNow() time.Time { return time.Unix(1_775_121_700, 0).UTC() } func hasSpanNamed(spans []sdktrace.ReadOnlySpan, name string) bool { for _, span := range spans { if span.Name() == name { return true } } return false } var _ = attempt.Attempt{}