init push
This commit is contained in:
240
internal/service/ses_sender_test.go
Normal file
240
internal/service/ses_sender_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sesv2"
|
||||
sestypes "github.com/aws/aws-sdk-go-v2/service/sesv2/types"
|
||||
"github.com/aws/smithy-go"
|
||||
smithymiddleware "github.com/aws/smithy-go/middleware"
|
||||
|
||||
"wucher/internal/config"
|
||||
"wucher/internal/queue"
|
||||
)
|
||||
|
||||
type mockSESAPI struct {
|
||||
input *sesv2.SendEmailInput
|
||||
output *sesv2.SendEmailOutput
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (m *mockSESAPI) SendEmail(_ context.Context, params *sesv2.SendEmailInput, _ ...func(*sesv2.Options)) (*sesv2.SendEmailOutput, error) {
|
||||
m.calls++
|
||||
m.input = params
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
if m.output != nil {
|
||||
return m.output, nil
|
||||
}
|
||||
return &sesv2.SendEmailOutput{}, nil
|
||||
}
|
||||
|
||||
func TestSESSenderSendSimple(t *testing.T) {
|
||||
metadata := smithymiddleware.Metadata{}
|
||||
awsmiddleware.SetRequestIDMetadata(&metadata, "req-123")
|
||||
api := &mockSESAPI{
|
||||
output: &sesv2.SendEmailOutput{
|
||||
MessageId: strPtr("ses-msg-1"),
|
||||
ResultMetadata: metadata,
|
||||
},
|
||||
}
|
||||
sender := NewSESSenderWithAPI(config.SESConfig{
|
||||
Region: "ap-southeast-1",
|
||||
From: "Wucher <no-reply@example.com>",
|
||||
ConfigurationSetName: "auth-events",
|
||||
}, api)
|
||||
|
||||
result, err := sender.Send(context.Background(), queue.EmailJob{
|
||||
MessageType: "password_reset",
|
||||
To: "user@example.com",
|
||||
CC: []string{"cc@example.com"},
|
||||
BCC: []string{"bcc@example.com"},
|
||||
ReplyTo: []string{"reply@example.com"},
|
||||
Subject: "Reset Password",
|
||||
TextBody: "plain body",
|
||||
HTMLBody: "<p>html body</p>",
|
||||
Tags: map[string]string{
|
||||
"message_type": "password_reset",
|
||||
"template": "auth_password_reset",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
if result.Provider != "ses" || result.ProviderMessageID != "ses-msg-1" || result.RequestID != "req-123" {
|
||||
t.Fatalf("unexpected result: %+v", result)
|
||||
}
|
||||
if api.calls != 1 || api.input == nil {
|
||||
t.Fatalf("expected SES SendEmail to be called once")
|
||||
}
|
||||
if got := *api.input.ConfigurationSetName; got != "auth-events" {
|
||||
t.Fatalf("unexpected configuration set: %q", got)
|
||||
}
|
||||
if got := *api.input.FromEmailAddress; got != "\"Wucher\" <no-reply@example.com>" {
|
||||
t.Fatalf("unexpected from address: %q", got)
|
||||
}
|
||||
if got := api.input.Destination.ToAddresses; len(got) != 1 || got[0] != "user@example.com" {
|
||||
t.Fatalf("unexpected to addresses: %#v", got)
|
||||
}
|
||||
if got := api.input.Destination.CcAddresses; len(got) != 1 || got[0] != "cc@example.com" {
|
||||
t.Fatalf("unexpected cc addresses: %#v", got)
|
||||
}
|
||||
if got := api.input.Destination.BccAddresses; len(got) != 1 || got[0] != "bcc@example.com" {
|
||||
t.Fatalf("unexpected bcc addresses: %#v", got)
|
||||
}
|
||||
if got := api.input.ReplyToAddresses; len(got) != 1 || got[0] != "reply@example.com" {
|
||||
t.Fatalf("unexpected reply-to addresses: %#v", got)
|
||||
}
|
||||
if api.input.Content == nil || api.input.Content.Simple == nil {
|
||||
t.Fatalf("expected simple SES content")
|
||||
}
|
||||
if got := *api.input.Content.Simple.Subject.Data; got != "Reset Password" {
|
||||
t.Fatalf("unexpected subject: %q", got)
|
||||
}
|
||||
if got := *api.input.Content.Simple.Body.Text.Data; got != "plain body" {
|
||||
t.Fatalf("unexpected text body: %q", got)
|
||||
}
|
||||
if got := *api.input.Content.Simple.Body.Html.Data; got != "<p>html body</p>" {
|
||||
t.Fatalf("unexpected html body: %q", got)
|
||||
}
|
||||
if len(api.input.EmailTags) != 2 {
|
||||
t.Fatalf("expected tags to be forwarded, got %#v", api.input.EmailTags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSESSenderSendTemplate(t *testing.T) {
|
||||
api := &mockSESAPI{}
|
||||
sender := NewSESSenderWithAPI(config.SESConfig{
|
||||
Region: "ap-southeast-1",
|
||||
From: "no-reply@example.com",
|
||||
}, api)
|
||||
|
||||
_, err := sender.Send(context.Background(), queue.EmailJob{
|
||||
MessageType: "invite_set_password",
|
||||
To: "user@example.com",
|
||||
TemplateName: "auth.invite_set_password",
|
||||
TemplateData: map[string]any{"name": "John"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
if api.input == nil || api.input.Content == nil || api.input.Content.Template == nil {
|
||||
t.Fatalf("expected templated SES content")
|
||||
}
|
||||
if got := *api.input.Content.Template.TemplateName; got != "auth.invite_set_password" {
|
||||
t.Fatalf("unexpected template name: %q", got)
|
||||
}
|
||||
if got := *api.input.Content.Template.TemplateData; got != "{\"name\":\"John\"}" {
|
||||
t.Fatalf("unexpected template data: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSESSenderSendInlineAttachmentUsesRawMIME(t *testing.T) {
|
||||
api := &mockSESAPI{}
|
||||
sender := NewSESSenderWithAPI(config.SESConfig{
|
||||
Region: "ap-southeast-1",
|
||||
From: "no-reply@example.com",
|
||||
}, api)
|
||||
|
||||
_, err := sender.Send(context.Background(), queue.EmailJob{
|
||||
MessageType: "security_pin_reset",
|
||||
To: "user@example.com",
|
||||
Subject: "Reset Security PIN",
|
||||
TextBody: "plain body",
|
||||
HTMLBody: `<img src="cid:branding-email-logo"><p>html body</p>`,
|
||||
Attachments: []queue.EmailAttachment{{
|
||||
Filename: "email-logo.png",
|
||||
ContentType: "image/png",
|
||||
ContentID: "branding-email-logo",
|
||||
Content: []byte("png"),
|
||||
Inline: true,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
if api.input == nil || api.input.Content == nil || api.input.Content.Raw == nil {
|
||||
t.Fatalf("expected raw SES content")
|
||||
}
|
||||
|
||||
raw := string(api.input.Content.Raw.Data)
|
||||
if !strings.Contains(raw, `Content-ID: <branding-email-logo>`) {
|
||||
t.Fatalf("expected inline content id in raw MIME: %q", raw)
|
||||
}
|
||||
if !strings.Contains(raw, `Content-Disposition: inline; filename="email-logo.png"`) {
|
||||
t.Fatalf("expected inline content disposition in raw MIME: %q", raw)
|
||||
}
|
||||
if !strings.Contains(raw, `cid:branding-email-logo`) {
|
||||
t.Fatalf("expected html body cid reference in raw MIME: %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSESSenderSendValidationError(t *testing.T) {
|
||||
sender := NewSESSenderWithAPI(config.SESConfig{
|
||||
Region: "ap-southeast-1",
|
||||
From: "no-reply@example.com",
|
||||
}, &mockSESAPI{})
|
||||
|
||||
if _, err := sender.Send(context.Background(), queue.EmailJob{
|
||||
To: "not-an-email",
|
||||
Subject: "Hello",
|
||||
TextBody: "Body",
|
||||
}); err == nil {
|
||||
t.Fatalf("expected invalid recipient error")
|
||||
}
|
||||
|
||||
invalidCfgSender := NewSESSenderWithAPI(config.SESConfig{}, &mockSESAPI{})
|
||||
if _, err := invalidCfgSender.Send(context.Background(), queue.EmailJob{
|
||||
To: "user@example.com",
|
||||
Subject: "Hello",
|
||||
TextBody: "Body",
|
||||
}); err == nil {
|
||||
t.Fatalf("expected missing SES config error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyDeliveryError(t *testing.T) {
|
||||
t.Run("throttling is transient", func(t *testing.T) {
|
||||
err := classifyDeliveryError(sesSenderAPIError{code: "TooManyRequestsException", fault: smithy.FaultClient})
|
||||
if !queue.IsTransient(err) {
|
||||
t.Fatalf("expected transient classification, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("message rejected is permanent", func(t *testing.T) {
|
||||
err := classifyDeliveryError(sesSenderAPIError{code: "MessageRejected", fault: smithy.FaultClient})
|
||||
if !queue.IsPermanent(err) {
|
||||
t.Fatalf("expected permanent classification, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validation error is permanent", func(t *testing.T) {
|
||||
err := classifyDeliveryError(errors.New("invalid email job: recipient is required"))
|
||||
if !queue.IsPermanent(err) {
|
||||
t.Fatalf("expected permanent classification, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type sesSenderAPIError struct {
|
||||
code string
|
||||
fault smithy.ErrorFault
|
||||
}
|
||||
|
||||
func (e sesSenderAPIError) Error() string { return e.code }
|
||||
func (e sesSenderAPIError) ErrorCode() string { return e.code }
|
||||
func (e sesSenderAPIError) ErrorMessage() string { return e.code }
|
||||
func (e sesSenderAPIError) ErrorFault() smithy.ErrorFault { return e.fault }
|
||||
|
||||
func strPtr(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
var _ sesAPI = (*mockSESAPI)(nil)
|
||||
var _ = sestypes.MessageTag{}
|
||||
Reference in New Issue
Block a user