feat: mail service

This commit is contained in:
Ilia Denisov
2026-04-17 18:39:16 +02:00
committed by GitHub
parent 23ffcb7535
commit 5b7593e6f6
183 changed files with 31215 additions and 248 deletions
@@ -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)