package validation import ( "strings" "testing" "time" ) // ============================================================================== // EMAIL VALIDATION TESTS // ============================================================================== func TestIsValidEmail(t *testing.T) { tests := []struct { name string email string want bool }{ // Valid emails {"Valid standard", "test@example.com", true}, {"Valid with subdomain", "user@mail.example.com", true}, {"Valid with plus", "user+tag@example.com", true}, {"Valid with dot", "first.last@example.com", true}, {"Valid with hyphen", "user-name@example.com", true}, {"Valid with numbers", "user123@example.com", true}, // Invalid emails {"Empty", "", false}, {"No @", "userexample.com", false}, {"Multiple @", "user@@example.com", false}, {"No domain", "user@", false}, {"No local part", "@example.com", false}, {"Spaces", "user @example.com", false}, {"Missing TLD", "user@example", false}, {"Too long", strings.Repeat("a", 250) + "@example.com", false}, {"Special chars", "user<>@example.com", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsValidEmail(tt.email); got != tt.want { t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want) } }) } } // ============================================================================== // EMAIL INJECTION TESTS // ============================================================================== func TestContainsEmailInjection(t *testing.T) { tests := []struct { name string input string want bool // true if injection detected }{ // Safe inputs {"Normal text", "Hello World", false}, {"Name with apostrophe", "O'Connor", false}, {"Hyphenated name", "Anne-Marie", false}, // Injection attempts {"CRLF injection", "Name\r\nBcc: attacker@evil.com", true}, {"LF injection", "Name\nBcc: attacker@evil.com", true}, {"Content-Type header", "Name\r\nContent-Type: text/html", true}, {"BCC header", "bcc: attacker@evil.com", true}, {"CC header", "cc: attacker@evil.com", true}, {"To header", "to: victim@example.com", true}, {"From header", "from: fake@example.com", true}, {"Reply-To header", "reply-to: attacker@evil.com", true}, {"MIME-Version", "MIME-Version: 1.0", true}, {"X-Mailer", "X-Mailer: Evil", true}, {"Case insensitive", "BCC: attacker@evil.com", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ContainsEmailInjection(tt.input); got != tt.want { t.Errorf("ContainsEmailInjection(%q) = %v, want %v", tt.input, got, tt.want) } }) } } // ============================================================================== // NAME VALIDATION TESTS // ============================================================================== func TestIsValidName(t *testing.T) { tests := []struct { name string input string want bool }{ // Valid names {"Simple name", "John", true}, {"Full name", "John Smith", true}, {"Hyphenated", "Anne-Marie", true}, {"Apostrophe", "O'Connor", true}, {"Multiple words", "Juan José García", true}, {"Spanish characters", "José María", true}, {"French characters", "François Dubois", true}, {"German characters", "Müller", true}, // Invalid names {"Empty", "", false}, {"Numbers", "John123", false}, {"Special chars", "John@Smith", false}, {"HTML tags", "", false}, {"Email", "john@example.com", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsValidName(tt.input); got != tt.want { t.Errorf("IsValidName(%q) = %v, want %v", tt.input, got, tt.want) } }) } } // ============================================================================== // SUBJECT VALIDATION TESTS // ============================================================================== func TestIsValidSubject(t *testing.T) { tests := []struct { name string input string want bool }{ // Valid subjects {"Simple", "Hello", true}, {"With spaces", "Hello World", true}, {"With punctuation", "Question about your services!", true}, {"With numbers", "Order #12345", true}, {"Complex", "Re: Your inquiry (urgent)", true}, // Invalid subjects {"Empty", "", false}, {"HTML tags", "", false}, {"Email injection", "Subject\nBcc: evil@example.com", false}, {"Special chars", "Subject $ % ^ &", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsValidSubject(tt.input); got != tt.want { t.Errorf("IsValidSubject(%q) = %v, want %v", tt.input, got, tt.want) } }) } } // ============================================================================== // COMPANY VALIDATION TESTS // ============================================================================== func TestIsValidCompany(t *testing.T) { tests := []struct { name string input string want bool }{ // Valid company names {"Empty (optional)", "", true}, {"Simple", "Acme Corp", true}, {"With punctuation", "Smith & Sons, Inc.", true}, {"With hyphen", "Coca-Cola", true}, {"With parentheses", "Example (Spain)", true}, // Invalid company names {"HTML tags", "\n\n\nHello World\n\n\n\n", } SanitizeContactForm(req) // Check whitespace trimmed if req.Name != "John Smith" { t.Errorf("Name not properly sanitized: %q", req.Name) } // Check newlines removed from headers if strings.Contains(req.Email, "\n") || strings.Contains(req.Email, "\r") { t.Errorf("Email still contains newlines: %q", req.Email) } // Check HTML escaped if !strings.Contains(req.Message, "<script>") { t.Errorf("Message not properly HTML escaped: %q", req.Message) } // Check whitespace normalized if strings.Contains(req.Message, " ") { t.Errorf("Message whitespace not normalized: %q", req.Message) } } // ============================================================================== // FULL VALIDATION TESTS // ============================================================================== func TestValidateContactForm(t *testing.T) { tests := []struct { name string req *ContactFormRequest wantError bool errorField string }{ { name: "Valid form", req: &ContactFormRequest{ Name: "John Smith", Email: "john@example.com", Company: "Acme Corp", Subject: "Inquiry", Message: "Hello, I have a question.", Honeypot: "", Timestamp: time.Now().Unix() - 10, // 10 seconds ago }, wantError: false, }, { name: "Bot detected - honeypot filled", req: &ContactFormRequest{ Name: "Bot", Email: "bot@example.com", Subject: "Spam", Message: "Spam message", Honeypot: "http://evil.com", // Bot filled this }, wantError: true, errorField: "website", }, { name: "Bot detected - submitted too fast", req: &ContactFormRequest{ Name: "John", Email: "john@example.com", Subject: "Test", Message: "Test", Timestamp: time.Now().Unix(), // Just now (< 2 seconds) }, wantError: true, errorField: "timestamp", }, { name: "Missing required field - name", req: &ContactFormRequest{ Name: "", Email: "john@example.com", Subject: "Test", Message: "Test", }, wantError: true, errorField: "name", }, { name: "Invalid email format", req: &ContactFormRequest{ Name: "John", Email: "invalid-email", Subject: "Test", Message: "Test", }, wantError: true, errorField: "email", }, { name: "Email injection attempt", req: &ContactFormRequest{ Name: "Evil\nBcc: attacker@evil.com", Email: "evil@example.com", Subject: "Test", Message: "Test", }, wantError: true, errorField: "name", }, { name: "Name too long", req: &ContactFormRequest{ Name: strings.Repeat("a", 101), Email: "john@example.com", Subject: "Test", Message: "Test", }, wantError: true, errorField: "name", }, { name: "Subject too long", req: &ContactFormRequest{ Name: "John", Email: "john@example.com", Subject: strings.Repeat("a", 201), Message: "Test", }, wantError: true, errorField: "subject", }, { name: "Message too long", req: &ContactFormRequest{ Name: "John", Email: "john@example.com", Subject: "Test", Message: strings.Repeat("a", 5001), }, wantError: true, errorField: "message", }, { name: "Invalid name characters", req: &ContactFormRequest{ Name: "John123", Email: "john@example.com", Subject: "Test", Message: "Test", }, wantError: true, errorField: "name", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateContactForm(tt.req) if tt.wantError { if err == nil { t.Errorf("Expected error but got none") return } // Check error is ValidationError valErr, ok := err.(*ValidationError) if !ok { t.Errorf("Expected ValidationError, got %T", err) return } // Check correct field if valErr.Field != tt.errorField { t.Errorf("Expected error field %q, got %q", tt.errorField, valErr.Field) } } else { if err != nil { t.Errorf("Expected no error, got: %v", err) } } }) } } // ============================================================================== // SECURITY TESTS (Attack Simulations) // ============================================================================== func TestSecurityAttacks(t *testing.T) { attacks := []struct { name string field string value string reason string }{ { name: "SQL Injection in name", field: "name", value: "Robert'; DROP TABLE users; --", reason: "Should reject SQL injection attempts", }, // NOTE: XSS in message is allowed during validation - it's escaped during sanitization // This is intentional - we don't reject valid messages that happen to contain < or > // The sanitization step handles HTML escaping before sending email { name: "Email header injection", field: "email", value: "test@example.com\nBcc: attacker@evil.com", reason: "Should block email header injection", }, { name: "Command injection", field: "name", value: "Test; rm -rf /", reason: "Should block command injection attempts", }, { name: "Path traversal", field: "subject", value: "../../../etc/passwd", reason: "Should reject path traversal attempts", }, } for _, attack := range attacks { t.Run(attack.name, func(t *testing.T) { req := &ContactFormRequest{ Name: "John Smith", Email: "john@example.com", Subject: "Test", Message: "Test", Timestamp: time.Now().Unix() - 10, } // Inject attack into specified field switch attack.field { case "name": req.Name = attack.value case "email": req.Email = attack.value case "subject": req.Subject = attack.value case "message": req.Message = attack.value } err := ValidateContactForm(req) if err == nil { t.Errorf("%s: Expected validation to fail, but it passed", attack.reason) } }) } } // ============================================================================== // NORMALIZATION TESTS // ============================================================================== func TestRemoveNewlines(t *testing.T) { tests := []struct { input string want string }{ {"No newlines", "No newlines"}, {"With\nnewline", "Withnewline"}, {"With\r\nCRLF", "WithCRLF"}, {"Multiple\n\n\nnewlines", "Multiplenewlines"}, } for _, tt := range tests { if got := removeNewlines(tt.input); got != tt.want { t.Errorf("removeNewlines(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestNormalizeWhitespace(t *testing.T) { tests := []struct { input string want string }{ {"Normal text", "Normal text"}, {"Multiple spaces", "Multiple spaces"}, {"Multiple\n\n\n\nnewlines", "Multiple\n\nnewlines"}, {" Leading and trailing ", "Leading and trailing"}, } for _, tt := range tests { if got := normalizeWhitespace(tt.input); got != tt.want { t.Errorf("normalizeWhitespace(%q) = %q, want %q", tt.input, got, tt.want) } } } // ============================================================================== // BENCHMARK TESTS // ============================================================================== func BenchmarkIsValidEmail(b *testing.B) { email := "test@example.com" for i := 0; i < b.N; i++ { IsValidEmail(email) } } func BenchmarkContainsEmailInjection(b *testing.B) { text := "Normal text without injection" for i := 0; i < b.N; i++ { ContainsEmailInjection(text) } } func BenchmarkValidateContactForm(b *testing.B) { req := &ContactFormRequest{ Name: "John Smith", Email: "john@example.com", Company: "Acme Corp", Subject: "Inquiry", Message: "Hello, I have a question.", Timestamp: time.Now().Unix() - 10, } b.ResetTimer() for i := 0; i < b.N; i++ { _ = ValidateContactForm(req) } }