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()) } }