6ed6c7780b
Links to PROJECT-MEMORY.md and DECISIONS.md for development rules and architectural decisions, plus quick commands and doc index.
457 lines
12 KiB
Go
457 lines
12 KiB
Go
package email
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewService(t *testing.T) {
|
|
config := &Config{
|
|
SMTPHost: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
SMTPUser: "user@example.com",
|
|
SMTPPassword: "password",
|
|
FromEmail: "from@example.com",
|
|
ToEmail: "to@example.com",
|
|
}
|
|
|
|
service := NewService(config)
|
|
|
|
if service == nil {
|
|
t.Fatal("NewService should return a non-nil service")
|
|
}
|
|
|
|
if service.config != config {
|
|
t.Error("NewService should store the config")
|
|
}
|
|
}
|
|
|
|
func TestContactFormData_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data ContactFormData
|
|
wantError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "Valid - all fields",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Name: "Test User",
|
|
Company: "Test Company",
|
|
Subject: "Test Subject",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Valid - minimal fields",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "Invalid - missing email",
|
|
data: ContactFormData{
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "email is required",
|
|
},
|
|
{
|
|
name: "Invalid - missing message",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "message is required",
|
|
},
|
|
{
|
|
name: "Invalid - bad email format (no @)",
|
|
data: ContactFormData{
|
|
Email: "testexample.com",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "invalid email format",
|
|
},
|
|
{
|
|
name: "Invalid - bad email format (no .)",
|
|
data: ContactFormData{
|
|
Email: "test@examplecom",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "invalid email format",
|
|
},
|
|
{
|
|
name: "Invalid - email with newline",
|
|
data: ContactFormData{
|
|
Email: "test@example.com\r\nBcc: hacker@evil.com",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "invalid email: contains prohibited characters",
|
|
},
|
|
{
|
|
name: "Invalid - subject with newline",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Subject: "Test\r\nBcc: hacker@evil.com",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "invalid subject: contains prohibited characters",
|
|
},
|
|
{
|
|
name: "Invalid - email too long",
|
|
data: ContactFormData{
|
|
Email: strings.Repeat("a", 250) + "@example.com",
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "email too long",
|
|
},
|
|
{
|
|
name: "Invalid - name too long",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Name: strings.Repeat("a", 101),
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "name too long",
|
|
},
|
|
{
|
|
name: "Invalid - company too long",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Company: strings.Repeat("a", 101),
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "company too long",
|
|
},
|
|
{
|
|
name: "Invalid - subject too long",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Subject: strings.Repeat("a", 201),
|
|
Message: "This is a test message with enough characters.",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "subject too long",
|
|
},
|
|
{
|
|
name: "Invalid - message too long",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Message: strings.Repeat("a", 5001),
|
|
},
|
|
wantError: true,
|
|
errorMsg: "message too long",
|
|
},
|
|
{
|
|
name: "Invalid - message too short",
|
|
data: ContactFormData{
|
|
Email: "test@example.com",
|
|
Message: "Short",
|
|
},
|
|
wantError: true,
|
|
errorMsg: "message too short",
|
|
},
|
|
{
|
|
name: "Valid - trims whitespace",
|
|
data: ContactFormData{
|
|
Email: " test@example.com ",
|
|
Name: " Test User ",
|
|
Message: " This is a test message with enough characters. ",
|
|
},
|
|
wantError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.data.Validate()
|
|
if (err != nil) != tt.wantError {
|
|
t.Errorf("Validate() error = %v, wantError %v", err, tt.wantError)
|
|
}
|
|
if err != nil && tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
|
|
t.Errorf("Validate() error = %v, want error containing %q", err, tt.errorMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainsNewlines(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"No newlines", "normal text", false},
|
|
{"Carriage return", "text\rmore", true},
|
|
{"Newline", "text\nmore", true},
|
|
{"CRLF", "text\r\nmore", true},
|
|
{"Empty", "", false},
|
|
{"Spaces only", " ", false},
|
|
{"Tab (allowed)", "text\ttab", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := containsNewlines(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("containsNewlines(%q) = %v, want %v", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatMultipartMessage(t *testing.T) {
|
|
service := NewService(&Config{
|
|
SMTPHost: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
})
|
|
|
|
message := service.formatMultipartMessage(
|
|
"from@example.com",
|
|
"to@example.com",
|
|
"reply@example.com",
|
|
"Test Subject",
|
|
"<html><body>HTML Body</body></html>",
|
|
"Plain text body",
|
|
)
|
|
|
|
// Check required headers
|
|
if !strings.Contains(message, "From: from@example.com") {
|
|
t.Error("Message should contain From header")
|
|
}
|
|
if !strings.Contains(message, "To: to@example.com") {
|
|
t.Error("Message should contain To header")
|
|
}
|
|
if !strings.Contains(message, "Reply-To: reply@example.com") {
|
|
t.Error("Message should contain Reply-To header")
|
|
}
|
|
if !strings.Contains(message, "Subject: Test Subject") {
|
|
t.Error("Message should contain Subject header")
|
|
}
|
|
if !strings.Contains(message, "MIME-Version: 1.0") {
|
|
t.Error("Message should contain MIME-Version header")
|
|
}
|
|
if !strings.Contains(message, "multipart/alternative") {
|
|
t.Error("Message should be multipart/alternative")
|
|
}
|
|
if !strings.Contains(message, "text/plain") {
|
|
t.Error("Message should contain text/plain part")
|
|
}
|
|
if !strings.Contains(message, "text/html") {
|
|
t.Error("Message should contain text/html part")
|
|
}
|
|
if !strings.Contains(message, "Plain text body") {
|
|
t.Error("Message should contain plain text body")
|
|
}
|
|
}
|
|
|
|
func TestFormatMultipartMessage_NoReplyTo(t *testing.T) {
|
|
service := NewService(&Config{
|
|
SMTPHost: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
})
|
|
|
|
message := service.formatMultipartMessage(
|
|
"from@example.com",
|
|
"to@example.com",
|
|
"", // No reply-to
|
|
"Test Subject",
|
|
"<html>HTML</html>",
|
|
"Plain text",
|
|
)
|
|
|
|
if strings.Contains(message, "Reply-To:") {
|
|
t.Error("Message should not contain Reply-To header when empty")
|
|
}
|
|
}
|
|
|
|
func TestBuildEmailBody(t *testing.T) {
|
|
service := NewService(&Config{
|
|
SMTPHost: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
})
|
|
|
|
data := &ContactFormData{
|
|
Email: "sender@example.com",
|
|
Name: "Test User",
|
|
Company: "Test Company",
|
|
Subject: "Test Subject",
|
|
Message: "This is a test message.",
|
|
IP: "192.168.1.1",
|
|
Time: time.Now(),
|
|
}
|
|
|
|
htmlBody, textBody, err := service.buildEmailBody(data)
|
|
|
|
if err != nil {
|
|
t.Errorf("buildEmailBody() error = %v", err)
|
|
}
|
|
|
|
// Check HTML body contains data
|
|
if !strings.Contains(htmlBody, "Test User") {
|
|
t.Error("HTML body should contain name")
|
|
}
|
|
if !strings.Contains(htmlBody, "sender@example.com") {
|
|
t.Error("HTML body should contain email")
|
|
}
|
|
if !strings.Contains(htmlBody, "This is a test message") {
|
|
t.Error("HTML body should contain message")
|
|
}
|
|
|
|
// Check text body contains data
|
|
if !strings.Contains(textBody, "Test User") {
|
|
t.Error("Text body should contain name")
|
|
}
|
|
if !strings.Contains(textBody, "sender@example.com") {
|
|
t.Error("Text body should contain email")
|
|
}
|
|
}
|
|
|
|
func TestBuildEmailBody_EmptyName(t *testing.T) {
|
|
service := NewService(&Config{
|
|
SMTPHost: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
})
|
|
|
|
data := &ContactFormData{
|
|
Email: "sender@example.com",
|
|
Name: "", // Empty name
|
|
Message: "This is a test message.",
|
|
Time: time.Now(),
|
|
}
|
|
|
|
htmlBody, textBody, err := service.buildEmailBody(data)
|
|
|
|
if err != nil {
|
|
t.Errorf("buildEmailBody() error = %v", err)
|
|
}
|
|
|
|
// Should show "Not provided" for empty name
|
|
if !strings.Contains(htmlBody, "Not provided") {
|
|
t.Error("HTML body should show 'Not provided' for empty name")
|
|
}
|
|
if !strings.Contains(textBody, "Not provided") {
|
|
t.Error("Text body should show 'Not provided' for empty name")
|
|
}
|
|
}
|
|
|
|
func TestSendContactForm_ValidationError(t *testing.T) {
|
|
service := NewService(&Config{
|
|
SMTPHost: "smtp.example.com",
|
|
SMTPPort: "587",
|
|
SMTPUser: "user",
|
|
SMTPPassword: "pass",
|
|
ToEmail: "to@example.com",
|
|
})
|
|
|
|
// Invalid data - missing email
|
|
data := &ContactFormData{
|
|
Message: "Test message that is long enough.",
|
|
}
|
|
|
|
err := service.SendContactForm(data)
|
|
|
|
if err == nil {
|
|
t.Error("SendContactForm should return error for invalid data")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "validation failed") {
|
|
t.Errorf("Error should mention validation failure: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendMultipartEmail_MissingConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *Config
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "Missing SMTP host",
|
|
config: &Config{SMTPPort: "587", SMTPUser: "user", SMTPPassword: "pass", ToEmail: "to@example.com"},
|
|
wantErr: "SMTP configuration incomplete",
|
|
},
|
|
{
|
|
name: "Missing SMTP port",
|
|
config: &Config{SMTPHost: "smtp.example.com", SMTPUser: "user", SMTPPassword: "pass", ToEmail: "to@example.com"},
|
|
wantErr: "SMTP configuration incomplete",
|
|
},
|
|
{
|
|
name: "Missing SMTP user",
|
|
config: &Config{SMTPHost: "smtp.example.com", SMTPPort: "587", SMTPPassword: "pass", ToEmail: "to@example.com"},
|
|
wantErr: "SMTP credentials missing",
|
|
},
|
|
{
|
|
name: "Missing SMTP password",
|
|
config: &Config{SMTPHost: "smtp.example.com", SMTPPort: "587", SMTPUser: "user", ToEmail: "to@example.com"},
|
|
wantErr: "SMTP credentials missing",
|
|
},
|
|
{
|
|
name: "Missing recipient email",
|
|
config: &Config{SMTPHost: "smtp.example.com", SMTPPort: "587", SMTPUser: "user", SMTPPassword: "pass"},
|
|
wantErr: "recipient email not configured",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
service := NewService(tt.config)
|
|
err := service.sendMultipartEmail("Subject", "<html>", "text", "reply@example.com")
|
|
|
|
if err == nil {
|
|
t.Error("sendMultipartEmail should return error for incomplete config")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Errorf("Error = %v, want error containing %q", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCVThemeCSS(t *testing.T) {
|
|
css := CVThemeCSS()
|
|
|
|
if css == "" {
|
|
t.Error("CVThemeCSS should return non-empty CSS")
|
|
}
|
|
|
|
// Check for some expected CSS properties
|
|
if !strings.Contains(css, "font-family") {
|
|
t.Error("CSS should contain font-family")
|
|
}
|
|
if !strings.Contains(css, "color") {
|
|
t.Error("CSS should contain color definitions")
|
|
}
|
|
}
|
|
|
|
func TestContactEmailHTMLTemplate(t *testing.T) {
|
|
template := ContactEmailHTMLTemplate()
|
|
|
|
if template == "" {
|
|
t.Error("ContactEmailHTMLTemplate should return non-empty template")
|
|
}
|
|
|
|
// Check for template variables
|
|
if !strings.Contains(template, "{{.Name}}") {
|
|
t.Error("Template should contain {{.Name}}")
|
|
}
|
|
if !strings.Contains(template, "{{.Email}}") {
|
|
t.Error("Template should contain {{.Email}}")
|
|
}
|
|
if !strings.Contains(template, "{{.Message}}") {
|
|
t.Error("Template should contain {{.Message}}")
|
|
}
|
|
}
|