Files
mailx/manager_test.go
T

487 lines
13 KiB
Go
Raw Normal View History

2026-08-15 01:38:05 +08:00
package mailx_test
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"testing"
mailx "code.yun.ink/pkg/mailx"
)
func TestNewManagerEmpty(t *testing.T) {
m := mailx.NewManager()
if _, err := m.Sender(""); err == nil {
t.Fatal("Sender() on empty manager should error")
}
if err := m.Send(context.Background(), mailx.NewMessage().To("a@b.com").Subject("s").Build()); err == nil {
t.Fatal("Send() on empty manager should error")
}
}
func TestRegisterSetsDefault(t *testing.T) {
m := mailx.NewManager()
if err := m.Register(newMockSender("a")); err != nil {
t.Fatal(err)
}
if err := m.Register(newMockSender("b")); err != nil {
t.Fatal(err)
}
s, err := m.Sender("")
if err != nil {
t.Fatal(err)
}
if s.Name() != "a" {
t.Errorf("default sender = %q, want a", s.Name())
}
}
func TestRegisterDuplicate(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("a"))
err := m.Register(newMockSender("a"))
if err == nil || !strings.Contains(err.Error(), "already registered") {
t.Fatalf("err = %v, want already registered", err)
}
}
func TestRegisterNil(t *testing.T) {
m := mailx.NewManager()
if err := m.Register(nil); err == nil {
t.Fatal("Register(nil) should error")
}
}
func TestRegisterEmptyName(t *testing.T) {
m := mailx.NewManager()
if err := m.Register(newMockSender("")); err == nil {
t.Fatal("Register(empty name) should error")
}
}
func TestUnregister(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("a"))
_ = m.Register(newMockSender("b"))
m.Unregister("a")
s, err := m.Sender("")
if err != nil {
t.Fatal(err)
}
if s.Name() != "b" {
t.Errorf("fallback default = %q, want b", s.Name())
}
// 注销最后一个后不再有默认通道
m.Unregister("b")
if _, err := m.Sender(""); err == nil {
t.Fatal("expected error after unregistering all senders")
}
}
func TestSetDefault(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("a"))
_ = m.Register(newMockSender("b"))
if err := m.SetDefault("b"); err != nil {
t.Fatal(err)
}
s, _ := m.Sender("")
if s.Name() != "b" {
t.Errorf("default = %q, want b", s.Name())
}
if err := m.SetDefault("nope"); err == nil {
t.Fatal("SetDefault(unknown) should error")
}
}
func TestSenderLookup(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("a"))
if _, err := m.Sender("a"); err != nil {
t.Fatalf("Sender(a) = %v", err)
}
if _, err := m.Sender("nope"); err == nil {
t.Fatal("Sender(unknown) should error")
}
}
func TestSend(t *testing.T) {
m := mailx.NewManager()
mock := newMockSender("mock")
_ = m.Register(mock)
msg := mailx.NewMessage().To("a@b.com").Subject("s").Body("b").Build()
if err := m.Send(context.Background(), msg); err != nil {
t.Fatal(err)
}
if mock.count() != 1 {
t.Fatalf("send count = %d, want 1", mock.count())
}
}
func TestSendWithRoutesByName(t *testing.T) {
m := mailx.NewManager()
a := newMockSender("a")
b := newMockSender("b")
_ = m.Register(a)
_ = m.Register(b)
msg := mailx.NewMessage().To("x@y.com").Subject("s").Build()
if err := m.SendWith(context.Background(), "b", msg); err != nil {
t.Fatal(err)
}
if a.count() != 0 || b.count() != 1 {
t.Fatalf("a=%d b=%d, want a=0 b=1", a.count(), b.count())
}
if err := m.SendWith(context.Background(), "nope", msg); err == nil {
t.Fatal("SendWith(unknown) should error")
}
}
func TestSendBy(t *testing.T) {
m := mailx.NewManager()
mock := newMockSender("temp")
msg := mailx.NewMessage().To("x@y.com").Subject("s").Build()
if err := m.SendBy(context.Background(), mock, msg); err != nil {
t.Fatal(err)
}
if mock.count() != 1 {
t.Fatalf("count = %d, want 1", mock.count())
}
if err := m.SendBy(context.Background(), nil, msg); err == nil {
t.Fatal("SendBy(nil) should error")
}
}
func TestSendValidation(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("mock"))
if err := m.Send(context.Background(), nil); err == nil {
t.Fatal("Send(nil) should error")
}
if err := m.Send(context.Background(), &mailx.Message{Subject: "s"}); err == nil {
t.Fatal("message without recipients should error")
}
}
func TestSendErrorPropagated(t *testing.T) {
m := mailx.NewManager()
mock := newMockSender("mock")
mock.err = errors.New("boom")
_ = m.Register(mock)
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
err := m.Send(context.Background(), msg)
if err == nil || !strings.Contains(err.Error(), "boom") {
t.Fatalf("err = %v, want containing boom", err)
}
}
func TestSendInjectsLogger(t *testing.T) {
m := mailx.NewManager()
mock := newMockSender("mock")
_ = m.Register(mock)
m.SetLogger(recordingLogger{})
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
ctx := context.Background()
if err := m.Send(ctx, msg); err != nil {
t.Fatal(err)
}
// 发送时 context 应带有管理器注入的 logger(通道可通过 LoggerFromContext 取到)
if _, ok := mock.lastLogger().(recordingLogger); !ok {
t.Fatalf("injected logger type = %T, want recordingLogger", mock.lastLogger())
}
}
func TestQueryMethods(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("a"))
_ = m.Register(newMockSender("b"))
if !m.Registered("a") || !m.Registered("b") {
t.Error("Registered() should be true for registered senders")
}
if m.Registered("nope") {
t.Error("Registered() should be false for unknown sender")
}
names := m.Names()
if len(names) != 2 {
t.Fatalf("Names() = %v, want 2", names)
}
seen := map[string]bool{}
for _, n := range names {
seen[n] = true
}
if !seen["a"] || !seen["b"] {
t.Errorf("Names() missing a/b: %v", names)
}
if m.Default() != "a" {
t.Errorf("Default() = %q, want a", m.Default())
}
_ = m.SetDefault("b")
if m.Default() != "b" {
t.Errorf("Default() = %q, want b", m.Default())
}
}
func TestErrSentinel(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("mock"))
// 空 manager 且未注册任何通道时,查默认通道返回 ErrSenderNotFound
if _, err := mailx.NewManager().Sender(""); !errors.Is(err, mailx.ErrSenderNotFound) {
t.Errorf("Sender() on empty manager = %v, want ErrSenderNotFound", err)
}
// 未注册的名称返回 ErrSenderNotFound
if _, err := m.Sender("nope"); !errors.Is(err, mailx.ErrSenderNotFound) {
t.Errorf("Sender(unknown) = %v, want ErrSenderNotFound", err)
}
// 消息校验失败返回 ErrInvalidMessage
if err := m.Send(context.Background(), &mailx.Message{Subject: "s"}); !errors.Is(err, mailx.ErrInvalidMessage) {
t.Errorf("Send(no recipients) = %v, want ErrInvalidMessage", err)
}
// 非法配置返回 ErrInvalidConfig
if err := m.Register(nil); !errors.Is(err, mailx.ErrInvalidConfig) {
t.Errorf("Register(nil) = %v, want ErrInvalidConfig", err)
}
}
// TestConcurrentRegisterUnregister 验证并发注册/注销不 panic 且不丢数据
func TestConcurrentRegisterUnregister(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("base"))
ctx := context.Background()
const n = 30
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
name := fmt.Sprintf("sender-%d", i)
_ = m.RegisterNamed(name, newMockSender("mock"))
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
_ = m.SendWith(ctx, name, msg)
m.Unregister(name)
}(i)
}
wg.Wait()
if !m.Registered("base") {
t.Error("base sender should still be registered")
}
}
func TestConcurrentSetLogger(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("mock"))
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
ctx := context.Background()
const n = 100
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if i%2 == 0 {
m.SetLogger(recordingLogger{})
} else {
_ = m.Send(ctx, msg) // 内部读取 Logger,验证并发读写不 panic/不竞态
}
}(i)
}
wg.Wait()
// Logger() 应始终返回非 nil 且类型正确
if _, ok := m.Logger().(recordingLogger); !ok {
t.Fatalf("Logger() type = %T, want recordingLogger", m.Logger())
}
}
func TestConcurrentSend(t *testing.T) {
m := mailx.NewManager()
mock := newMockSender("mock")
_ = m.Register(mock)
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
ctx := context.Background()
const n = 50
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := m.Send(ctx, msg); err != nil {
t.Errorf("Send: %v", err)
}
}()
}
wg.Wait()
if mock.count() != n {
t.Fatalf("count = %d, want %d", mock.count(), n)
}
}
// TestRegisterNamedSameType 验证同一通道类型可注册多份不同配置的实例
func TestRegisterNamedSameType(t *testing.T) {
m := mailx.NewManager()
main := newMockSender("smtp")
backup := newMockSender("smtp")
if err := m.RegisterNamed("smtp-main", main); err != nil {
t.Fatal(err)
}
if err := m.RegisterNamed("smtp-backup", backup); err != nil {
t.Fatal(err)
}
// 两个实例都注册成功
if len(m.Names()) != 2 {
t.Fatalf("Names() = %v, want 2 instances", m.Names())
}
// 类型相同但实例不同
infos := m.Senders()
if len(infos) != 2 {
t.Fatalf("Senders() = %v, want 2", infos)
}
for _, info := range infos {
if info.Type != "smtp" {
t.Errorf("SenderInfo.Type = %q, want smtp", info.Type)
}
}
// 按实例名路由发送到具体配置
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
if err := m.SendWith(context.Background(), "smtp-backup", msg); err != nil {
t.Fatal(err)
}
if main.count() != 0 || backup.count() != 1 {
t.Errorf("main=%d backup=%d, want main=0 backup=1", main.count(), backup.count())
}
}
// TestRegisterNamedDuplicateInstance 验证实例名冲突时报错
func TestRegisterNamedDuplicateInstance(t *testing.T) {
m := mailx.NewManager()
_ = m.RegisterNamed("smtp-main", newMockSender("smtp"))
err := m.RegisterNamed("smtp-main", newMockSender("smtp"))
if err == nil || !strings.Contains(err.Error(), "already registered") {
t.Fatalf("err = %v, want already registered", err)
}
}
// TestRegisterNamedEmptyName 验证空实例名时报错
func TestRegisterNamedEmptyName(t *testing.T) {
m := mailx.NewManager()
if err := m.RegisterNamed("", newMockSender("smtp")); !errors.Is(err, mailx.ErrInvalidConfig) {
t.Fatalf("err = %v, want ErrInvalidConfig", err)
}
}
// TestRegisterSameTypeDirect 验证 Register 用通道类型名注册时,同类型只允许一个(默认实例)
func TestRegisterSameTypeDirect(t *testing.T) {
m := mailx.NewManager()
_ = m.Register(newMockSender("smtp"))
if err := m.Register(newMockSender("smtp")); err == nil {
t.Fatal("Register(same type) should error")
}
}
// TestSendersInfo 验证 Senders() 返回实例名与类型
func TestSendersInfo(t *testing.T) {
m := mailx.NewManager()
_ = m.RegisterNamed("smtp-main", newMockSender("smtp"))
_ = m.RegisterNamed("aliyun-prod", newMockSender("aliyun"))
infos := m.Senders()
want := map[string]string{"smtp-main": "smtp", "aliyun-prod": "aliyun"}
got := map[string]string{}
for _, i := range infos {
got[i.Name] = i.Type
}
for k, v := range want {
if got[k] != v {
t.Errorf("Senders()[%s] = %q, want %q (all: %+v)", k, got[k], v, infos)
}
}
if len(got) != len(want) {
t.Errorf("Senders() count = %d, want %d", len(got), len(want))
}
}
// TestUnregisterNamedInstance 验证按实例名注销后不再路由
func TestUnregisterNamedInstance(t *testing.T) {
m := mailx.NewManager()
main := newMockSender("smtp")
backup := newMockSender("smtp")
_ = m.RegisterNamed("smtp-main", main)
_ = m.RegisterNamed("smtp-backup", backup)
m.Unregister("smtp-main")
if m.Registered("smtp-main") {
t.Error("smtp-main should be unregistered")
}
if !m.Registered("smtp-backup") {
t.Error("smtp-backup should remain registered")
}
// 注销的实例不可再路由
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
if err := m.SendWith(context.Background(), "smtp-main", msg); !errors.Is(err, mailx.ErrSenderNotFound) {
t.Errorf("SendWith(unregistered) = %v, want ErrSenderNotFound", err)
}
}
// TestUnregisterLastFallback 验证注销默认实例后自动回退到其他实例
func TestUnregisterLastFallback(t *testing.T) {
m := mailx.NewManager()
_ = m.RegisterNamed("a", newMockSender("smtp"))
_ = m.RegisterNamed("b", newMockSender("aliyun"))
if m.Default() != "a" {
t.Fatalf("Default() = %q, want a", m.Default())
}
m.Unregister("a")
if m.Default() != "b" {
t.Errorf("Default() after unregister = %q, want b", m.Default())
}
}
// TestSetDefaultNamedInstance 验证 SetDefault 支持实例名
func TestSetDefaultNamedInstance(t *testing.T) {
m := mailx.NewManager()
_ = m.RegisterNamed("smtp-main", newMockSender("smtp"))
_ = m.RegisterNamed("smtp-backup", newMockSender("smtp"))
if err := m.SetDefault("smtp-backup"); err != nil {
t.Fatal(err)
}
s, _ := m.Sender("")
if s.Name() != "smtp" {
t.Errorf("default sender type = %q, want smtp", s.Name())
}
// 确认默认实例是 backup(发送到 backup
backup, _ := m.Sender("smtp-backup")
if _, ok := backup.(*mockSender); !ok || m.Default() != "smtp-backup" {
t.Errorf("Default() = %q, want smtp-backup", m.Default())
}
}