Merge pull request #1165 from trheyi/main
Refactor SMTP provider implementation for improved clarity and functionality
This commit is contained in:
commit
ccac2e0196
3 changed files with 720 additions and 24 deletions
|
|
@ -436,7 +436,8 @@ func TestSend_WithScheduledTime(t *testing.T) {
|
|||
// Verify scheduled time is set
|
||||
deliveryTime := r.FormValue("o:deliverytime")
|
||||
assert.NotEmpty(t, deliveryTime)
|
||||
assert.Contains(t, deliveryTime, "GMT") // RFC1123Z format includes GMT
|
||||
// RFC1123Z format includes timezone offset (e.g., "+0000", "+0800")
|
||||
assert.Regexp(t, `[+-]\d{4}`, deliveryTime)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"message": "Queued"})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -13,8 +14,8 @@ import (
|
|||
"github.com/yaoapp/yao/messenger/types"
|
||||
)
|
||||
|
||||
// SMTPProvider implements the Provider interface for SMTP email sending
|
||||
type SMTPProvider struct {
|
||||
// Provider implements the Provider interface for SMTP email sending
|
||||
type Provider struct {
|
||||
config types.ProviderConfig
|
||||
host string
|
||||
port int
|
||||
|
|
@ -26,8 +27,8 @@ type SMTPProvider struct {
|
|||
}
|
||||
|
||||
// NewSMTPProvider creates a new SMTP provider
|
||||
func NewSMTPProvider(config types.ProviderConfig) (*SMTPProvider, error) {
|
||||
provider := &SMTPProvider{
|
||||
func NewSMTPProvider(config types.ProviderConfig) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
config: config,
|
||||
useTLS: true, // Default to TLS
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@ func NewSMTPProvider(config types.ProviderConfig) (*SMTPProvider, error) {
|
|||
}
|
||||
|
||||
// Send sends a message using SMTP
|
||||
func (p *SMTPProvider) Send(ctx context.Context, message *types.Message) error {
|
||||
func (p *Provider) Send(ctx context.Context, message *types.Message) error {
|
||||
if message.Type != types.MessageTypeEmail {
|
||||
return fmt.Errorf("SMTP provider only supports email messages")
|
||||
}
|
||||
|
|
@ -111,7 +112,7 @@ func (p *SMTPProvider) Send(ctx context.Context, message *types.Message) error {
|
|||
}
|
||||
|
||||
// SendBatch sends multiple messages in batch
|
||||
func (p *SMTPProvider) SendBatch(ctx context.Context, messages []*types.Message) error {
|
||||
func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) error {
|
||||
for _, message := range messages {
|
||||
if err := p.Send(ctx, message); err != nil {
|
||||
return fmt.Errorf("failed to send message to %v: %w", message.To, err)
|
||||
|
|
@ -121,17 +122,17 @@ func (p *SMTPProvider) SendBatch(ctx context.Context, messages []*types.Message)
|
|||
}
|
||||
|
||||
// GetType returns the provider type
|
||||
func (p *SMTPProvider) GetType() string {
|
||||
func (p *Provider) GetType() string {
|
||||
return "smtp"
|
||||
}
|
||||
|
||||
// GetName returns the provider name
|
||||
func (p *SMTPProvider) GetName() string {
|
||||
func (p *Provider) GetName() string {
|
||||
return p.config.Name
|
||||
}
|
||||
|
||||
// Validate validates the provider configuration
|
||||
func (p *SMTPProvider) Validate() error {
|
||||
func (p *Provider) Validate() error {
|
||||
if p.host == "" {
|
||||
return fmt.Errorf("host is required")
|
||||
}
|
||||
|
|
@ -151,12 +152,12 @@ func (p *SMTPProvider) Validate() error {
|
|||
}
|
||||
|
||||
// Close closes the provider connection (no-op for SMTP)
|
||||
func (p *SMTPProvider) Close() error {
|
||||
func (p *Provider) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMessage builds the email message content
|
||||
func (p *SMTPProvider) buildMessage(message *types.Message) (string, error) {
|
||||
func (p *Provider) buildMessage(message *types.Message) (string, error) {
|
||||
var content strings.Builder
|
||||
|
||||
// From header
|
||||
|
|
@ -211,8 +212,21 @@ func (p *SMTPProvider) buildMessage(message *types.Message) (string, error) {
|
|||
return content.String(), nil
|
||||
}
|
||||
|
||||
// extractEmailAddress extracts the email address from a string that may contain display name
|
||||
// e.g., "John Doe <john@example.com>" -> "john@example.com"
|
||||
func extractEmailAddress(address string) string {
|
||||
// Regular expression to match email addresses in angle brackets
|
||||
re := regexp.MustCompile(`<([^>]+)>`)
|
||||
matches := re.FindStringSubmatch(address)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
// If no angle brackets, assume the whole string is the email address
|
||||
return strings.TrimSpace(address)
|
||||
}
|
||||
|
||||
// sendEmail sends the email using SMTP
|
||||
func (p *SMTPProvider) sendEmail(ctx context.Context, to []string, content string) error {
|
||||
func (p *Provider) sendEmail(ctx context.Context, to []string, content string) error {
|
||||
addr := fmt.Sprintf("%s:%d", p.host, p.port)
|
||||
|
||||
// Create auth
|
||||
|
|
@ -222,14 +236,13 @@ func (p *SMTPProvider) sendEmail(ctx context.Context, to []string, content strin
|
|||
if p.useSSL {
|
||||
// Use SSL/TLS connection
|
||||
return p.sendWithTLS(ctx, addr, auth, to, content)
|
||||
} else {
|
||||
// Use standard SMTP with STARTTLS and context support
|
||||
return p.sendWithContext(ctx, addr, auth, to, content)
|
||||
}
|
||||
// Use standard SMTP with STARTTLS and context support
|
||||
return p.sendWithContext(ctx, addr, auth, to, content)
|
||||
}
|
||||
|
||||
// sendWithContext sends email using standard SMTP with context support
|
||||
func (p *SMTPProvider) sendWithContext(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error {
|
||||
func (p *Provider) sendWithContext(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error {
|
||||
// Create a dialer with timeout from context
|
||||
d := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
|
|
@ -264,8 +277,9 @@ func (p *SMTPProvider) sendWithContext(ctx context.Context, addr string, auth sm
|
|||
return fmt.Errorf("SMTP authentication failed: %w", err)
|
||||
}
|
||||
|
||||
// Set sender
|
||||
if err = client.Mail(p.from); err != nil {
|
||||
// Set sender (extract pure email address from potentially formatted from address)
|
||||
fromEmail := extractEmailAddress(p.from)
|
||||
if err = client.Mail(fromEmail); err != nil {
|
||||
return fmt.Errorf("failed to set sender: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -291,19 +305,31 @@ func (p *SMTPProvider) sendWithContext(ctx context.Context, addr string, auth sm
|
|||
}
|
||||
|
||||
// sendWithTLS sends email with explicit TLS connection
|
||||
func (p *SMTPProvider) sendWithTLS(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error {
|
||||
// Create TLS connection
|
||||
func (p *Provider) sendWithTLS(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error {
|
||||
// Create TLS connection with context support
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: p.host,
|
||||
InsecureSkipVerify: false,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
// Use dialer with context for TLS connection
|
||||
d := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(d, "tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TLS connection: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Check if context is cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("connection cancelled: %w", ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
// Create SMTP client
|
||||
client, err := smtp.NewClient(conn, p.host)
|
||||
if err != nil {
|
||||
|
|
@ -318,8 +344,9 @@ func (p *SMTPProvider) sendWithTLS(ctx context.Context, addr string, auth smtp.A
|
|||
}
|
||||
}
|
||||
|
||||
// Set sender
|
||||
if err := client.Mail(p.from); err != nil {
|
||||
// Set sender (extract pure email address from potentially formatted from address)
|
||||
fromEmail := extractEmailAddress(p.from)
|
||||
if err := client.Mail(fromEmail); err != nil {
|
||||
return fmt.Errorf("failed to set sender: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
668
messenger/providers/smtp/smtp_test.go
Normal file
668
messenger/providers/smtp/smtp_test.go
Normal file
|
|
@ -0,0 +1,668 @@
|
|||
package smtp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
// Test constants for authorized recipient addresses
|
||||
const (
|
||||
TestEmailAgent = "agent@iqka.com"
|
||||
TestEmailX = "x@iqka.com"
|
||||
TestEmailXiang = "xiang@iqka.com"
|
||||
)
|
||||
|
||||
// Test helper functions
|
||||
|
||||
func createTestMessage(msgType types.MessageType) *types.Message {
|
||||
message := &types.Message{
|
||||
Type: msgType,
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Email",
|
||||
Body: "This is a test email body",
|
||||
HTML: "<h1>Test Email</h1><p>This is a test email body</p>",
|
||||
Headers: map[string]string{
|
||||
"X-Test-Header": "test-value",
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"campaign": "test-campaign",
|
||||
"user_id": "12345",
|
||||
},
|
||||
Priority: 1,
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func loadPrimaryTestConfig(t *testing.T) types.ProviderConfig {
|
||||
// Prepare test environment using YAO_TEST_APPLICATION which points to yao-dev-app
|
||||
// Environment variables are already set in env.local.sh
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create test config directly using environment variables for primary SMTP
|
||||
// Port 465 requires SSL, port 587 requires TLS
|
||||
smtpPort := os.Getenv("SMTP_PORT")
|
||||
useSSL := smtpPort == "465"
|
||||
useTLS := smtpPort == "587" || smtpPort == "25"
|
||||
|
||||
config := types.ProviderConfig{
|
||||
Name: "primary",
|
||||
Connector: "smtp",
|
||||
Options: map[string]interface{}{
|
||||
"host": os.Getenv("SMTP_HOST"),
|
||||
"port": os.Getenv("SMTP_PORT"),
|
||||
"username": os.Getenv("SMTP_USERNAME"),
|
||||
"password": os.Getenv("SMTP_PASSWORD"),
|
||||
"from": os.Getenv("SMTP_FROM"),
|
||||
"use_tls": useTLS,
|
||||
"use_ssl": useSSL,
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func loadReliableTestConfig(t *testing.T) types.ProviderConfig {
|
||||
// Prepare test environment using YAO_TEST_APPLICATION which points to yao-dev-app
|
||||
// Environment variables are already set in env.local.sh
|
||||
test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION")
|
||||
defer test.Clean()
|
||||
|
||||
// Create test config directly using environment variables for reliable SMTP
|
||||
config := types.ProviderConfig{
|
||||
Name: "reliable",
|
||||
Connector: "smtp",
|
||||
Options: map[string]interface{}{
|
||||
"host": os.Getenv("RELIABLE_SMTP_HOST"),
|
||||
"port": 587, // Hardcoded in reliable.smtp.yao
|
||||
"username": os.Getenv("RELIABLE_SMTP_USERNAME"),
|
||||
"password": os.Getenv("RELIABLE_SMTP_PASSWORD"),
|
||||
"from": os.Getenv("RELIABLE_SMTP_FROM"),
|
||||
"use_tls": true,
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Test NewSMTPProvider
|
||||
|
||||
func TestNewSMTPProvider_Primary(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
|
||||
// Verify configuration using actual environment variables from env.local.sh
|
||||
assert.Equal(t, os.Getenv("SMTP_HOST"), provider.host)
|
||||
assert.Equal(t, os.Getenv("SMTP_USERNAME"), provider.username)
|
||||
assert.Equal(t, os.Getenv("SMTP_PASSWORD"), provider.password)
|
||||
assert.Equal(t, os.Getenv("SMTP_FROM"), provider.from)
|
||||
assert.Equal(t, "primary", provider.config.Name)
|
||||
// Port 465 uses SSL, not TLS
|
||||
if os.Getenv("SMTP_PORT") == "465" {
|
||||
assert.True(t, provider.useSSL)
|
||||
assert.False(t, provider.useTLS)
|
||||
} else {
|
||||
assert.True(t, provider.useTLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_Reliable(t *testing.T) {
|
||||
config := loadReliableTestConfig(t)
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
|
||||
// Verify configuration using actual environment variables from env.local.sh
|
||||
assert.Equal(t, os.Getenv("RELIABLE_SMTP_HOST"), provider.host)
|
||||
assert.Equal(t, 587, provider.port)
|
||||
assert.Equal(t, os.Getenv("RELIABLE_SMTP_USERNAME"), provider.username)
|
||||
assert.Equal(t, os.Getenv("RELIABLE_SMTP_PASSWORD"), provider.password)
|
||||
assert.Equal(t, os.Getenv("RELIABLE_SMTP_FROM"), provider.from)
|
||||
assert.Equal(t, "reliable", provider.config.Name)
|
||||
assert.True(t, provider.useTLS)
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingOptions(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
Options: nil,
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires options")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingHost(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
Options: map[string]interface{}{
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'host' option")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingUsername(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
Options: map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"password": "password",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'username' option")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingPassword(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
Options: map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"from": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'password' option")
|
||||
}
|
||||
|
||||
func TestNewSMTPProvider_MissingFrom(t *testing.T) {
|
||||
config := types.ProviderConfig{
|
||||
Name: "test",
|
||||
Connector: "smtp",
|
||||
Options: map[string]interface{}{
|
||||
"host": "smtp.example.com",
|
||||
"port": 587,
|
||||
"username": "test@example.com",
|
||||
"password": "password",
|
||||
},
|
||||
}
|
||||
|
||||
provider, err := NewSMTPProvider(config)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, provider)
|
||||
assert.Contains(t, err.Error(), "SMTP provider requires 'from' option")
|
||||
}
|
||||
|
||||
// Test Provider Interface Methods
|
||||
|
||||
func TestGetType(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "smtp", provider.GetType())
|
||||
}
|
||||
|
||||
func TestGetName(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "primary", provider.GetName())
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Validate()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidate_MissingHost(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.host = ""
|
||||
err = provider.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "host is required")
|
||||
}
|
||||
|
||||
func TestValidate_InvalidPort(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.port = 0
|
||||
err = provider.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "port must be positive")
|
||||
}
|
||||
|
||||
func TestValidate_MissingUsername(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.username = ""
|
||||
err = provider.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "username is required")
|
||||
}
|
||||
|
||||
func TestValidate_MissingPassword(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.password = ""
|
||||
err = provider.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "password is required")
|
||||
}
|
||||
|
||||
func TestValidate_MissingFrom(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider.from = ""
|
||||
err = provider.Validate()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "from address is required")
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = provider.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test Send Methods
|
||||
|
||||
func TestSend_NonEmailMessage(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
smsMessage := createTestMessage(types.MessageTypeSMS)
|
||||
|
||||
err = provider.Send(ctx, smsMessage)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "SMTP provider only supports email messages")
|
||||
}
|
||||
|
||||
func TestSend_EmailMessage_RealAPI_Primary(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP operations
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
// Use test recipient addresses that are authorized for testing
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailAgent},
|
||||
Subject: "SMTP Unit Test Email - " + time.Now().Format("2006-01-02 15:04:05"),
|
||||
Body: "This is a unit test email sent via real SMTP server",
|
||||
HTML: "<h1>SMTP Unit Test</h1><p>This is a unit test email sent via real SMTP server</p>",
|
||||
Headers: map[string]string{
|
||||
"X-Test-Run": "smtp-provider-test",
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"test_type": "unit_test",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
// Log error but don't fail test, as it might be network or SMTP configuration issues
|
||||
t.Logf("Real SMTP API call failed (this may be expected in CI/test environment): %v", err)
|
||||
|
||||
// Check if it's expected error type (network, authentication, etc.)
|
||||
if strings.Contains(err.Error(), "SMTP authentication failed") {
|
||||
t.Log("SMTP authentication failed - this indicates the request reached the server")
|
||||
} else if strings.Contains(err.Error(), "failed to connect to SMTP server") {
|
||||
t.Log("Network error - this may be expected in test environment")
|
||||
} else {
|
||||
t.Logf("Unexpected error type: %v", err)
|
||||
}
|
||||
} else {
|
||||
t.Log("Real SMTP API call succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_EmailMessage_RealAPI_Reliable(t *testing.T) {
|
||||
config := loadReliableTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP operations
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
// Use test recipient addresses that are authorized for testing
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailX},
|
||||
Subject: "Reliable SMTP Unit Test - " + time.Now().Format("2006-01-02 15:04:05"),
|
||||
Body: "This is a unit test email sent via reliable SMTP server",
|
||||
HTML: "<h1>Reliable SMTP Test</h1><p>This is a unit test email sent via reliable SMTP server</p>",
|
||||
Headers: map[string]string{
|
||||
"X-Test-Run": "smtp-reliable-test",
|
||||
"X-Test-Type": "reliable-smtp",
|
||||
},
|
||||
Metadata: map[string]interface{}{
|
||||
"test_type": "reliable_test",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
// Log error but don't fail test, as it might be network or SMTP configuration issues
|
||||
t.Logf("Real reliable SMTP API call failed (this may be expected in CI/test environment): %v", err)
|
||||
|
||||
// Check if it's expected error type (network, authentication, etc.)
|
||||
if strings.Contains(err.Error(), "SMTP authentication failed") {
|
||||
t.Log("Reliable SMTP authentication failed - this indicates the request reached the server")
|
||||
} else if strings.Contains(err.Error(), "failed to connect to SMTP server") {
|
||||
t.Log("Network error - this may be expected in test environment")
|
||||
} else {
|
||||
t.Logf("Unexpected error type: %v", err)
|
||||
}
|
||||
} else {
|
||||
t.Log("Real reliable SMTP API call succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_ContextTimeout_RealAPI(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a very short timeout context to test timeout functionality
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailX},
|
||||
Subject: "SMTP Context Timeout Test",
|
||||
Body: "This should timeout before sending",
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify it's a context timeout error
|
||||
if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
t.Log("Context timeout working correctly with real SMTP API")
|
||||
} else if strings.Contains(err.Error(), "context canceled") {
|
||||
t.Log("Context cancellation working correctly with real SMTP API")
|
||||
} else {
|
||||
t.Logf("Got different error (may be network related): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendBatch_RealAPI(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP batch operations
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
// Create multiple test emails using authorized test addresses
|
||||
messages := []*types.Message{
|
||||
{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailX},
|
||||
Subject: "SMTP Batch Test 1 - " + time.Now().Format("15:04:05"),
|
||||
Body: "SMTP batch test message 1",
|
||||
HTML: "<p>SMTP batch test message 1</p>",
|
||||
},
|
||||
{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailXiang},
|
||||
Subject: "SMTP Batch Test 2 - " + time.Now().Format("15:04:05"),
|
||||
Body: "SMTP batch test message 2",
|
||||
HTML: "<p>SMTP batch test message 2</p>",
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.SendBatch(ctx, messages)
|
||||
if err != nil {
|
||||
t.Logf("Real SMTP batch API call failed (this may be expected): %v", err)
|
||||
|
||||
// Verify error handling logic
|
||||
if strings.Contains(err.Error(), "failed to send message to") {
|
||||
t.Log("SMTP batch sending failed as expected - error handling works correctly")
|
||||
}
|
||||
} else {
|
||||
t.Log("Real SMTP batch API call succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_MultipleRecipients_RealAPI(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use context with reasonable timeout for SMTP operations
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
// Test with multiple authorized recipient addresses
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailAgent, TestEmailX, TestEmailXiang},
|
||||
Subject: "SMTP Multiple Recipients Test - " + time.Now().Format("15:04:05"),
|
||||
Body: "This email is sent to multiple recipients for SMTP testing",
|
||||
HTML: "<h1>SMTP Multiple Recipients Test</h1><p>This email is sent to multiple recipients for SMTP testing</p>",
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "smtp-multiple-recipients",
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
t.Logf("SMTP multiple recipients API call failed (this may be expected): %v", err)
|
||||
|
||||
// Check error handling for multiple recipients
|
||||
if strings.Contains(err.Error(), "SMTP authentication failed") {
|
||||
t.Log("SMTP multiple recipients test reached SMTP server")
|
||||
}
|
||||
} else {
|
||||
t.Log("SMTP multiple recipients API call succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
// Test Edge Cases
|
||||
|
||||
func TestSend_WithCustomFrom(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailAgent},
|
||||
Subject: "SMTP Custom From Test - " + time.Now().Format("15:04:05"),
|
||||
Body: "This email tests custom from address",
|
||||
From: "custom-sender@example.com", // Custom from address
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "custom-from",
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
t.Logf("SMTP custom from test failed (this may be expected): %v", err)
|
||||
} else {
|
||||
t.Log("SMTP custom from test succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_PlainTextOnly(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailAgent},
|
||||
Subject: "SMTP Plain Text Test - " + time.Now().Format("15:04:05"),
|
||||
Body: "This is a plain text only email for testing SMTP functionality",
|
||||
// No HTML content
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "plain-text-only",
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
t.Logf("SMTP plain text test failed (this may be expected): %v", err)
|
||||
} else {
|
||||
t.Log("SMTP plain text test succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_HTMLOnly(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailAgent},
|
||||
Subject: "SMTP HTML Only Test - " + time.Now().Format("15:04:05"),
|
||||
HTML: "<h1>HTML Only Email</h1><p>This is an HTML only email for testing SMTP functionality</p><p><strong>Bold text</strong> and <em>italic text</em></p>",
|
||||
// No plain text body
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "html-only",
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
t.Logf("SMTP HTML only test failed (this may be expected): %v", err)
|
||||
} else {
|
||||
t.Log("SMTP HTML only test succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_MultipartMessage(t *testing.T) {
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
emailMessage := &types.Message{
|
||||
Type: types.MessageTypeEmail,
|
||||
To: []string{TestEmailAgent},
|
||||
Subject: "SMTP Multipart Test - " + time.Now().Format("15:04:05"),
|
||||
Body: "This is the plain text version of a multipart email for testing SMTP functionality",
|
||||
HTML: "<h1>Multipart Email</h1><p>This is the HTML version of a multipart email for testing SMTP functionality</p><p>Both plain text and HTML versions are included.</p>",
|
||||
Headers: map[string]string{
|
||||
"X-Test-Type": "multipart-message",
|
||||
},
|
||||
}
|
||||
|
||||
err = provider.Send(ctx, emailMessage)
|
||||
if err != nil {
|
||||
t.Logf("SMTP multipart test failed (this may be expected): %v", err)
|
||||
} else {
|
||||
t.Log("SMTP multipart test succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark Tests
|
||||
|
||||
func BenchmarkNewSMTPProvider(b *testing.B) {
|
||||
// Setup
|
||||
t := &testing.T{}
|
||||
config := loadPrimaryTestConfig(t)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
provider, err := NewSMTPProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = provider
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate(b *testing.B) {
|
||||
t := &testing.T{}
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := provider.Validate()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuildMessage(b *testing.B) {
|
||||
t := &testing.T{}
|
||||
config := loadPrimaryTestConfig(t)
|
||||
provider, err := NewSMTPProvider(config)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
message := createTestMessage(types.MessageTypeEmail)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := provider.buildMessage(message)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue