This commit is contained in:
yun
2026-08-15 01:38:05 +08:00
parent 139330aee2
commit eb8f660ab5
57 changed files with 5401 additions and 1438 deletions
+290
View File
@@ -0,0 +1,290 @@
package aliyun
import (
"context"
"errors"
"testing"
"time"
mailx "code.yun.ink/pkg/mailx"
dm20151123 "github.com/alibabacloud-go/dm-20151123/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
// mockDM 实现 dmMailer 接口,记录请求并可按需返回错误
type mockDM struct {
sendErr error
statsErr error
lastSend *dm20151123.SingleSendMailRequest
lastStats *dm20151123.SenderStatisticsDetailByParamRequest
// statsPages 模拟分页返回;nil 时返回单个空页
statsPages []*dm20151123.SenderStatisticsDetailByParamResponse
}
func (m *mockDM) SingleSendMailWithOptions(req *dm20151123.SingleSendMailRequest, _ *util.RuntimeOptions) (*dm20151123.SingleSendMailResponse, error) {
m.lastSend = req
if m.sendErr != nil {
return nil, m.sendErr
}
return &dm20151123.SingleSendMailResponse{}, nil
}
func (m *mockDM) SenderStatisticsDetailByParamWithOptions(req *dm20151123.SenderStatisticsDetailByParamRequest, _ *util.RuntimeOptions) (*dm20151123.SenderStatisticsDetailByParamResponse, error) {
m.lastStats = req
if m.statsErr != nil {
return nil, m.statsErr
}
if len(m.statsPages) > 0 {
resp := m.statsPages[0]
m.statsPages = m.statsPages[1:]
return resp, nil
}
return &dm20151123.SenderStatisticsDetailByParamResponse{Body: &dm20151123.SenderStatisticsDetailByParamResponseBody{}}, nil
}
// TestSendSuccess 验证完整发送流程与请求构造
func TestSendSuccess(t *testing.T) {
mock := &mockDM{}
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com", ReplyAddress: "r@example.com"}, client: mock}
msg := mailx.NewMessage().
To("a@example.com", "b@example.com").
Subject("hi").
HTML("<b>html</b>").
Build()
if err := a.Send(context.Background(), msg); err != nil {
t.Fatal(err)
}
req := mock.lastSend
if req == nil {
t.Fatal("no request captured")
}
if tea.StringValue(req.AccountName) != "noreply@example.com" {
t.Errorf("AccountName = %q", tea.StringValue(req.AccountName))
}
if tea.StringValue(req.ToAddress) != "a@example.com,b@example.com" {
t.Errorf("ToAddress = %q", tea.StringValue(req.ToAddress))
}
if tea.StringValue(req.Subject) != "hi" {
t.Errorf("Subject = %q", tea.StringValue(req.Subject))
}
if tea.StringValue(req.HtmlBody) != "<b>html</b>" {
t.Errorf("HtmlBody = %q", tea.StringValue(req.HtmlBody))
}
if tea.BoolValue(req.ReplyToAddress) {
t.Errorf("ReplyToAddress = %v, want false (uses custom ReplyAddress)", tea.BoolValue(req.ReplyToAddress))
}
if tea.StringValue(req.ReplyAddress) != "r@example.com" {
t.Errorf("ReplyAddress = %q", tea.StringValue(req.ReplyAddress))
}
}
// TestSendTextOnly 验证纯文本正文被转义为 HTML
func TestSendTextOnly(t *testing.T) {
mock := &mockDM{}
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com"}, client: mock}
msg := mailx.NewMessage().To("a@example.com").Subject("s").Text("a < b & c").Build()
if err := a.Send(context.Background(), msg); err != nil {
t.Fatal(err)
}
got := tea.StringValue(mock.lastSend.HtmlBody)
if got != "a &lt; b &amp; c" {
t.Errorf("escaped HtmlBody = %q", got)
}
}
// TestSendMsgReplyTo 验证 msg.ReplyTo 优先于 cfg.ReplyAddress
func TestSendMsgReplyTo(t *testing.T) {
mock := &mockDM{}
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com", ReplyAddress: "cfg@example.com"}, client: mock}
msg := mailx.NewMessage().To("a@example.com").Subject("s").ReplyTo("msg@example.com").Build()
if err := a.Send(context.Background(), msg); err != nil {
t.Fatal(err)
}
if tea.StringValue(mock.lastSend.ReplyAddress) != "msg@example.com" {
t.Errorf("ReplyAddress = %q, want msg@example.com", tea.StringValue(mock.lastSend.ReplyAddress))
}
}
// TestSendNoReplyTo 验证无回复地址时 ReplyToAddress=true
func TestSendNoReplyTo(t *testing.T) {
mock := &mockDM{}
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com"}, client: mock}
msg := mailx.NewMessage().To("a@example.com").Subject("s").Build()
if err := a.Send(context.Background(), msg); err != nil {
t.Fatal(err)
}
if !tea.BoolValue(mock.lastSend.ReplyToAddress) {
t.Errorf("ReplyToAddress = %v, want true", tea.BoolValue(mock.lastSend.ReplyToAddress))
}
}
// TestSendTooManyRecipients 验证超过 100 收件人报错
func TestSendTooManyRecipients(t *testing.T) {
mock := &mockDM{}
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com"}, client: mock}
to := make([]string, 101)
for i := range to {
to[i] = "u@example.com"
}
msg := mailx.NewMessage().To(to...).Subject("s").Build()
err := a.Send(context.Background(), msg)
if err == nil || !errors.Is(err, mailx.ErrInvalidMessage) {
t.Fatalf("err = %v, want ErrInvalidMessage", err)
}
}
// TestSendNoAccount 验证缺少 AccountName 报错
func TestSendNoAccount(t *testing.T) {
a := &Aliyun{cfg: Config{}, client: &mockDM{}}
msg := mailx.NewMessage().To("a@example.com").Subject("s").Build()
err := a.Send(context.Background(), msg)
if err == nil || !errors.Is(err, mailx.ErrInvalidConfig) {
t.Fatalf("err = %v, want ErrInvalidConfig", err)
}
}
// TestSendInlineError 验证内嵌图片报错
func TestSendInlineError(t *testing.T) {
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com"}, client: &mockDM{}}
msg := mailx.NewMessage().To("a@example.com").Subject("s").
InlineImageBytes("cid1", "a.png", []byte{1}).
Build()
err := a.Send(context.Background(), msg)
if err == nil || !errors.Is(err, mailx.ErrInvalidConfig) {
t.Fatalf("err = %v, want ErrInvalidConfig", err)
}
}
// TestSendFail 验证发送失败包装为 ErrSendFailed
func TestSendFail(t *testing.T) {
mock := &mockDM{sendErr: errors.New("aliyun down")}
a := &Aliyun{cfg: Config{AccountName: "noreply@example.com"}, client: mock}
msg := mailx.NewMessage().To("a@example.com").Subject("s").Build()
err := a.Send(context.Background(), msg)
if err == nil || !errors.Is(err, mailx.ErrSendFailed) {
t.Fatalf("err = %v, want ErrSendFailed", err)
}
}
// TestRuntimeOptions 验证超时计算
func TestRuntimeOptions(t *testing.T) {
// 默认超时 30s
a := &Aliyun{cfg: Config{}}
ro := a.runtimeOptions(context.Background())
if ro == nil {
t.Fatal("runtime options nil")
}
// 配置超时 1s
a = &Aliyun{cfg: Config{Timeout: time.Second}}
ro = a.runtimeOptions(context.Background())
// ctx 已有更早 deadline
early, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
ro = a.runtimeOptions(early)
_ = ro // 只要不 panic 即可
}
// TestSyncStatusSuccess 验证状态同步记录映射
func TestSyncStatusSuccess(t *testing.T) {
mock := &mockDM{
statsPages: []*dm20151123.SenderStatisticsDetailByParamResponse{
{
Body: &dm20151123.SenderStatisticsDetailByParamResponseBody{
Data: &dm20151123.SenderStatisticsDetailByParamResponseBodyData{
MailDetail: []*dm20151123.SenderStatisticsDetailByParamResponseBodyDataMailDetail{
{
AccountName: tea.String("noreply@example.com"),
ToAddress: tea.String("a@example.com"),
Subject: tea.String("hi"),
Status: tea.Int32(0),
Message: tea.String(""),
LastUpdateTime: tea.String("2026-08-15T10:00Z"),
},
{
AccountName: tea.String("noreply@example.com"),
ToAddress: tea.String("b@example.com"),
Subject: tea.String("bye"),
Status: tea.Int32(4),
Message: tea.String("rejected"),
LastUpdateTime: tea.String("2026-08-15T11:00Z"),
},
},
},
NextStart: tea.String("page2"),
},
},
{
Body: &dm20151123.SenderStatisticsDetailByParamResponseBody{
Data: &dm20151123.SenderStatisticsDetailByParamResponseBodyData{
MailDetail: []*dm20151123.SenderStatisticsDetailByParamResponseBodyDataMailDetail{
{
ToAddress: tea.String("c@example.com"),
Subject: tea.String("page2 mail"),
Status: tea.Int32(2),
},
},
},
},
},
},
}
a := &Aliyun{cfg: Config{}, client: mock}
records, err := a.SyncStatus(context.Background())
if err != nil {
t.Fatal(err)
}
if len(records) != 3 {
t.Fatalf("records len = %d, want 3 (pagination)", len(records))
}
if records[0].Status != mailx.EmailSendStatusSuccess {
t.Errorf("records[0].Status = %v, want Success", records[0].Status)
}
if records[1].Status != mailx.EmailSendStatusFailed {
t.Errorf("records[1].Status = %v, want Failed", records[1].Status)
}
if records[1].ErrorMessage != "rejected" {
t.Errorf("records[1].ErrorMessage = %q", records[1].ErrorMessage)
}
if records[2].Status != mailx.EmailSendStatusInvalidAddress {
t.Errorf("records[2].Status = %v, want InvalidAddress", records[2].Status)
}
// 验证第二页请求带 NextStart
if mock.lastStats == nil || tea.StringValue(mock.lastStats.NextStart) != "page2" {
t.Errorf("second page NextStart = %v", mock.lastStats)
}
}
// TestSyncStatusFail 验证状态同步失败包装错误
func TestSyncStatusFail(t *testing.T) {
mock := &mockDM{statsErr: errors.New("api error")}
a := &Aliyun{cfg: Config{}, client: mock}
if _, err := a.SyncStatus(context.Background()); err == nil {
t.Fatal("SyncStatus should error")
}
}
// TestNewClientReuse 验证 client 惰性复用(只初始化一次)
func TestNewClientReuse(t *testing.T) {
a := New(Config{AccessKeyID: "ak", AccessKeySecret: "sk", AccountName: "n@e.com"})
c1, err := a.newClient()
if err != nil {
t.Fatal(err)
}
c2, err := a.newClient()
if err != nil {
t.Fatal(err)
}
if c1 != c2 {
t.Error("client should be reused")
}
}