A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
699 lines
23 KiB
Go
699 lines
23 KiB
Go
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: "<p>Test Body</p>",
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "empty subject",
|
|
subject: "",
|
|
bodyHTML: "<p>Test Body</p>",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "empty body",
|
|
subject: "Test Subject",
|
|
bodyHTML: "",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "both empty",
|
|
subject: "",
|
|
bodyHTML: "",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "whitespace only subject",
|
|
subject: " ",
|
|
bodyHTML: "<p>Test Body</p>",
|
|
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: "<p>Hello World</p>",
|
|
expected: "Hello World",
|
|
},
|
|
{
|
|
name: "link",
|
|
html: `<a href="https://example.com">Click here</a>`,
|
|
expected: "Click here",
|
|
},
|
|
{
|
|
name: "bold text",
|
|
html: "<strong>Important</strong>",
|
|
expected: "Important",
|
|
},
|
|
{
|
|
name: "nested tags",
|
|
html: "<div><p><strong>Nested</strong> text</p></div>",
|
|
expected: "Nested text",
|
|
},
|
|
{
|
|
name: "multiple tags",
|
|
html: "<h1>Title</h1><p>Paragraph</p>",
|
|
expected: "TitleParagraph",
|
|
},
|
|
{
|
|
name: "self-closing tag",
|
|
html: "Line1<br/>Line2",
|
|
expected: "Line1Line2",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := stripHTMLTags(tt.html)
|
|
if result != tt.expected {
|
|
t.Errorf("stripHTMLTags() = %s, want %s", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// stripHTMLTags removes HTML tags from a string
|
|
func stripHTMLTags(html string) string {
|
|
result := ""
|
|
inTag := false
|
|
for _, r := range html {
|
|
if r == '<' {
|
|
inTag = true
|
|
continue
|
|
}
|
|
if r == '>' {
|
|
inTag = false
|
|
continue
|
|
}
|
|
if !inTag {
|
|
result += string(r)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// TestEmailTemplateService_AllTemplatesHaveVariables tests that all templates define their required variables
|
|
func TestEmailTemplateService_AllTemplatesHaveVariables(t *testing.T) {
|
|
service := &EmailTemplateService{}
|
|
templateTypes := service.GetAllTemplateTypes()
|
|
|
|
for _, tt := range templateTypes {
|
|
t.Run(tt.TemplateType, func(t *testing.T) {
|
|
// Get default template content
|
|
subject, bodyHTML, bodyText := service.GetDefaultTemplateContent(tt.TemplateType, "de")
|
|
|
|
// Check that variables defined in template type are present in the content
|
|
for _, varName := range tt.Variables {
|
|
placeholder := "{{" + varName + "}}"
|
|
foundInSubject := strings.Contains(subject, placeholder)
|
|
foundInHTML := strings.Contains(bodyHTML, placeholder)
|
|
foundInText := strings.Contains(bodyText, placeholder)
|
|
|
|
// Variable should be present in at least one of subject, HTML or text
|
|
if !foundInSubject && !foundInHTML && !foundInText {
|
|
// Note: This is a warning, not an error, as some variables might be optional
|
|
t.Logf("Warning: Variable %s defined for %s but not found in template content", varName, tt.TemplateType)
|
|
}
|
|
}
|
|
|
|
// Check that all variables in content are defined
|
|
re := regexp.MustCompile(`\{\{(\w+)\}\}`)
|
|
allMatches := re.FindAllStringSubmatch(subject+bodyHTML+bodyText, -1)
|
|
definedVars := make(map[string]bool)
|
|
for _, v := range tt.Variables {
|
|
definedVars[v] = true
|
|
}
|
|
|
|
for _, match := range allMatches {
|
|
if len(match) > 1 {
|
|
varName := match[1]
|
|
if !definedVars[varName] {
|
|
t.Logf("Warning: Variable {{%s}} found in template but not defined in variables list for %s", varName, tt.TemplateType)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEmailTemplateService_TemplateVariableDescriptions tests that all variables have descriptions
|
|
func TestEmailTemplateService_TemplateVariableDescriptions(t *testing.T) {
|
|
service := &EmailTemplateService{}
|
|
templateTypes := service.GetAllTemplateTypes()
|
|
|
|
for _, tt := range templateTypes {
|
|
t.Run(tt.TemplateType, func(t *testing.T) {
|
|
for _, varName := range tt.Variables {
|
|
if desc, ok := tt.Descriptions[varName]; !ok || desc == "" {
|
|
t.Errorf("Variable %s in %s has no description", varName, tt.TemplateType)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEmailTemplateService_GermanTemplatesAreComplete tests that all German templates are fully translated
|
|
func TestEmailTemplateService_GermanTemplatesAreComplete(t *testing.T) {
|
|
service := &EmailTemplateService{}
|
|
|
|
emailTypes := []string{
|
|
models.EmailTypeWelcome,
|
|
models.EmailTypeEmailVerification,
|
|
models.EmailTypePasswordReset,
|
|
models.EmailTypePasswordChanged,
|
|
models.EmailType2FAEnabled,
|
|
models.EmailType2FADisabled,
|
|
models.EmailTypeNewDeviceLogin,
|
|
models.EmailTypeSuspiciousActivity,
|
|
models.EmailTypeAccountLocked,
|
|
models.EmailTypeAccountUnlocked,
|
|
models.EmailTypeDeletionRequested,
|
|
models.EmailTypeDeletionConfirmed,
|
|
models.EmailTypeDataExportReady,
|
|
models.EmailTypeEmailChanged,
|
|
models.EmailTypeNewVersionPublished,
|
|
models.EmailTypeConsentReminder,
|
|
models.EmailTypeConsentDeadlineWarning,
|
|
models.EmailTypeAccountSuspended,
|
|
}
|
|
|
|
germanKeywords := []string{"Hallo", "freundlichen", "Grüßen", "BreakPilot", "Ihr"}
|
|
|
|
for _, emailType := range emailTypes {
|
|
t.Run(emailType, func(t *testing.T) {
|
|
subject, bodyHTML, bodyText := service.GetDefaultTemplateContent(emailType, "de")
|
|
|
|
// Check that German text is present
|
|
foundGerman := false
|
|
for _, keyword := range germanKeywords {
|
|
if strings.Contains(bodyHTML, keyword) || strings.Contains(bodyText, keyword) {
|
|
foundGerman = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundGerman {
|
|
t.Errorf("Template %s does not appear to be in German", emailType)
|
|
}
|
|
|
|
// Check that subject is not just the fallback
|
|
if subject == "No template" {
|
|
t.Errorf("Template %s has fallback subject instead of German subject", emailType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEmailTemplateService_HTMLStructure tests that HTML templates have valid structure
|
|
func TestEmailTemplateService_HTMLStructure(t *testing.T) {
|
|
service := &EmailTemplateService{}
|
|
|
|
emailTypes := []string{
|
|
models.EmailTypeWelcome,
|
|
models.EmailTypeEmailVerification,
|
|
models.EmailTypePasswordReset,
|
|
}
|
|
|
|
for _, emailType := range emailTypes {
|
|
t.Run(emailType, func(t *testing.T) {
|
|
_, bodyHTML, _ := service.GetDefaultTemplateContent(emailType, "de")
|
|
|
|
// Check for basic HTML structure
|
|
if !strings.Contains(bodyHTML, "<!DOCTYPE html>") {
|
|
t.Errorf("Template %s missing DOCTYPE", emailType)
|
|
}
|
|
if !strings.Contains(bodyHTML, "<html>") {
|
|
t.Errorf("Template %s missing <html> tag", emailType)
|
|
}
|
|
if !strings.Contains(bodyHTML, "</html>") {
|
|
t.Errorf("Template %s missing closing </html> tag", emailType)
|
|
}
|
|
if !strings.Contains(bodyHTML, "<body") {
|
|
t.Errorf("Template %s missing <body> tag", emailType)
|
|
}
|
|
if !strings.Contains(bodyHTML, "</body>") {
|
|
t.Errorf("Template %s missing closing </body> tag", emailType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkReplaceVariables benchmarks variable replacement
|
|
func BenchmarkReplaceVariables(b *testing.B) {
|
|
template := "Hallo {{user_name}}, Ihr Link: {{reset_url}}, gültig bis {{expires_in}}"
|
|
variables := map[string]string{
|
|
"user_name": "Max Mustermann",
|
|
"reset_url": "https://example.com/reset?token=abc123",
|
|
"expires_in": "24 Stunden",
|
|
}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
replaceVariables(template, variables)
|
|
}
|
|
}
|
|
|
|
// BenchmarkStripHTMLTags benchmarks HTML tag stripping
|
|
func BenchmarkStripHTMLTags(b *testing.B) {
|
|
html := "<html><body><h1>Title</h1><p>This is a <strong>test</strong> paragraph with <a href='#'>links</a>.</p></body></html>"
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
stripHTMLTags(html)
|
|
}
|
|
}
|