1360 lines
32 KiB
Go
1360 lines
32 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"goyco/internal/testutils"
|
|
)
|
|
|
|
func TestNewSMTPSender(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
port int
|
|
username string
|
|
password string
|
|
from string
|
|
expected *SMTPSender
|
|
}{
|
|
{
|
|
name: "valid configuration",
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password123",
|
|
from: "noreply@example.com",
|
|
expected: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password123",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
},
|
|
{
|
|
name: "empty credentials",
|
|
host: "smtp.example.com",
|
|
port: 25,
|
|
username: "",
|
|
password: "",
|
|
from: "noreply@example.com",
|
|
expected: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 25,
|
|
username: "",
|
|
password: "",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sender := NewSMTPSender(tt.host, tt.port, tt.username, tt.password, tt.from)
|
|
|
|
if sender.host != tt.expected.host {
|
|
t.Errorf("host = %v, want %v", sender.host, tt.expected.host)
|
|
}
|
|
if sender.port != tt.expected.port {
|
|
t.Errorf("port = %v, want %v", sender.port, tt.expected.port)
|
|
}
|
|
if sender.username != tt.expected.username {
|
|
t.Errorf("username = %v, want %v", sender.username, tt.expected.username)
|
|
}
|
|
if sender.password != tt.expected.password {
|
|
t.Errorf("password = %v, want %v", sender.password, tt.expected.password)
|
|
}
|
|
if sender.from != tt.expected.from {
|
|
t.Errorf("from = %v, want %v", sender.from, tt.expected.from)
|
|
}
|
|
if sender.timeout != tt.expected.timeout {
|
|
t.Errorf("timeout = %v, want %v", sender.timeout, tt.expected.timeout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSMTPSenderWithTimeout(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
port int
|
|
username string
|
|
password string
|
|
from string
|
|
timeout time.Duration
|
|
}{
|
|
{
|
|
name: "custom timeout",
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user",
|
|
password: "pass",
|
|
from: "from@example.com",
|
|
timeout: 60 * time.Second,
|
|
},
|
|
{
|
|
name: "zero timeout",
|
|
host: "smtp.example.com",
|
|
port: 25,
|
|
username: "",
|
|
password: "",
|
|
from: "from@example.com",
|
|
timeout: 0,
|
|
},
|
|
{
|
|
name: "very short timeout",
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user",
|
|
password: "pass",
|
|
from: "from@example.com",
|
|
timeout: 1 * time.Millisecond,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sender := NewSMTPSenderWithTimeout(tt.host, tt.port, tt.username, tt.password, tt.from, tt.timeout)
|
|
|
|
if sender.host != tt.host {
|
|
t.Errorf("host = %v, want %v", sender.host, tt.host)
|
|
}
|
|
if sender.port != tt.port {
|
|
t.Errorf("port = %v, want %v", sender.port, tt.port)
|
|
}
|
|
if sender.username != tt.username {
|
|
t.Errorf("username = %v, want %v", sender.username, tt.username)
|
|
}
|
|
if sender.password != tt.password {
|
|
t.Errorf("password = %v, want %v", sender.password, tt.password)
|
|
}
|
|
if sender.from != tt.from {
|
|
t.Errorf("from = %v, want %v", sender.from, tt.from)
|
|
}
|
|
if sender.timeout != tt.timeout {
|
|
t.Errorf("timeout = %v, want %v", sender.timeout, tt.timeout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Validation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sender *SMTPSender
|
|
to string
|
|
subject string
|
|
body string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "nil sender",
|
|
sender: nil,
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "smtp sender is not configured",
|
|
},
|
|
{
|
|
name: "empty recipient",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "recipient address is required",
|
|
},
|
|
{
|
|
name: "whitespace-only recipient",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: " ",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "recipient address is required",
|
|
},
|
|
{
|
|
name: "tab-only recipient",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "\t",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "recipient address is required",
|
|
},
|
|
{
|
|
name: "newline-only recipient",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "\n",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "recipient address is required",
|
|
},
|
|
{
|
|
name: "valid recipient with whitespace",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: " test@example.com ",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.sender.Send(tt.to, tt.subject, tt.body)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error but got none")
|
|
} else if !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("error message = %v, want to contain %v", err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Send(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sender *SMTPSender
|
|
to string
|
|
subject string
|
|
body string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "nil sender",
|
|
sender: nil,
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "smtp sender is not configured",
|
|
},
|
|
{
|
|
name: "empty recipient",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "recipient address is required",
|
|
},
|
|
{
|
|
name: "whitespace-only recipient",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: " ",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
expectError: true,
|
|
errorMsg: "recipient address is required",
|
|
},
|
|
{
|
|
name: "valid email with authentication",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: "<html><body>Test Body</body></html>",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid email without authentication",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 25,
|
|
username: "",
|
|
password: "",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: "<html><body>Test Body</body></html>",
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.sender.Send(tt.to, tt.subject, tt.body)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error but got none")
|
|
} else if !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("error message = %v, want to contain %v", err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func isDialErrorExpected(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
msg := err.Error()
|
|
expected := []string{
|
|
"connection refused",
|
|
"no such host",
|
|
"timeout",
|
|
"lookup",
|
|
"name resolution",
|
|
"temporary failure",
|
|
"network is unreachable",
|
|
}
|
|
|
|
for _, fragment := range expected {
|
|
if strings.Contains(msg, fragment) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func newTestSMTPClientOrSkip(t *testing.T) *testutils.TestSMTPClient {
|
|
t.Helper()
|
|
client, err := testutils.NewTestSMTPClient(func(port int) testutils.AsyncEmailSender {
|
|
return NewSMTPSenderWithTimeout(
|
|
"localhost",
|
|
port,
|
|
"testuser",
|
|
"testpass",
|
|
"test@example.com",
|
|
5*time.Second,
|
|
)
|
|
})
|
|
if err != nil {
|
|
msg := err.Error()
|
|
if strings.Contains(msg, "operation not permitted") || strings.Contains(msg, "permission denied") {
|
|
t.Skipf("skipping SMTP client integration tests: %v", err)
|
|
}
|
|
t.Fatalf("Failed to create test client: %v", err)
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
func TestSMTPSender_Send_MessageFormat(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
err := sender.Send("test@example.com", "Test Subject", "<html><body>Test Body</body></html>")
|
|
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Send_Timeout(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 1 * time.Millisecond,
|
|
}
|
|
|
|
err := sender.Send("test@example.com", "Test Subject", "Test Body")
|
|
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("expected timeout or connection error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Send_Headers(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
}
|
|
|
|
expectedAddress := "smtp.example.com:587"
|
|
actualAddress := net.JoinHostPort(sender.host, strconv.Itoa(sender.port))
|
|
|
|
if actualAddress != expectedAddress {
|
|
t.Errorf("address = %v, want %v", actualAddress, expectedAddress)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_SendAsync(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
resultChan := sender.SendAsync("test@example.com", "Test Subject", "Test Body")
|
|
|
|
select {
|
|
case err := <-resultChan:
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("async send timed out")
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_SendAsync_Concurrent(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
numEmails := 5
|
|
resultChans := make([]<-chan error, numEmails)
|
|
|
|
for i := range numEmails {
|
|
resultChans[i] = sender.SendAsync(
|
|
fmt.Sprintf("test%d@example.com", i),
|
|
fmt.Sprintf("Test Subject %d", i),
|
|
fmt.Sprintf("Test Body %d", i),
|
|
)
|
|
}
|
|
|
|
for i, resultChan := range resultChans {
|
|
select {
|
|
case err := <-resultChan:
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type for email %d: %v", i, err)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Errorf("async send %d timed out", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_EdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sender *SMTPSender
|
|
to string
|
|
subject string
|
|
body string
|
|
}{
|
|
{
|
|
name: "very long subject",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: strings.Repeat("A", 1000),
|
|
body: "Test Body",
|
|
},
|
|
{
|
|
name: "very long body",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: strings.Repeat("A", 10000),
|
|
},
|
|
{
|
|
name: "special characters in subject",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "Test Subject with émojis 🎉 and special chars: !@#$%^&*()",
|
|
body: "Test Body",
|
|
},
|
|
{
|
|
name: "HTML body with special characters",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: "<html><body>Test Body with émojis 🎉 and special chars: !@#$%^&*()</body></html>",
|
|
},
|
|
{
|
|
name: "empty subject",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "",
|
|
body: "Test Body",
|
|
},
|
|
{
|
|
name: "empty body",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test@example.com",
|
|
subject: "Test Subject",
|
|
body: "",
|
|
},
|
|
{
|
|
name: "very long email address",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: strings.Repeat("a", 50) + "@" + strings.Repeat("b", 50) + ".com",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
},
|
|
{
|
|
name: "email with unicode characters",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "tëst@éxämplé.com",
|
|
subject: "Tëst Súbjëct",
|
|
body: "Tëst Bódy with únicódé",
|
|
},
|
|
{
|
|
name: "email with plus sign",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test+tag@example.com",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
},
|
|
{
|
|
name: "email with multiple dots",
|
|
sender: &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
},
|
|
to: "test.user.name@sub.domain.example.com",
|
|
subject: "Test Subject",
|
|
body: "Test Body",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.sender.Send(tt.to, tt.subject, tt.body)
|
|
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_RecipientTrimming(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
testCases := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{" test@example.com ", "test@example.com"},
|
|
{"\ttest@example.com\t", "test@example.com"},
|
|
{"\n test@example.com \n", "test@example.com"},
|
|
{"test@example.com", "test@example.com"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(fmt.Sprintf("trim_%s", tc.input), func(t *testing.T) {
|
|
err := sender.Send(tc.input, "Test Subject", "Test Body")
|
|
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_TimeoutBehavior(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 1 * time.Nanosecond,
|
|
}
|
|
|
|
start := time.Now()
|
|
err := sender.Send("test@example.com", "Test Subject", "Test Body")
|
|
duration := time.Since(start)
|
|
|
|
if duration > 100*time.Millisecond {
|
|
t.Errorf("timeout took too long: %v", duration)
|
|
}
|
|
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("expected timeout error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_TimeoutVariations(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
timeout time.Duration
|
|
}{
|
|
{
|
|
name: "zero timeout",
|
|
timeout: 0,
|
|
},
|
|
{
|
|
name: "1 nanosecond timeout",
|
|
timeout: 1 * time.Nanosecond,
|
|
},
|
|
{
|
|
name: "1 microsecond timeout",
|
|
timeout: 1 * time.Microsecond,
|
|
},
|
|
{
|
|
name: "1 millisecond timeout",
|
|
timeout: 1 * time.Millisecond,
|
|
},
|
|
{
|
|
name: "1 second timeout",
|
|
timeout: 1 * time.Second,
|
|
},
|
|
{
|
|
name: "1 hour timeout",
|
|
timeout: 1 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: tt.timeout,
|
|
}
|
|
|
|
start := time.Now()
|
|
err := sender.Send("test@example.com", "Test Subject", "Test Body")
|
|
duration := time.Since(start)
|
|
|
|
if tt.timeout < 100*time.Millisecond {
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("expected timeout or DNS error, got: %v", err)
|
|
}
|
|
if duration > 2*time.Second {
|
|
t.Errorf("timeout took too long: %v", duration)
|
|
}
|
|
} else {
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_ErrorRecovery(t *testing.T) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
for i := range 5 {
|
|
err := sender.Send("test@example.com", "Test Subject", "Test Body")
|
|
if err != nil && !isDialErrorExpected(err) {
|
|
t.Errorf("unexpected error type on attempt %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_InterfaceCompliance(t *testing.T) {
|
|
var _ EmailSender = (*SMTPSender)(nil)
|
|
}
|
|
|
|
func BenchmarkSMTPSender_Send(b *testing.B) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
for b.Loop() {
|
|
sender.Send("test@example.com", "Test Subject", "Test Body")
|
|
}
|
|
}
|
|
|
|
func BenchmarkSMTPSender_SendAsync(b *testing.B) {
|
|
sender := &SMTPSender{
|
|
host: "smtp.example.com",
|
|
port: 587,
|
|
username: "user@example.com",
|
|
password: "password",
|
|
from: "noreply@example.com",
|
|
timeout: 30 * time.Second,
|
|
}
|
|
|
|
for b.Loop() {
|
|
resultChan := sender.SendAsync("test@example.com", "Test Subject", "Test Body")
|
|
<-resultChan
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Comprehensive(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
err := client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
if err != nil {
|
|
t.Errorf("Send failed: %v", err)
|
|
}
|
|
|
|
if !client.WaitForEmail(1 * time.Second) {
|
|
t.Error("Email not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 1 {
|
|
t.Errorf("Expected 1 email, got %d", len(emails))
|
|
return
|
|
}
|
|
|
|
validator := testutils.NewTestEmailValidator()
|
|
errors := validator.ValidateEmail(&emails[0], "recipient@example.com", "Test Subject", "Test Body")
|
|
if len(errors) > 0 {
|
|
t.Errorf("Email validation failed: %v", errors)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_HTML_Email(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
htmlBody := "<html><body><h1>Test</h1><p>This is a test email.</p></body></html>"
|
|
err := client.SendTestEmail("recipient@example.com", "HTML Test Subject", htmlBody)
|
|
if err != nil {
|
|
t.Errorf("Send failed: %v", err)
|
|
}
|
|
|
|
if !client.WaitForEmail(1 * time.Second) {
|
|
t.Error("Email not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 1 {
|
|
t.Errorf("Expected 1 email, got %d", len(emails))
|
|
return
|
|
}
|
|
|
|
email := emails[0]
|
|
if !strings.Contains(email.Body, "<html>") {
|
|
t.Errorf("Body should contain HTML: %v", email.Body)
|
|
}
|
|
|
|
if email.Headers["Content-Type"] != "text/html; charset=\"UTF-8\"" {
|
|
t.Errorf("Content-Type = %v, want text/html; charset=\"UTF-8\"", email.Headers["Content-Type"])
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Async_Email(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
resultChan := client.SendTestEmailAsync("recipient@example.com", "Async Test Subject", "Async Test Body")
|
|
|
|
select {
|
|
case err := <-resultChan:
|
|
if err != nil {
|
|
t.Errorf("Async send failed: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("Async send timed out")
|
|
}
|
|
|
|
if !client.WaitForEmail(1 * time.Second) {
|
|
t.Error("Email not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 1 {
|
|
t.Errorf("Expected 1 email, got %d", len(emails))
|
|
return
|
|
}
|
|
|
|
validator := testutils.NewTestEmailValidator()
|
|
errors := validator.ValidateEmail(&emails[0], "recipient@example.com", "Async Test Subject", "Async Test Body")
|
|
if len(errors) > 0 {
|
|
t.Errorf("Email validation failed: %v", errors)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Multiple_Emails(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
numEmails := 5
|
|
for i := range numEmails {
|
|
err := client.SendTestEmail(
|
|
"recipient@example.com",
|
|
"Test Subject",
|
|
"Test Body",
|
|
)
|
|
if err != nil {
|
|
t.Errorf("Send %d failed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
if !client.WaitForEmail(2 * time.Second) {
|
|
t.Error("Emails not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != numEmails {
|
|
t.Errorf("Expected %d emails, got %d", numEmails, len(emails))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Concurrent_Emails(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
numEmails := 10
|
|
done := make(chan error, numEmails)
|
|
|
|
for i := range numEmails {
|
|
go func(i int) {
|
|
err := client.SendTestEmail(
|
|
"recipient@example.com",
|
|
"Test Subject",
|
|
"Test Body",
|
|
)
|
|
done <- err
|
|
}(i)
|
|
}
|
|
|
|
for i := range numEmails {
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Errorf("Concurrent send %d failed: %v", i, err)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
t.Errorf("Concurrent send %d timed out", i)
|
|
}
|
|
}
|
|
|
|
if !client.WaitForEmail(2 * time.Second) {
|
|
t.Error("Emails not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != numEmails {
|
|
t.Errorf("Expected %d emails, got %d", numEmails, len(emails))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Server_Failure(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
client.Server().SetShouldFail(true)
|
|
|
|
err := client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 0 {
|
|
t.Errorf("Expected 0 emails, got %d", len(emails))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Timeout(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
client.Server().SetDelay(2 * time.Second)
|
|
client.SetTimeout(1 * time.Second)
|
|
|
|
start := time.Now()
|
|
err := client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
duration := time.Since(start)
|
|
|
|
if err == nil {
|
|
t.Error("Expected timeout error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "timeout") {
|
|
t.Errorf("Expected timeout error, got: %v", err)
|
|
}
|
|
|
|
if duration > 2*time.Second {
|
|
t.Errorf("Timeout took too long: %v", duration)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Email_Headers(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
err := client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
if err != nil {
|
|
t.Errorf("Send failed: %v", err)
|
|
}
|
|
|
|
if !client.WaitForEmail(1 * time.Second) {
|
|
t.Error("Email not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 1 {
|
|
t.Errorf("Expected 1 email, got %d", len(emails))
|
|
return
|
|
}
|
|
|
|
email := emails[0]
|
|
expectedHeaders := map[string]string{
|
|
"From": "test@example.com",
|
|
"To": "recipient@example.com",
|
|
"Subject": "Test Subject",
|
|
"Content-Type": "text/html; charset=\"UTF-8\"",
|
|
"MIME-Version": "1.0",
|
|
}
|
|
|
|
validator := testutils.NewTestEmailValidator()
|
|
errors := validator.ValidateEmailHeaders(&email, expectedHeaders)
|
|
if len(errors) > 0 {
|
|
t.Errorf("Header validation failed: %v", errors)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Email_Matcher(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
emails := []struct {
|
|
to string
|
|
subject string
|
|
body string
|
|
}{
|
|
{"user1@example.com", "Subject 1", "Body 1"},
|
|
{"user2@example.com", "Subject 2", "Body 2"},
|
|
{"user3@example.com", "Subject 3", "Body 3"},
|
|
}
|
|
|
|
for _, email := range emails {
|
|
err := client.SendTestEmail(email.to, email.subject, email.body)
|
|
if err != nil {
|
|
t.Errorf("Send failed: %v", err)
|
|
}
|
|
}
|
|
|
|
if !client.WaitForEmail(2 * time.Second) {
|
|
t.Error("Emails not received within timeout")
|
|
}
|
|
|
|
receivedEmails := client.GetReceivedEmails()
|
|
if len(receivedEmails) != len(emails) {
|
|
t.Errorf("Expected %d emails, got %d", len(emails), len(receivedEmails))
|
|
}
|
|
|
|
matcher := testutils.NewTestEmailMatcher()
|
|
|
|
email1 := matcher.FindEmail(receivedEmails, map[string]any{
|
|
"to": "user1@example.com",
|
|
})
|
|
if email1 == nil {
|
|
t.Error("Email 1 not found")
|
|
}
|
|
|
|
email2 := matcher.FindEmail(receivedEmails, map[string]any{
|
|
"subject": "Subject 2",
|
|
})
|
|
if email2 == nil {
|
|
t.Error("Email 2 not found")
|
|
}
|
|
|
|
email3 := matcher.FindEmail(receivedEmails, map[string]any{
|
|
"body_contains": "Body 3",
|
|
})
|
|
if email3 == nil {
|
|
t.Error("Email 3 not found")
|
|
}
|
|
|
|
count := matcher.CountMatchingEmails(receivedEmails, map[string]any{
|
|
"subject_contains": "Subject",
|
|
})
|
|
if count != 3 {
|
|
t.Errorf("Expected 3 emails with 'Subject' in subject, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Email_Builder(t *testing.T) {
|
|
builder := testutils.NewTestEmailBuilder()
|
|
email := builder.
|
|
From("sender@example.com").
|
|
To("recipient@example.com").
|
|
Subject("Test Subject").
|
|
Body("Test Body").
|
|
Header("X-Custom-Header", "Custom Value").
|
|
Build()
|
|
|
|
if email.From != "sender@example.com" {
|
|
t.Errorf("From = %v, want sender@example.com", email.From)
|
|
}
|
|
if len(email.To) != 1 || email.To[0] != "recipient@example.com" {
|
|
t.Errorf("To = %v, want [recipient@example.com]", email.To)
|
|
}
|
|
if email.Subject != "Test Subject" {
|
|
t.Errorf("Subject = %v, want Test Subject", email.Subject)
|
|
}
|
|
if email.Body != "Test Body" {
|
|
t.Errorf("Body = %v, want Test Body", email.Body)
|
|
}
|
|
if email.Headers["X-Custom-Header"] != "Custom Value" {
|
|
t.Errorf("X-Custom-Header = %v, want Custom Value", email.Headers["X-Custom-Header"])
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Performance(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
numEmails := 100
|
|
start := time.Now()
|
|
|
|
for i := range numEmails {
|
|
err := client.SendTestEmail(
|
|
"recipient@example.com",
|
|
"Test Subject",
|
|
"Test Body",
|
|
)
|
|
if err != nil {
|
|
t.Errorf("Send %d failed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
t.Logf("Sent %d emails in %v (%.2f emails/sec)", numEmails, duration, float64(numEmails)/duration.Seconds())
|
|
|
|
if !client.WaitForEmail(5 * time.Second) {
|
|
t.Error("Emails not received within timeout")
|
|
}
|
|
|
|
receivedEmails := client.GetReceivedEmails()
|
|
if len(receivedEmails) != numEmails {
|
|
t.Errorf("Expected %d emails, got %d", numEmails, len(receivedEmails))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Stress(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
largeBody := strings.Repeat("This is a test email body. ", 1000)
|
|
|
|
err := client.SendTestEmail("recipient@example.com", "Large Email Test", largeBody)
|
|
if err != nil {
|
|
t.Errorf("Send large email failed: %v", err)
|
|
}
|
|
|
|
if !client.WaitForEmail(2 * time.Second) {
|
|
t.Error("Large email not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 1 {
|
|
t.Errorf("Expected 1 email, got %d", len(emails))
|
|
}
|
|
|
|
email := emails[0]
|
|
if len(email.Body) < 1000 {
|
|
t.Errorf("Email body too short: %d characters", len(email.Body))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_Error_Recovery(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
err := client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
if err != nil {
|
|
t.Errorf("First send failed: %v", err)
|
|
}
|
|
|
|
if !client.WaitForEmail(1 * time.Second) {
|
|
t.Error("First email not received within timeout")
|
|
}
|
|
|
|
client.Server().SetShouldFail(true)
|
|
|
|
err = client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
if err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
|
|
client.Server().SetShouldFail(false)
|
|
|
|
err = client.SendTestEmail("recipient@example.com", "Test Subject", "Test Body")
|
|
if err != nil {
|
|
t.Errorf("Recovery send failed: %v", err)
|
|
}
|
|
|
|
if !client.WaitForEmail(1 * time.Second) {
|
|
t.Error("Recovery email not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != 2 {
|
|
t.Errorf("Expected 2 emails, got %d", len(emails))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_ConcurrentStress(t *testing.T) {
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
numGoroutines := 50
|
|
numEmailsPerGoroutine := 2
|
|
totalEmails := numGoroutines * numEmailsPerGoroutine
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, totalEmails)
|
|
|
|
start := time.Now()
|
|
|
|
for i := range numGoroutines {
|
|
wg.Add(1)
|
|
go func(goroutineID int) {
|
|
defer wg.Done()
|
|
for j := range numEmailsPerGoroutine {
|
|
err := client.SendTestEmail(
|
|
fmt.Sprintf("user%d@example.com", goroutineID),
|
|
fmt.Sprintf("Subject %d-%d", goroutineID, j),
|
|
fmt.Sprintf("Body %d-%d", goroutineID, j),
|
|
)
|
|
if err != nil {
|
|
errors <- fmt.Errorf("goroutine %d, email %d: %v", goroutineID, j, err)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
duration := time.Since(start)
|
|
t.Logf("Sent %d emails in %v (%.2f emails/sec)", totalEmails, duration, float64(totalEmails)/duration.Seconds())
|
|
|
|
errorCount := 0
|
|
for err := range errors {
|
|
t.Errorf("Concurrent send error: %v", err)
|
|
errorCount++
|
|
}
|
|
|
|
if errorCount > 0 {
|
|
t.Errorf("Had %d errors out of %d emails", errorCount, totalEmails)
|
|
}
|
|
|
|
if !client.WaitForEmail(5 * time.Second) {
|
|
t.Error("Emails not received within timeout")
|
|
}
|
|
|
|
emails := client.GetReceivedEmails()
|
|
if len(emails) != totalEmails {
|
|
t.Errorf("Expected %d emails, got %d", totalEmails, len(emails))
|
|
}
|
|
}
|
|
|
|
func TestSMTPSender_RaceConditions(t *testing.T) {
|
|
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
sender := client.Sender()
|
|
|
|
numGoroutines := 10
|
|
var wg sync.WaitGroup
|
|
|
|
for i := range numGoroutines {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
sender.Send(fmt.Sprintf("test%d@example.com", id), "Test Subject", "Test Body")
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestSMTPSender_MemoryLeaks(t *testing.T) {
|
|
|
|
client := newTestSMTPClientOrSkip(t)
|
|
defer client.Close()
|
|
|
|
sender := client.Sender()
|
|
|
|
numEmails := 1000
|
|
var wg sync.WaitGroup
|
|
|
|
for i := range numEmails {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
sender.Send(fmt.Sprintf("test%d@example.com", id), "Test Subject", "Test Body")
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|