This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/services/email_service_test.go
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

625 lines
14 KiB
Go

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: "<h1>Hello</h1><p>World</p>",
textBody: "Hello\nWorld",
shouldFail: false,
expectError: false,
},
{
name: "email with special characters",
to: "user+test@example.com",
subject: "Test: Öäü Special Characters",
htmlBody: "<p>Special: €£¥</p>",
textBody: "Special: €£¥",
shouldFail: false,
expectError: false,
},
{
name: "SMTP failure",
to: "user@example.com",
subject: "Test",
htmlBody: "<p>Test</p>",
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: "<h1>Test</h1>",
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: "<p>Öäü</p>",
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 ""
}