package aws import ( "context" "errors" "testing" "time" mailx "code.yun.ink/pkg/mailx" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/ses" "github.com/aws/aws-sdk-go/service/ses/sesiface" ) // mockSES 通过嵌入 sesiface.SESAPI 实现 mock,仅覆盖 SendEmailWithContext type mockSES struct { sesiface.SESAPI sendErr error lastIn *ses.SendEmailInput lastCtx context.Context } func (m *mockSES) SendEmailWithContext(ctx aws.Context, input *ses.SendEmailInput, _ ...request.Option) (*ses.SendEmailOutput, error) { m.lastCtx = ctx m.lastIn = input if m.sendErr != nil { return nil, m.sendErr } return &ses.SendEmailOutput{MessageId: aws.String("mock-id")}, nil } // TestSendSuccess 验证完整发送流程与输入构造 func TestSendSuccess(t *testing.T) { mock := &mockSES{} a := &Aws{cfg: Config{Sender: "noreply@example.com"}, svc: 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"). Build() if err := a.Send(context.Background(), msg); err != nil { t.Fatal(err) } in := mock.lastIn if in == nil { t.Fatal("no input captured") } if *in.Source != "noreply@example.com" { t.Errorf("Source = %q", *in.Source) } if len(in.Destination.ToAddresses) != 2 || len(in.Destination.CcAddresses) != 1 || len(in.Destination.BccAddresses) != 1 { t.Errorf("dest addresses wrong: %+v", in.Destination) } if len(in.ReplyToAddresses) != 1 || *in.ReplyToAddresses[0] != "reply@example.com" { t.Errorf("ReplyTo = %+v", in.ReplyToAddresses) } if in.Message.Body.Html == nil || in.Message.Body.Text == nil { t.Errorf("body should have both html and text: %+v", in.Message.Body) } if *in.Message.Subject.Charset != "UTF-8" { t.Errorf("subject charset = %q", *in.Message.Subject.Charset) } } // TestSendFromFallback 验证 msg.From 为空时回退 cfg.Sender func TestSendFromFallback(t *testing.T) { mock := &mockSES{} a := &Aws{cfg: Config{Sender: "cfg-sender@example.com"}, svc: mock} msg := mailx.NewMessage().To("a@example.com").Subject("s").Build() if err := a.Send(context.Background(), msg); err != nil { t.Fatal(err) } if *mock.lastIn.Source != "cfg-sender@example.com" { t.Errorf("Source = %q, want cfg-sender", *mock.lastIn.Source) } } // TestSendFail 验证发送失败包装为 ErrSendFailed func TestSendFail(t *testing.T) { mock := &mockSES{sendErr: errors.New("aws down")} a := &Aws{cfg: Config{Sender: "s@example.com"}, svc: 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) } } // TestSendNoSender 验证缺少 Sender 时报错且不调用 SDK func TestSendNoSender(t *testing.T) { mock := &mockSES{} a := &Aws{cfg: Config{}, svc: mock} 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) } if mock.lastIn != nil { t.Error("SDK should not be called") } } // TestSendInlineError 验证内嵌图片返回明确错误 func TestSendInlineError(t *testing.T) { mock := &mockSES{} a := &Aws{cfg: Config{Sender: "s@example.com"}, svc: mock} 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) } } // TestAwsBody 验证 awsBody 各分支 func TestAwsBody(t *testing.T) { // 仅 HTML b := awsBody(&mailx.Message{Body: "hi"}) if b.Html == nil || b.Text != nil { t.Errorf("html only: %+v", b) } // 仅 Text b = awsBody(&mailx.Message{TextBody: "plain"}) if b.Html != nil || b.Text == nil { t.Errorf("text only: %+v", b) } // 两者都有 b = awsBody(&mailx.Message{Body: "hi", TextBody: "plain"}) if b.Html == nil || b.Text == nil { t.Errorf("both: %+v", b) } // 都没有 b = awsBody(&mailx.Message{}) if b.Html != nil || b.Text != nil { t.Errorf("neither: %+v", b) } } // TestWithTimeout 验证超时叠加逻辑 func TestWithTimeout(t *testing.T) { ctx := context.Background() // 未配置 Timeout:原样返回 a := &Aws{cfg: Config{}} c, cancel := a.withTimeout(ctx) if c != ctx { t.Error("no timeout should return original ctx") } cancel() // 配置了 Timeout:返回带 deadline 的 ctx a = &Aws{cfg: Config{Timeout: time.Second}} c, cancel = a.withTimeout(ctx) if c == ctx { t.Error("with timeout should return new ctx") } if _, ok := c.Deadline(); !ok { t.Error("new ctx should have deadline") } cancel() // ctx 已有更早 deadline:保持原 ctx early, ecancel := context.WithTimeout(ctx, time.Millisecond) defer ecancel() c, cancel = a.withTimeout(early) if c != early { t.Error("earlier deadline should keep original ctx") } cancel() }