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("html"). 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) != "html" { 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 < b & 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") } }