package listdeliveries import ( "context" "errors" "testing" "time" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "github.com/stretchr/testify/require" ) func TestServiceExecuteAppliesDefaultLimit(t *testing.T) { t.Parallel() store := &stubStore{ result: Result{ Items: []deliverydomain.Delivery{validDelivery("delivery-default", "notification:delivery-default")}, }, } service := newTestService(t, Config{Store: store}) result, err := service.Execute(context.Background(), Input{}) require.NoError(t, err) require.Len(t, result.Items, 1) require.Equal(t, DefaultLimit, store.lastInput.Limit) } func TestInputValidateRejectsInvalidFiltersAndCursor(t *testing.T) { t.Parallel() validCursor := Cursor{ CreatedAt: time.Unix(1_775_121_700, 0).UTC(), DeliveryID: common.DeliveryID("delivery-cursor"), } validFrom := time.Unix(1_775_121_700, 0).UTC() validTo := validFrom.Add(time.Minute) tests := []struct { name string input Input wantErr string }{ { name: "invalid recipient", input: Input{ Filters: Filters{Recipient: common.Email("not-an-email")}, }, wantErr: "recipient:", }, { name: "invalid status", input: Input{ Filters: Filters{Status: deliverydomain.Status("bad")}, }, wantErr: `status "bad" is unsupported`, }, { name: "invalid source", input: Input{ Filters: Filters{Source: deliverydomain.Source("bad")}, }, wantErr: `source "bad" is unsupported`, }, { name: "invalid template id", input: Input{ Filters: Filters{TemplateID: common.TemplateID(" bad-template")}, }, wantErr: "template id:", }, { name: "invalid idempotency key", input: Input{ Filters: Filters{IdempotencyKey: common.IdempotencyKey(" bad-key")}, }, wantErr: "idempotency key:", }, { name: "invalid created at range", input: Input{ Filters: Filters{ FromCreatedAt: &validTo, ToCreatedAt: &validFrom, }, }, wantErr: "from created at must not be after to created at", }, { name: "invalid cursor", input: Input{ Cursor: &Cursor{ CreatedAt: time.Time{}, DeliveryID: common.DeliveryID("delivery-cursor"), }, }, wantErr: "cursor:", }, { name: "valid cursor and filters", input: Input{ Limit: 1, Cursor: &Cursor{ CreatedAt: validCursor.CreatedAt, DeliveryID: validCursor.DeliveryID, }, Filters: Filters{ Recipient: common.Email("pilot@example.com"), Status: deliverydomain.StatusSent, Source: deliverydomain.SourceNotification, TemplateID: common.TemplateID("auth.login_code"), IdempotencyKey: common.IdempotencyKey("notification:delivery-123"), FromCreatedAt: &validFrom, ToCreatedAt: &validTo, }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() err := tt.input.Validate() if tt.wantErr == "" { require.NoError(t, err) return } require.Error(t, err) require.ErrorContains(t, err, tt.wantErr) }) } } func TestServiceExecutePropagatesInvalidCursor(t *testing.T) { t.Parallel() service := newTestService(t, Config{ Store: &stubStore{listErr: ErrInvalidCursor}, }) _, err := service.Execute(context.Background(), Input{Limit: 1}) require.ErrorIs(t, err, ErrInvalidCursor) } func TestServiceExecuteWrapsServiceUnavailable(t *testing.T) { t.Parallel() service := newTestService(t, Config{ Store: &stubStore{listErr: errors.New("redis unavailable")}, }) _, err := service.Execute(context.Background(), Input{Limit: 1}) require.ErrorIs(t, err, ErrServiceUnavailable) require.ErrorContains(t, err, "redis unavailable") } func TestServiceExecuteRejectsOversizedResult(t *testing.T) { t.Parallel() service := newTestService(t, Config{ Store: &stubStore{ result: Result{ Items: []deliverydomain.Delivery{ validDelivery("delivery-one", "notification:delivery-one"), validDelivery("delivery-two", "notification:delivery-two"), }, }, }, }) _, err := service.Execute(context.Background(), Input{Limit: 1}) require.ErrorIs(t, err, ErrServiceUnavailable) require.ErrorContains(t, err, "returned 2 items for limit 1") } type stubStore struct { lastInput Input result Result listErr error } func (store *stubStore) List(_ context.Context, input Input) (Result, error) { store.lastInput = input if store.listErr != nil { return Result{}, store.listErr } return store.result, nil } func newTestService(t *testing.T, cfg Config) *Service { t.Helper() service, err := New(cfg) require.NoError(t, err) return service } func validDelivery(deliveryID string, idempotencyKey common.IdempotencyKey) deliverydomain.Delivery { createdAt := time.Unix(1_775_121_700, 0).UTC() updatedAt := createdAt.Add(time.Minute) sentAt := updatedAt.Add(time.Second) record := deliverydomain.Delivery{ DeliveryID: common.DeliveryID(deliveryID), Source: deliverydomain.SourceNotification, PayloadMode: deliverydomain.PayloadModeRendered, Envelope: deliverydomain.Envelope{To: []common.Email{common.Email("pilot@example.com")}}, Content: deliverydomain.Content{Subject: "Ready", TextBody: "Turn ready"}, IdempotencyKey: idempotencyKey, Status: deliverydomain.StatusSent, AttemptCount: 1, CreatedAt: createdAt, UpdatedAt: updatedAt, SentAt: &sentAt, } if err := record.Validate(); err != nil { panic(err) } return record } var _ Store = (*stubStore)(nil)