package mailgun import ( "context" "errors" "testing" mailx "code.yun.ink/pkg/mailx" "github.com/mailgun/mailgun-go/v4" ) // mockMG 实现 mgClient 接口,记录 NewMessage 的参数与 Send 调用 type mockMG struct { sendErr error // NewMessage 捕获 from, subject, text string to []string // Send 捕获 msgSent bool sentMsg *mailgun.Message } func (m *mockMG) NewMessage(from, subject, text string, to ...string) *mailgun.Message { m.from, m.subject, m.text = from, subject, text m.to = to return mailgun.NewMessage(from, subject, text, to...) } func (m *mockMG) Send(_ context.Context, msg *mailgun.Message) (string, string, error) { m.msgSent = true m.sentMsg = msg if m.sendErr != nil { return "", "", m.sendErr } return "mock-id", "queued", nil } // TestSendSuccess 验证完整发送流程与消息构造参数 func TestSendSuccess(t *testing.T) { mock := &mockMG{} g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com", Sender: "noreply@example.com"}, client: mock} msg := mailx.NewMessage(). To("a@example.com", "b@example.com"). Cc("cc@example.com"). Bcc("bcc@example.com"). Subject("hi"). Text("plain"). HTML("html"). ReplyTo("reply@example.com"). AttachBytes("a.txt", []byte("data")). InlineImageBytes("cid1", "a.png", []byte{1}). Build() if err := g.Send(context.Background(), msg); err != nil { t.Fatal(err) } if !mock.msgSent { t.Fatal("message not sent") } if mock.from != "noreply@example.com" { t.Errorf("From = %q", mock.from) } if mock.subject != "hi" { t.Errorf("Subject = %q", mock.subject) } if mock.text != "plain" { t.Errorf("Text = %q", mock.text) } if len(mock.to) != 2 || mock.to[0] != "a@example.com" || mock.to[1] != "b@example.com" { t.Errorf("To = %v", mock.to) } } // TestSendFromFallback 验证 msg.From 为空时回退 cfg.Sender func TestSendFromFallback(t *testing.T) { mock := &mockMG{} g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com", Sender: "cfg@example.com"}, client: mock} msg := mailx.NewMessage().To("a@example.com").Subject("s").Build() if err := g.Send(context.Background(), msg); err != nil { t.Fatal(err) } if mock.from != "cfg@example.com" { t.Errorf("From = %q, want cfg fallback", mock.from) } } // TestSendTextFallback 验证 Body 为纯文本时作为 text 传递 func TestSendTextFallback(t *testing.T) { mock := &mockMG{} g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com", Sender: "s@example.com"}, client: mock} msg := mailx.NewMessage().To("a@example.com").Subject("s").Body("plain body no html").Build() if err := g.Send(context.Background(), msg); err != nil { t.Fatal(err) } if mock.text != "plain body no html" { t.Errorf("Text = %q, want fallback to Body", mock.text) } } // TestSendHTMLOnly 验证 Body 为 HTML 时不作为 text 传递 func TestSendHTMLOnly(t *testing.T) { mock := &mockMG{} g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com", Sender: "s@example.com"}, client: mock} msg := mailx.NewMessage().To("a@example.com").Subject("s").HTML("html").Build() if err := g.Send(context.Background(), msg); err != nil { t.Fatal(err) } if mock.text != "" { t.Errorf("Text = %q, want empty for html body", mock.text) } } // TestSendFail 验证发送失败包装为 ErrSendFailed func TestSendFail(t *testing.T) { mock := &mockMG{sendErr: errors.New("mailgun down")} g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com", Sender: "s@example.com"}, client: mock} msg := mailx.NewMessage().To("a@example.com").Subject("s").Build() err := g.Send(context.Background(), msg) if err == nil || !errors.Is(err, mailx.ErrSendFailed) { t.Fatalf("err = %v, want ErrSendFailed", err) } } // TestSendNoSender 验证缺少 Sender 报错 func TestSendNoSender(t *testing.T) { g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com"}, client: &mockMG{}} msg := mailx.NewMessage().To("a@example.com").Subject("s").Build() err := g.Send(context.Background(), msg) if err == nil || !errors.Is(err, mailx.ErrInvalidConfig) { t.Fatalf("err = %v, want ErrInvalidConfig", err) } } // TestSendNoDomain 验证缺少 Domain/APIKey 报错 func TestSendNoDomain(t *testing.T) { g := &MailGun{cfg: Config{Sender: "s@example.com"}, client: &mockMG{}} msg := mailx.NewMessage().To("a@example.com").Subject("s").Build() err := g.Send(context.Background(), msg) if err == nil || !errors.Is(err, mailx.ErrInvalidConfig) { t.Fatalf("err = %v, want ErrInvalidConfig", err) } } // TestSendAttachmentErrors 验证附件边界情况 func TestSendAttachmentErrors(t *testing.T) { g := &MailGun{cfg: Config{APIKey: "key", Domain: "mg.example.com", Sender: "s@example.com"}, client: &mockMG{}} // 附件既无 path 也无 data msg := mailx.NewMessage().To("a@example.com").Subject("s"). AttachBytes("x.txt", []byte{1}). Build() msg.Attachments[0].Data = nil // 清空 data if err := g.Send(context.Background(), msg); err == nil { t.Fatal("attachment without path/data should error") } // 附件路径不存在 msg = mailx.NewMessage().To("a@example.com").Subject("s"). Attach("no-such-file.txt"). Build() if err := g.Send(context.Background(), msg); err == nil { t.Fatal("missing attachment file should error") } // 内嵌图片路径不存在 msg = mailx.NewMessage().To("a@example.com").Subject("s"). InlineImage("cid1", "no-such-img.png"). Build() if err := g.Send(context.Background(), msg); err == nil { t.Fatal("missing inline file should error") } } // TestGetClient 验证 getClient 惰性复用 func TestGetClient(t *testing.T) { g := New(Config{APIKey: "key", Domain: "mg.example.com"}) c1 := g.getClient() c2 := g.getClient() if c1 != c2 { t.Error("client should be reused") } }