test: Add unit tests for email_enhanced.go error handling
Add comprehensive tests for EnhancedEmailManager covering: - Rate limiting (exceeds limit, resets after minute, concurrency, negative values) - Provider username defaults (SendGrid, SparkPost, Resend, Postmark) - TLS routing (TLS, StartTLS, plain connections) - Retry logic (retries on failure, rate limit prevents retry) - Connection error handling for sendTLS, sendStartTLS, sendPlain This is the first test file for email_enhanced.go which previously had 0% coverage on sendTLS, sendStartTLS, and TestConnection functions.
This commit is contained in:
parent
f02a340d72
commit
827a8b4d37
1 changed files with 463 additions and 0 deletions
463
internal/notifications/email_enhanced_test.go
Normal file
463
internal/notifications/email_enhanced_test.go
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewEnhancedEmailManager(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "smtp.example.com",
|
||||
SMTPPort: 587,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
RateLimit: 10,
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
if manager == nil {
|
||||
t.Fatal("NewEnhancedEmailManager returned nil")
|
||||
}
|
||||
if manager.rateLimit == nil {
|
||||
t.Fatal("rate limiter not initialized")
|
||||
}
|
||||
if manager.rateLimit.rate != 10 {
|
||||
t.Errorf("expected rate limit 10, got %d", manager.rateLimit.rate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRateLimit_NoLimit(t *testing.T) {
|
||||
manager := NewEnhancedEmailManager(EmailProviderConfig{
|
||||
RateLimit: 0, // No limit
|
||||
})
|
||||
|
||||
// Should always succeed when no rate limit
|
||||
for i := 0; i < 100; i++ {
|
||||
if err := manager.checkRateLimit(); err != nil {
|
||||
t.Errorf("checkRateLimit should not error with no limit: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRateLimit_ExceedsLimit(t *testing.T) {
|
||||
manager := NewEnhancedEmailManager(EmailProviderConfig{
|
||||
RateLimit: 3,
|
||||
})
|
||||
|
||||
// First 3 should succeed
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := manager.checkRateLimit(); err != nil {
|
||||
t.Errorf("call %d should succeed: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4th should fail
|
||||
err := manager.checkRateLimit()
|
||||
if err == nil {
|
||||
t.Error("expected rate limit error on 4th call")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "rate limit exceeded") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRateLimit_ResetsAfterMinute(t *testing.T) {
|
||||
manager := NewEnhancedEmailManager(EmailProviderConfig{
|
||||
RateLimit: 2,
|
||||
})
|
||||
|
||||
// Use up the limit
|
||||
manager.checkRateLimit()
|
||||
manager.checkRateLimit()
|
||||
|
||||
// Manually set lastSent to over a minute ago
|
||||
manager.rateLimit.mu.Lock()
|
||||
manager.rateLimit.lastSent = time.Now().Add(-2 * time.Minute)
|
||||
manager.rateLimit.mu.Unlock()
|
||||
|
||||
// Should succeed after reset
|
||||
if err := manager.checkRateLimit(); err != nil {
|
||||
t.Errorf("should succeed after minute reset: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendViaProvider_ProviderUsernameDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
provider string
|
||||
initialUsername string
|
||||
expectedUsername string
|
||||
}{
|
||||
{
|
||||
name: "SendGrid sets apikey username",
|
||||
provider: "SendGrid",
|
||||
initialUsername: "",
|
||||
expectedUsername: "apikey",
|
||||
},
|
||||
{
|
||||
name: "SendGrid preserves existing username",
|
||||
provider: "SendGrid",
|
||||
initialUsername: "custom",
|
||||
expectedUsername: "custom",
|
||||
},
|
||||
{
|
||||
name: "SparkPost sets SMTP_Injection username",
|
||||
provider: "SparkPost",
|
||||
initialUsername: "",
|
||||
expectedUsername: "SMTP_Injection",
|
||||
},
|
||||
{
|
||||
name: "Resend sets resend username",
|
||||
provider: "Resend",
|
||||
initialUsername: "",
|
||||
expectedUsername: "resend",
|
||||
},
|
||||
{
|
||||
name: "Unknown provider leaves username unchanged",
|
||||
provider: "Custom",
|
||||
initialUsername: "",
|
||||
expectedUsername: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test", // Will fail to connect
|
||||
SMTPPort: 587,
|
||||
Username: tt.initialUsername,
|
||||
Password: "test",
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
Provider: tt.provider,
|
||||
AuthRequired: true,
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
// Call sendViaProvider - it will fail on connection, but will have set username
|
||||
_ = manager.sendViaProvider([]byte("test"))
|
||||
|
||||
if manager.config.Username != tt.expectedUsername {
|
||||
t.Errorf("expected username %q, got %q", tt.expectedUsername, manager.config.Username)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendViaProvider_PostmarkUsernameFromPassword(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 587,
|
||||
Username: "",
|
||||
Password: "postmark-api-token",
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
Provider: "Postmark",
|
||||
AuthRequired: true,
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
_ = manager.sendViaProvider([]byte("test"))
|
||||
|
||||
// Postmark copies password to username when username is empty
|
||||
if manager.config.Username != "postmark-api-token" {
|
||||
t.Errorf("expected username to be set from password, got %q", manager.config.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendViaProvider_RoutingByTLSConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tls bool
|
||||
startTLS bool
|
||||
port int
|
||||
expectsError string // Partial match of expected error
|
||||
}{
|
||||
{
|
||||
name: "TLS true routes to sendTLS",
|
||||
tls: true,
|
||||
startTLS: false,
|
||||
port: 587,
|
||||
expectsError: "TLS dial failed",
|
||||
},
|
||||
{
|
||||
name: "Port 465 routes to sendTLS",
|
||||
tls: false,
|
||||
startTLS: false,
|
||||
port: 465,
|
||||
expectsError: "TLS dial failed",
|
||||
},
|
||||
{
|
||||
name: "StartTLS routes to sendStartTLS",
|
||||
tls: false,
|
||||
startTLS: true,
|
||||
port: 587,
|
||||
expectsError: "TCP dial failed",
|
||||
},
|
||||
{
|
||||
name: "Plain routes to sendPlain",
|
||||
tls: false,
|
||||
startTLS: false,
|
||||
port: 25,
|
||||
expectsError: "TCP dial failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: tt.port,
|
||||
TLS: tt.tls,
|
||||
StartTLS: tt.startTLS,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.sendViaProvider([]byte("test"))
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected connection error")
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), tt.expectsError) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.expectsError, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailWithRetry_RetriesOnFailure(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 587,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
MaxRetries: 2,
|
||||
RetryDelay: 0, // No delay for tests
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.SendEmailWithRetry("Test", "<p>test</p>", "test")
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error after all retries exhausted")
|
||||
}
|
||||
|
||||
// Should mention the retry count
|
||||
if !strings.Contains(err.Error(), "3 attempts") {
|
||||
t.Errorf("error should mention attempt count: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailWithRetry_RateLimitPreventsRetry(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 587,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
MaxRetries: 3,
|
||||
RetryDelay: 0,
|
||||
RateLimit: 1, // Only 1 per minute
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
|
||||
// First send uses the 1 allowed
|
||||
err := manager.SendEmailWithRetry("Test", "<p>test</p>", "test")
|
||||
if err == nil {
|
||||
t.Error("expected error (connection should fail)")
|
||||
}
|
||||
|
||||
// Second send should hit rate limit on all retries
|
||||
err = manager.SendEmailWithRetry("Test2", "<p>test</p>", "test")
|
||||
if err == nil {
|
||||
t.Error("expected rate limit error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "rate limit exceeded") {
|
||||
t.Errorf("expected rate limit error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmailOnce_BuildsMultipartMessage(t *testing.T) {
|
||||
// We can't test actual sending, but we can verify the method doesn't panic
|
||||
// with valid inputs and returns expected connection error
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 587,
|
||||
From: "sender@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
ReplyTo: "reply@example.com",
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.sendEmailOnce("Test Subject", "<p>HTML Body</p>", "Text Body")
|
||||
|
||||
// Should fail on connection, not on message building
|
||||
if err == nil {
|
||||
t.Error("expected connection error")
|
||||
}
|
||||
// Error should be from connection, not from message construction
|
||||
if strings.Contains(err.Error(), "message") && strings.Contains(err.Error(), "build") {
|
||||
t.Errorf("unexpected message build error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestConnection_TLSRouting(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tls bool
|
||||
port int
|
||||
wantTLS bool
|
||||
}{
|
||||
{"TLS true uses TLS dial", true, 587, true},
|
||||
{"Port 465 uses TLS dial", false, 465, true},
|
||||
{"Port 587 without TLS uses plain dial", false, 587, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: tt.port,
|
||||
TLS: tt.tls,
|
||||
},
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.TestConnection()
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected connection error")
|
||||
}
|
||||
|
||||
// Verify error message indicates correct connection type
|
||||
if tt.wantTLS && strings.Contains(err.Error(), "TCP dial") {
|
||||
t.Error("TLS connection should not produce TCP dial error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendTLS_ConnectionError(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 465,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.sendTLS("invalid.host.test:465", nil, []byte("test"))
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected TLS dial error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "TLS dial failed") {
|
||||
t.Errorf("expected TLS dial error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendStartTLS_ConnectionError(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 587,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.sendStartTLS("invalid.host.test:587", nil, []byte("test"))
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected TCP dial error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "TCP dial failed") {
|
||||
t.Errorf("expected TCP dial error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendPlain_ConnectionError(t *testing.T) {
|
||||
config := EmailProviderConfig{
|
||||
EmailConfig: EmailConfig{
|
||||
SMTPHost: "invalid.host.test",
|
||||
SMTPPort: 25,
|
||||
From: "test@example.com",
|
||||
To: []string{"recipient@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
manager := NewEnhancedEmailManager(config)
|
||||
err := manager.sendPlain("invalid.host.test:25", nil, []byte("test"))
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected TCP dial error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "TCP dial failed") {
|
||||
t.Errorf("expected TCP dial error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRateLimit_NegativeLimit(t *testing.T) {
|
||||
// Negative rate limit should be treated as no limit
|
||||
manager := NewEnhancedEmailManager(EmailProviderConfig{
|
||||
RateLimit: -1,
|
||||
})
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := manager.checkRateLimit(); err != nil {
|
||||
t.Errorf("negative rate limit should allow all calls: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRateLimit_Concurrency(t *testing.T) {
|
||||
manager := NewEnhancedEmailManager(EmailProviderConfig{
|
||||
RateLimit: 100,
|
||||
})
|
||||
|
||||
// Run concurrent rate limit checks
|
||||
done := make(chan bool, 50)
|
||||
for i := 0; i < 50; i++ {
|
||||
go func() {
|
||||
manager.checkRateLimit()
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 50; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify counter is correct (should be 50)
|
||||
manager.rateLimit.mu.Lock()
|
||||
count := manager.rateLimit.sentCount
|
||||
manager.rateLimit.mu.Unlock()
|
||||
|
||||
if count != 50 {
|
||||
t.Errorf("expected count 50 after concurrent calls, got %d", count)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue