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", "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", "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", "", "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}}") } }