package services import ( "fmt" "net/smtp" "regexp" "strings" "testing" ) // MockSMTPSender is a mock SMTP sender for testing type MockSMTPSender struct { SentEmails []SentEmail ShouldFail bool FailError error } // SentEmail represents a sent email for testing type SentEmail struct { To []string Subject string Body string } // SendMail is a mock implementation of smtp.SendMail func (m *MockSMTPSender) SendMail(addr string, auth smtp.Auth, from string, to []string, msg []byte) error { if m.ShouldFail { return m.FailError } // Parse the email to extract subject and body msgStr := string(msg) subject := extractSubject(msgStr) m.SentEmails = append(m.SentEmails, SentEmail{ To: to, Subject: subject, Body: msgStr, }) return nil } // extractSubject extracts the subject from an email message func extractSubject(msg string) string { lines := strings.Split(msg, "\r\n") for _, line := range lines { if strings.HasPrefix(line, "Subject: ") { return strings.TrimPrefix(line, "Subject: ") } } return "" } // TestEmailService_SendEmail tests basic email sending func TestEmailService_SendEmail(t *testing.T) { tests := []struct { name string to string subject string htmlBody string textBody string shouldFail bool expectError bool }{ { name: "valid email", to: "user@example.com", subject: "Test Email", htmlBody: "

Hello

World

", textBody: "Hello\nWorld", shouldFail: false, expectError: false, }, { name: "email with special characters", to: "user+test@example.com", subject: "Test: Öäü Special Characters", htmlBody: "

Special: €£¥

", textBody: "Special: €£¥", shouldFail: false, expectError: false, }, { name: "SMTP failure", to: "user@example.com", subject: "Test", htmlBody: "

Test

", textBody: "Test", shouldFail: true, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Validate email format isValidEmail := strings.Contains(tt.to, "@") && strings.Contains(tt.to, ".") if !isValidEmail && !tt.expectError { t.Error("Invalid email format should produce error") } // Validate subject is not empty if tt.subject == "" && !tt.expectError { t.Error("Empty subject should produce error") } // Validate body content exists if (tt.htmlBody == "" && tt.textBody == "") && !tt.expectError { t.Error("Both bodies empty should produce error") } // Simulate SMTP send var err error if tt.shouldFail { err = fmt.Errorf("SMTP error: connection refused") } if tt.expectError && err == nil { t.Error("Expected error, got nil") } if !tt.expectError && err != nil { t.Errorf("Expected no error, got %v", err) } }) } } // TestEmailService_SendVerificationEmail tests verification email sending func TestEmailService_SendVerificationEmail(t *testing.T) { tests := []struct { name string to string userName string token string expectError bool }{ { name: "valid verification email", to: "newuser@example.com", userName: "Max Mustermann", token: "abc123def456", expectError: false, }, { name: "user without name", to: "user@example.com", userName: "", token: "token123", expectError: false, }, { name: "empty token", to: "user@example.com", userName: "Test User", token: "", expectError: true, }, { name: "invalid email", to: "invalid-email", userName: "Test", token: "token123", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Validate inputs var err error if tt.token == "" { err = &ValidationError{Field: "token", Message: "required"} } else if !strings.Contains(tt.to, "@") { err = &ValidationError{Field: "email", Message: "invalid format"} } // Build verification link if tt.token != "" { verifyLink := fmt.Sprintf("https://example.com/verify-email?token=%s", tt.token) if verifyLink == "" { t.Error("Verification link should not be empty") } // Verify link contains token if !strings.Contains(verifyLink, tt.token) { t.Error("Verification link should contain token") } } if tt.expectError && err == nil { t.Error("Expected error, got nil") } if !tt.expectError && err != nil { t.Errorf("Expected no error, got %v", err) } }) } } // TestEmailService_SendPasswordResetEmail tests password reset email func TestEmailService_SendPasswordResetEmail(t *testing.T) { tests := []struct { name string to string userName string token string expectError bool }{ { name: "valid password reset", to: "user@example.com", userName: "John Doe", token: "reset-token-123", expectError: false, }, { name: "empty token", to: "user@example.com", userName: "John Doe", token: "", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var err error if tt.token == "" { err = &ValidationError{Field: "token", Message: "required"} } // Build reset link if tt.token != "" { resetLink := fmt.Sprintf("https://example.com/reset-password?token=%s", tt.token) // Verify link is secure (HTTPS) if !strings.HasPrefix(resetLink, "https://") { t.Error("Reset link should use HTTPS") } } if tt.expectError && err == nil { t.Error("Expected error, got nil") } if !tt.expectError && err != nil { t.Errorf("Expected no error, got %v", err) } }) } } // TestEmailService_Send2FAEmail tests 2FA notification emails func TestEmailService_Send2FAEmail(t *testing.T) { tests := []struct { name string to string action string expectError bool }{ { name: "2FA enabled notification", to: "user@example.com", action: "enabled", expectError: false, }, { name: "2FA disabled notification", to: "user@example.com", action: "disabled", expectError: false, }, { name: "invalid action", to: "user@example.com", action: "invalid", expectError: true, }, } validActions := map[string]bool{ "enabled": true, "disabled": true, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var err error if !validActions[tt.action] { err = &ValidationError{Field: "action", Message: "must be 'enabled' or 'disabled'"} } if tt.expectError && err == nil { t.Error("Expected error, got nil") } if !tt.expectError && err != nil { t.Errorf("Expected no error, got %v", err) } }) } } // TestEmailService_SendConsentReminderEmail tests consent reminder func TestEmailService_SendConsentReminderEmail(t *testing.T) { tests := []struct { name string to string documentName string daysLeft int expectError bool }{ { name: "reminder with 7 days left", to: "user@example.com", documentName: "Terms of Service", daysLeft: 7, expectError: false, }, { name: "reminder with 1 day left", to: "user@example.com", documentName: "Privacy Policy", daysLeft: 1, expectError: false, }, { name: "urgent reminder - overdue", to: "user@example.com", documentName: "Terms", daysLeft: 0, expectError: false, }, { name: "empty document name", to: "user@example.com", documentName: "", daysLeft: 7, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var err error if tt.documentName == "" { err = &ValidationError{Field: "document name", Message: "required"} } // Check urgency level var urgency string if tt.daysLeft <= 0 { urgency = "critical" } else if tt.daysLeft <= 3 { urgency = "urgent" } else { urgency = "normal" } if urgency == "" && !tt.expectError { t.Error("Urgency should be set") } if tt.expectError && err == nil { t.Error("Expected error, got nil") } if !tt.expectError && err != nil { t.Errorf("Expected no error, got %v", err) } }) } } // TestEmailService_MIMEFormatting tests MIME message formatting func TestEmailService_MIMEFormatting(t *testing.T) { tests := []struct { name string htmlBody string textBody string checkFor []string }{ { name: "multipart alternative", htmlBody: "

Test

", textBody: "Test", checkFor: []string{ "MIME-Version: 1.0", "Content-Type: multipart/alternative", "Content-Type: text/plain", "Content-Type: text/html", }, }, { name: "UTF-8 encoding", htmlBody: "

Öäü

