291 lines
9.4 KiB
Go
291 lines
9.4 KiB
Go
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")
|
|
}
|
|
}
|