package services import ( "regexp" "strings" "testing" "github.com/breakpilot/consent-service/internal/models" ) // ======================================== // Test All 19 Email Categories // ======================================== // TestEmailTemplateService_GetDefaultTemplateContent tests default content generation for each email type func TestEmailTemplateService_GetDefaultTemplateContent(t *testing.T) { service := &EmailTemplateService{} // All 19 email categories tests := []struct { name string emailType string language string wantSubject bool wantBodyHTML bool wantBodyText bool }{ // Auth Lifecycle (10 types) {"welcome_de", models.EmailTypeWelcome, "de", true, true, true}, {"email_verification_de", models.EmailTypeEmailVerification, "de", true, true, true}, {"password_reset_de", models.EmailTypePasswordReset, "de", true, true, true}, {"password_changed_de", models.EmailTypePasswordChanged, "de", true, true, true}, {"2fa_enabled_de", models.EmailType2FAEnabled, "de", true, true, true}, {"2fa_disabled_de", models.EmailType2FADisabled, "de", true, true, true}, {"new_device_login_de", models.EmailTypeNewDeviceLogin, "de", true, true, true}, {"suspicious_activity_de", models.EmailTypeSuspiciousActivity, "de", true, true, true}, {"account_locked_de", models.EmailTypeAccountLocked, "de", true, true, true}, {"account_unlocked_de", models.EmailTypeAccountUnlocked, "de", true, true, true}, // GDPR/Privacy (5 types) {"deletion_requested_de", models.EmailTypeDeletionRequested, "de", true, true, true}, {"deletion_confirmed_de", models.EmailTypeDeletionConfirmed, "de", true, true, true}, {"data_export_ready_de", models.EmailTypeDataExportReady, "de", true, true, true}, {"email_changed_de", models.EmailTypeEmailChanged, "de", true, true, true}, {"email_change_verify_de", models.EmailTypeEmailChangeVerify, "de", true, true, true}, // Consent Management (4 types) {"new_version_published_de", models.EmailTypeNewVersionPublished, "de", true, true, true}, {"consent_reminder_de", models.EmailTypeConsentReminder, "de", true, true, true}, {"consent_deadline_warning_de", models.EmailTypeConsentDeadlineWarning, "de", true, true, true}, {"account_suspended_de", models.EmailTypeAccountSuspended, "de", true, true, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { subject, bodyHTML, bodyText := service.GetDefaultTemplateContent(tt.emailType, tt.language) if tt.wantSubject && subject == "" { t.Errorf("GetDefaultTemplateContent(%s, %s): expected subject, got empty string", tt.emailType, tt.language) } if tt.wantBodyHTML && bodyHTML == "" { t.Errorf("GetDefaultTemplateContent(%s, %s): expected bodyHTML, got empty string", tt.emailType, tt.language) } if tt.wantBodyText && bodyText == "" { t.Errorf("GetDefaultTemplateContent(%s, %s): expected bodyText, got empty string", tt.emailType, tt.language) } }) } } // TestEmailTemplateService_GetDefaultTemplateContent_UnknownType tests default content for unknown type func TestEmailTemplateService_GetDefaultTemplateContent_UnknownType(t *testing.T) { service := &EmailTemplateService{} subject, bodyHTML, bodyText := service.GetDefaultTemplateContent("unknown_type", "de") // The service returns a fallback for unknown types if subject == "" { t.Errorf("GetDefaultTemplateContent(unknown_type, de): expected fallback subject, got empty") } if bodyHTML == "" { t.Errorf("GetDefaultTemplateContent(unknown_type, de): expected fallback bodyHTML, got empty") } if bodyText == "" { t.Errorf("GetDefaultTemplateContent(unknown_type, de): expected fallback bodyText, got empty") } } // TestEmailTemplateService_GetDefaultTemplateContent_UnsupportedLanguage tests fallback for unsupported language func TestEmailTemplateService_GetDefaultTemplateContent_UnsupportedLanguage(t *testing.T) { service := &EmailTemplateService{} // Test with unsupported language - should return fallback subject, bodyHTML, bodyText := service.GetDefaultTemplateContent(models.EmailTypeWelcome, "fr") // Should return fallback (not empty, but generic) if subject == "" || bodyHTML == "" || bodyText == "" { t.Error("GetDefaultTemplateContent should return fallback for unsupported language") } } // TestReplaceVariables tests variable replacement in templates func TestReplaceVariables(t *testing.T) { tests := []struct { name string template string variables map[string]string expected string }{ { name: "single variable", template: "Hallo {{user_name}}!", variables: map[string]string{"user_name": "Max"}, expected: "Hallo Max!", }, { name: "multiple variables", template: "Hallo {{user_name}}, klicken Sie hier: {{reset_link}}", variables: map[string]string{"user_name": "Max", "reset_link": "https://example.com"}, expected: "Hallo Max, klicken Sie hier: https://example.com", }, { name: "no variables", template: "Hallo Welt!", variables: map[string]string{}, expected: "Hallo Welt!", }, { name: "missing variable - not replaced", template: "Hallo {{user_name}} und {{missing}}!", variables: map[string]string{"user_name": "Max"}, expected: "Hallo Max und {{missing}}!", }, { name: "empty template", template: "", variables: map[string]string{"user_name": "Max"}, expected: "", }, { name: "variable with special characters", template: "IP: {{ip_address}}", variables: map[string]string{"ip_address": "192.168.1.1"}, expected: "IP: 192.168.1.1", }, { name: "variable with URL", template: "Link: {{verification_url}}", variables: map[string]string{"verification_url": "https://example.com/verify?token=abc123&user=test"}, expected: "Link: https://example.com/verify?token=abc123&user=test", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := testReplaceVariables(tt.template, tt.variables) if result != tt.expected { t.Errorf("replaceVariables() = %s, want %s", result, tt.expected) } }) } } // testReplaceVariables is a test helper function for variable replacement func testReplaceVariables(template string, variables map[string]string) string { result := template for key, value := range variables { placeholder := "{{" + key + "}}" for i := 0; i < len(result); i++ { idx := testFindSubstring(result, placeholder) if idx == -1 { break } result = result[:idx] + value + result[idx+len(placeholder):] } } return result } func testFindSubstring(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return i } } return -1 } // TestEmailTypeConstantsExist verifies that all expected email types are defined func TestEmailTypeConstantsExist(t *testing.T) { // Test that all 19 email type constants are defined and produce non-empty templates types := []string{ // Auth Lifecycle models.EmailTypeWelcome, models.EmailTypeEmailVerification, models.EmailTypePasswordReset, models.EmailTypePasswordChanged, models.EmailType2FAEnabled, models.EmailType2FADisabled, models.EmailTypeNewDeviceLogin, models.EmailTypeSuspiciousActivity, models.EmailTypeAccountLocked, models.EmailTypeAccountUnlocked, // GDPR/Privacy models.EmailTypeDeletionRequested, models.EmailTypeDeletionConfirmed, models.EmailTypeDataExportReady, models.EmailTypeEmailChanged, models.EmailTypeEmailChangeVerify, // Consent Management models.EmailTypeNewVersionPublished, models.EmailTypeConsentReminder, models.EmailTypeConsentDeadlineWarning, models.EmailTypeAccountSuspended, } service := &EmailTemplateService{} for _, emailType := range types { t.Run(emailType, func(t *testing.T) { subject, bodyHTML, _ := service.GetDefaultTemplateContent(emailType, "de") if subject == "" { t.Errorf("Email type %s has no default subject", emailType) } if bodyHTML == "" { t.Errorf("Email type %s has no default body HTML", emailType) } }) } // Verify we have exactly 19 types if len(types) != 19 { t.Errorf("Expected 19 email types, got %d", len(types)) } } // TestEmailTemplateService_ValidateTemplateContent tests template content validation func TestEmailTemplateService_ValidateTemplateContent(t *testing.T) { tests := []struct { name string subject string bodyHTML string wantError bool }{ { name: "valid content", subject: "Test Subject", bodyHTML: "
Test Body
", wantError: false, }, { name: "empty subject", subject: "", bodyHTML: "Test Body
", wantError: true, }, { name: "empty body", subject: "Test Subject", bodyHTML: "", wantError: true, }, { name: "both empty", subject: "", bodyHTML: "", wantError: true, }, { name: "whitespace only subject", subject: " ", bodyHTML: "Test Body
", wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := testValidateTemplateContent(tt.subject, tt.bodyHTML) if (err != nil) != tt.wantError { t.Errorf("validateTemplateContent() error = %v, wantError %v", err, tt.wantError) } }) } } // testValidateTemplateContent is a test helper function to validate template content func testValidateTemplateContent(subject, bodyHTML string) error { if strings.TrimSpace(subject) == "" { return &templateValidationError{Field: "subject", Message: "subject is required"} } if strings.TrimSpace(bodyHTML) == "" { return &templateValidationError{Field: "body_html", Message: "body_html is required"} } return nil } // templateValidationError represents a validation error in email templates type templateValidationError struct { Field string Message string } func (e *templateValidationError) Error() string { return e.Field + ": " + e.Message } // TestGetTestVariablesForType tests that test variables are properly generated for each email type func TestGetTestVariablesForType(t *testing.T) { tests := []struct { emailType string expectedVars []string }{ // Auth Lifecycle {models.EmailTypeWelcome, []string{"user_name", "app_name"}}, {models.EmailTypeEmailVerification, []string{"user_name", "verification_url"}}, {models.EmailTypePasswordReset, []string{"reset_url"}}, {models.EmailTypePasswordChanged, []string{"user_name", "changed_at"}}, {models.EmailType2FAEnabled, []string{"user_name", "enabled_at"}}, {models.EmailType2FADisabled, []string{"user_name", "disabled_at"}}, {models.EmailTypeNewDeviceLogin, []string{"device", "location", "ip_address", "login_time"}}, {models.EmailTypeSuspiciousActivity, []string{"activity_type", "activity_time"}}, {models.EmailTypeAccountLocked, []string{"locked_at", "reason"}}, {models.EmailTypeAccountUnlocked, []string{"unlocked_at"}}, // GDPR/Privacy {models.EmailTypeDeletionRequested, []string{"deletion_date", "cancel_url"}}, {models.EmailTypeDeletionConfirmed, []string{"deleted_at"}}, {models.EmailTypeDataExportReady, []string{"download_url", "expires_in"}}, {models.EmailTypeEmailChanged, []string{"old_email", "new_email"}}, // Consent Management {models.EmailTypeNewVersionPublished, []string{"document_name", "version"}}, {models.EmailTypeConsentReminder, []string{"document_name", "days_left"}}, {models.EmailTypeConsentDeadlineWarning, []string{"document_name", "hours_left"}}, {models.EmailTypeAccountSuspended, []string{"suspended_at", "reason"}}, } for _, tt := range tests { t.Run(tt.emailType, func(t *testing.T) { vars := getTestVariablesForType(tt.emailType) for _, expected := range tt.expectedVars { if _, ok := vars[expected]; !ok { t.Errorf("getTestVariablesForType(%s) missing variable %s", tt.emailType, expected) } } }) } } // getTestVariablesForType returns test variables for a given email type func getTestVariablesForType(emailType string) map[string]string { // Common variables vars := map[string]string{ "user_name": "Max Mustermann", "user_email": "max@example.com", "app_name": "BreakPilot", "app_url": "https://breakpilot.app", "support_url": "https://breakpilot.app/support", "support_email": "support@breakpilot.app", "security_url": "https://breakpilot.app/security", "login_url": "https://breakpilot.app/login", } switch emailType { case models.EmailTypeEmailVerification: vars["verification_url"] = "https://breakpilot.app/verify?token=xyz789" vars["verification_code"] = "ABC123" vars["expires_in"] = "24 Stunden" case models.EmailTypePasswordReset: vars["reset_url"] = "https://breakpilot.app/reset?token=abc123" vars["reset_code"] = "RST456" vars["expires_in"] = "1 Stunde" vars["ip_address"] = "192.168.1.1" case models.EmailTypePasswordChanged: vars["changed_at"] = "14.12.2025 15:30 Uhr" vars["ip_address"] = "192.168.1.1" vars["device_info"] = "Chrome auf MacOS" case models.EmailType2FAEnabled: vars["enabled_at"] = "14.12.2025 15:30 Uhr" vars["device_info"] = "Chrome auf MacOS" case models.EmailType2FADisabled: vars["disabled_at"] = "14.12.2025 15:30 Uhr" vars["ip_address"] = "192.168.1.1" case models.EmailTypeNewDeviceLogin: vars["device"] = "Chrome auf MacOS" vars["device_info"] = "Chrome auf MacOS" vars["location"] = "Berlin, Deutschland" vars["ip_address"] = "192.168.1.1" vars["login_time"] = "14.12.2025 15:30 Uhr" case models.EmailTypeSuspiciousActivity: vars["activity_type"] = "Mehrere fehlgeschlagene Logins" vars["activity_time"] = "14.12.2025 15:30 Uhr" vars["ip_address"] = "192.168.1.1" case models.EmailTypeAccountLocked: vars["locked_at"] = "14.12.2025 15:30 Uhr" vars["reason"] = "Zu viele fehlgeschlagene Login-Versuche" vars["unlock_time"] = "14.12.2025 16:30 Uhr" case models.EmailTypeAccountUnlocked: vars["unlocked_at"] = "14.12.2025 16:30 Uhr" case models.EmailTypeDeletionRequested: vars["requested_at"] = "14.12.2025 15:30 Uhr" vars["deletion_date"] = "14.01.2026" vars["cancel_url"] = "https://breakpilot.app/cancel-deletion?token=del123" vars["data_info"] = "Profildaten, Consent-Historie, Audit-Logs" case models.EmailTypeDeletionConfirmed: vars["deleted_at"] = "14.01.2026 00:00 Uhr" vars["feedback_url"] = "https://breakpilot.app/feedback" case models.EmailTypeDataExportReady: vars["download_url"] = "https://breakpilot.app/download/export123" vars["expires_in"] = "7 Tage" vars["file_size"] = "2.5 MB" case models.EmailTypeEmailChanged: vars["old_email"] = "old@example.com" vars["new_email"] = "new@example.com" vars["changed_at"] = "14.12.2025 15:30 Uhr" case models.EmailTypeEmailChangeVerify: vars["new_email"] = "new@example.com" vars["verification_url"] = "https://breakpilot.app/verify-email?token=ver123" vars["expires_in"] = "24 Stunden" case models.EmailTypeNewVersionPublished: vars["document_name"] = "Datenschutzerklärung" vars["document_type"] = "privacy" vars["version"] = "2.0.0" vars["consent_url"] = "https://breakpilot.app/consent" vars["deadline"] = "31.12.2025" case models.EmailTypeConsentReminder: vars["document_name"] = "Nutzungsbedingungen" vars["days_left"] = "7" vars["consent_url"] = "https://breakpilot.app/consent" vars["deadline"] = "21.12.2025" case models.EmailTypeConsentDeadlineWarning: vars["document_name"] = "Nutzungsbedingungen" vars["hours_left"] = "24 Stunden" vars["consent_url"] = "https://breakpilot.app/consent" vars["consequences"] = "Ihr Konto wird temporär suspendiert." case models.EmailTypeAccountSuspended: vars["suspended_at"] = "14.12.2025 15:30 Uhr" vars["reason"] = "Fehlende Zustimmung zu Pflichtdokumenten" vars["documents"] = "- Nutzungsbedingungen v2.0\n- Datenschutzerklärung v3.0" vars["consent_url"] = "https://breakpilot.app/consent" } return vars } // TestEmailTemplateService_HTMLEscape tests that HTML is properly escaped in text version func TestEmailTemplateService_HTMLEscape(t *testing.T) { tests := []struct { name string html string expected string }{ { name: "simple paragraph", html: "Hello World
", expected: "Hello World", }, { name: "link", html: `Click here`, expected: "Click here", }, { name: "bold text", html: "Important", expected: "Important", }, { name: "nested tags", html: "Nested text
Paragraph
", expected: "TitleParagraph", }, { name: "self-closing tag", html: "Line1This is a test paragraph with links.