更新
This commit is contained in:
@@ -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 < 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user