feat: mail service
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user