Files
galaxy-game/backend/internal/mail/admin_test.go
T
2026-05-06 10:14:55 +03:00

169 lines
5.2 KiB
Go

package mail_test
import (
"context"
"errors"
"testing"
"time"
"galaxy/backend/internal/config"
"galaxy/backend/internal/mail"
"github.com/google/uuid"
"go.uber.org/zap/zaptest"
)
func TestAdminListPagination(t *testing.T) {
t.Parallel()
db := startPostgres(t)
svc := mail.NewService(mail.Deps{
Store: mail.NewStore(db),
SMTP: newRecordingSender(),
Config: config.MailConfig{WorkerInterval: time.Hour, MaxAttempts: 3},
Logger: zaptest.NewLogger(t),
})
const total = 7
for i := range total {
if err := svc.EnqueueLoginCode(context.Background(), "a@example.test", "1234"+string(rune('0'+i)), 5*time.Minute); err != nil {
t.Fatalf("enqueue %d: %v", i, err)
}
}
page, err := svc.AdminListDeliveries(context.Background(), 1, 3)
if err != nil {
t.Fatalf("list page 1: %v", err)
}
if len(page.Items) != 3 {
t.Fatalf("page1 size=%d want 3", len(page.Items))
}
if page.Total != total {
t.Fatalf("page1 total=%d want %d", page.Total, total)
}
page, err = svc.AdminListDeliveries(context.Background(), 3, 3)
if err != nil {
t.Fatalf("list page 3: %v", err)
}
if len(page.Items) != 1 {
t.Fatalf("page3 size=%d want 1", len(page.Items))
}
}
func TestAdminGetDeliveryNotFound(t *testing.T) {
t.Parallel()
db := startPostgres(t)
svc := mail.NewService(mail.Deps{
Store: mail.NewStore(db),
SMTP: newRecordingSender(),
Config: config.MailConfig{WorkerInterval: time.Hour, MaxAttempts: 3},
Logger: zaptest.NewLogger(t),
})
if _, err := svc.AdminGetDelivery(context.Background(), uuid.New()); !errors.Is(err, mail.ErrDeliveryNotFound) {
t.Fatalf("get missing: want ErrDeliveryNotFound, got %v", err)
}
}
func TestAdminResendStateMatrix(t *testing.T) {
t.Parallel()
db := startPostgres(t)
sender := newRecordingSender()
// Match the number of Send calls the matrix triggers (initial
// success path + resend re-send for the dead-lettered row).
sender.behaviour = []func(mail.OutboundMessage) error{
func(mail.OutboundMessage) error { return errors.New("transient #1") },
func(mail.OutboundMessage) error { return errors.New("transient #2") },
func(mail.OutboundMessage) error { return nil }, // sent path
}
clock := time.Now().UTC().Add(-2 * time.Hour) // bring next_attempt_at into the past
svc := mail.NewService(mail.Deps{
Store: mail.NewStore(db),
SMTP: sender,
Config: config.MailConfig{WorkerInterval: time.Hour, MaxAttempts: 2},
Now: func() time.Time { return clock },
Logger: zaptest.NewLogger(t),
})
worker := mail.NewWorker(svc)
// 1. Drive a row to dead-lettered (two failures with MaxAttempts=2).
if err := svc.EnqueueLoginCode(context.Background(), "dead@example.test", "111111", 5*time.Minute); err != nil {
t.Fatalf("enqueue dead: %v", err)
}
if err := worker.Tick(context.Background()); err != nil {
t.Fatalf("tick #1: %v", err)
}
if err := worker.Tick(context.Background()); err != nil {
t.Fatalf("tick #2: %v", err)
}
deadList, err := svc.AdminListDeliveries(context.Background(), 1, 5)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(deadList.Items) != 1 || deadList.Items[0].Status != mail.StatusDeadLettered {
t.Fatalf("want 1 dead-lettered row, got %+v", deadList.Items)
}
deadID := deadList.Items[0].DeliveryID
// 2. Resend the dead-lettered row -> 200, status flips to pending,
// attempts=0.
resent, err := svc.AdminResendDelivery(context.Background(), deadID)
if err != nil {
t.Fatalf("resend dead: %v", err)
}
if resent.Status != mail.StatusPending {
t.Fatalf("status after resend=%q want pending", resent.Status)
}
if resent.Attempts != 0 {
t.Fatalf("attempts after resend=%d want 0", resent.Attempts)
}
// 3. Drive the worker once more — third Send call returns nil so
// the row transitions to sent.
if err := worker.Tick(context.Background()); err != nil {
t.Fatalf("tick post-resend: %v", err)
}
d, err := svc.AdminGetDelivery(context.Background(), deadID)
if err != nil {
t.Fatalf("get after send: %v", err)
}
if d.Status != mail.StatusSent {
t.Fatalf("status=%q want sent", d.Status)
}
// 4. Resend on `sent` -> ErrResendOnSent.
if _, err := svc.AdminResendDelivery(context.Background(), deadID); !errors.Is(err, mail.ErrResendOnSent) {
t.Fatalf("resend on sent: want ErrResendOnSent, got %v", err)
}
// 5. Resend on missing -> ErrDeliveryNotFound.
if _, err := svc.AdminResendDelivery(context.Background(), uuid.New()); !errors.Is(err, mail.ErrDeliveryNotFound) {
t.Fatalf("resend missing: want ErrDeliveryNotFound, got %v", err)
}
}
func TestServiceStats(t *testing.T) {
t.Parallel()
db := startPostgres(t)
svc := mail.NewService(mail.Deps{
Store: mail.NewStore(db),
SMTP: newRecordingSender(),
Config: config.MailConfig{WorkerInterval: time.Hour, MaxAttempts: 3},
Logger: zaptest.NewLogger(t),
})
for i := range 3 {
if err := svc.EnqueueLoginCode(context.Background(), "stats@example.test", "55555"+string(rune('0'+i)), 5*time.Minute); err != nil {
t.Fatalf("enqueue: %v", err)
}
}
stats, err := svc.Stats(context.Background())
if err != nil {
t.Fatalf("stats: %v", err)
}
if stats[mail.StatusPending] != 3 {
t.Fatalf("pending=%d want 3", stats[mail.StatusPending])
}
if _, ok := stats[mail.StatusSent]; !ok {
t.Fatal("Stats must always return all four buckets")
}
}