", textBody: "Öäü", checkFor: []string{ "charset=\"UTF-8\"", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Build MIME message (simplified) message := fmt.Sprintf("MIME-Version: 1.0\r\n"+ "Content-Type: multipart/alternative; boundary=\"boundary\"\r\n"+ "\r\n"+ "--boundary\r\n"+ "Content-Type: text/plain; charset=\"UTF-8\"\r\n"+ "\r\n%s\r\n"+ "--boundary\r\n"+ "Content-Type: text/html; charset=\"UTF-8\"\r\n"+ "\r\n%s\r\n"+ "--boundary--\r\n", tt.textBody, tt.htmlBody) // Verify required headers are present for _, required := range tt.checkFor { if !strings.Contains(message, required) { t.Errorf("Message should contain '%s'", required) } } // Verify both bodies are included if !strings.Contains(message, tt.textBody) { t.Error("Message should contain text body") } if !strings.Contains(message, tt.htmlBody) { t.Error("Message should contain HTML body") } }) } } // TestEmailService_TemplateRendering tests email template rendering func TestEmailService_TemplateRendering(t *testing.T) { tests := []struct { name string template string variables map[string]string expectVars []string }{ { name: "verification template", template: "verification", variables: map[string]string{ "Name": "John Doe", "VerifyLink": "https://example.com/verify?token=abc", }, expectVars: []string{"John Doe", "https://example.com/verify?token=abc"}, }, { name: "password reset template", template: "password_reset", variables: map[string]string{ "Name": "Jane Smith", "ResetLink": "https://example.com/reset?token=xyz", }, expectVars: []string{"Jane Smith", "https://example.com/reset?token=xyz"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Simulate template rendering rendered := fmt.Sprintf("Hello %s, please visit %s", tt.variables["Name"], getLink(tt.variables)) // Verify all variables are in rendered output for _, expectedVar := range tt.expectVars { if !strings.Contains(rendered, expectedVar) { t.Errorf("Rendered template should contain '%s'", expectedVar) } } }) } } // TestEmailService_EmailValidation tests email address validation func TestEmailService_EmailValidation(t *testing.T) { tests := []struct { email string isValid bool }{ {"user@example.com", true}, {"user+tag@example.com", true}, {"user.name@example.co.uk", true}, {"user@subdomain.example.com", true}, {"invalid", false}, {"@example.com", false}, {"user@", false}, {"user@.com", false}, {"", false}, } for _, tt := range tests { t.Run(tt.email, func(t *testing.T) { // RFC 5322 compliant email validation pattern emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) isValid := emailRegex.MatchString(tt.email) if isValid != tt.isValid { t.Errorf("Email %s: expected valid=%v, got %v", tt.email, tt.isValid, isValid) } }) } } // TestEmailService_SMTPConfig tests SMTP configuration func TestEmailService_SMTPConfig(t *testing.T) { tests := []struct { name string config EmailConfig expectError bool }{ { name: "valid config", config: EmailConfig{ Host: "smtp.example.com", Port: 587, Username: "user@example.com", Password: "password", FromName: "BreakPilot", FromAddr: "noreply@example.com", BaseURL: "https://example.com", }, expectError: false, }, { name: "missing host", config: EmailConfig{ Port: 587, Username: "user@example.com", Password: "password", }, expectError: true, }, { name: "invalid port", config: EmailConfig{ Host: "smtp.example.com", Port: 0, Username: "user@example.com", Password: "password", }, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var err error if tt.config.Host == "" { err = &ValidationError{Field: "host", Message: "required"} } else if tt.config.Port <= 0 || tt.config.Port > 65535 { err = &ValidationError{Field: "port", Message: "must be between 1 and 65535"} } if tt.expectError && err == nil { t.Error("Expected error, got nil") } if !tt.expectError && err != nil { t.Errorf("Expected no error, got %v", err) } }) } } // TestEmailService_RateLimiting tests email rate limiting logic func TestEmailService_RateLimiting(t *testing.T) { tests := []struct { name string emailsSent int timeWindow int // minutes limit int expectThrottle bool }{ { name: "under limit", emailsSent: 5, timeWindow: 60, limit: 10, expectThrottle: false, }, { name: "at limit", emailsSent: 10, timeWindow: 60, limit: 10, expectThrottle: false, }, { name: "over limit", emailsSent: 15, timeWindow: 60, limit: 10, expectThrottle: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { shouldThrottle := tt.emailsSent > tt.limit if shouldThrottle != tt.expectThrottle { t.Errorf("Expected throttle=%v, got %v", tt.expectThrottle, shouldThrottle) } }) } } // Helper functions func getLink(vars map[string]string) string { if link, ok := vars["VerifyLink"]; ok { return link } if link, ok := vars["ResetLink"]; ok { return link } return "" }