Files
mailx/manager_test.go
T
2026-08-15 01:38:05 +08:00

487 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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())
}
}