package validation import ( "strings" "testing" "time" ) // ============================================================================== // STRUCT TAG VALIDATOR TESTS // ============================================================================== func TestValidatorV2_ValidForm(t *testing.T) { 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 } err := ValidateContactFormV2(req) if err != nil { t.Errorf("Expected no error, got: %v", err) } // Verify transformations were applied if req.Name != "John Smith" { t.Errorf("Name should be trimmed") } } func TestValidatorV2_RequiredFields(t *testing.T) { tests := []struct { name string req *ContactFormRequest wantField string }{ { name: "Missing name", req: &ContactFormRequest{ Name: "", Email: "test@example.com", Subject: "Test", Message: "Test message", }, wantField: "name", }, { name: "Missing email", req: &ContactFormRequest{ Name: "John", Email: "", Subject: "Test", Message: "Test message", }, wantField: "email", }, { name: "Missing subject", req: &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "", Message: "Test message", }, wantField: "subject", }, { name: "Missing message", req: &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "", }, wantField: "message", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateContactFormV2(tt.req) if err == nil { t.Errorf("Expected validation error for missing field") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, tt.wantField, "required") { t.Errorf("Expected required error for field %s, got: %v", tt.wantField, verrs) } }) } } func TestValidatorV2_MaxLength(t *testing.T) { tests := []struct { name string req *ContactFormRequest wantField string wantTag string }{ { name: "Name too long", req: &ContactFormRequest{ Name: strings.Repeat("a", 101), Email: "test@example.com", Subject: "Test", Message: "Test message", }, wantField: "name", wantTag: "max", }, { name: "Email too long", req: &ContactFormRequest{ Name: "John", Email: strings.Repeat("a", 250) + "@example.com", Subject: "Test", Message: "Test message", }, wantField: "email", wantTag: "max", }, { name: "Subject too long", req: &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: strings.Repeat("a", 201), Message: "Test message", }, wantField: "subject", wantTag: "max", }, { name: "Message too long", req: &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: strings.Repeat("a", 5001), }, wantField: "message", wantTag: "max", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateContactFormV2(tt.req) if err == nil { t.Errorf("Expected validation error for field too long") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, tt.wantField, tt.wantTag) { t.Errorf("Expected %s error for field %s, got: %v", tt.wantTag, tt.wantField, verrs) } }) } } func TestValidatorV2_EmailValidation(t *testing.T) { tests := []struct { name string email string valid bool }{ {"Valid email", "test@example.com", true}, {"Valid with subdomain", "test@mail.example.com", true}, {"Invalid - no @", "testexample.com", false}, {"Invalid - no domain", "test@", false}, {"Invalid - no TLD", "test@example", false}, {"Invalid - spaces", "test @example.com", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := &ContactFormRequest{ Name: "John", Email: tt.email, Subject: "Test", Message: "Test message", } err := ValidateContactFormV2(req) if tt.valid { if err != nil { verrs, ok := err.(ValidationErrors) if ok && hasFieldError(verrs, "email", "email") { t.Errorf("Expected valid email, got error: %v", err) } } } else { if err == nil { t.Errorf("Expected validation error for invalid email") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, "email", "email") { t.Errorf("Expected email error, got: %v", verrs) } } }) } } func TestValidatorV2_PatternValidation(t *testing.T) { tests := []struct { name string field string value string wantError bool }{ // Name patterns {"Valid name", "name", "John Smith", false}, {"Valid hyphenated name", "name", "Anne-Marie", false}, {"Valid apostrophe name", "name", "O'Connor", false}, {"Invalid name with numbers", "name", "John123", true}, {"Invalid name with symbols", "name", "John@Smith", true}, // Subject patterns {"Valid subject", "subject", "Hello World", false}, {"Valid subject with punctuation", "subject", "Question #123", false}, {"Invalid subject with HTML", "subject", "", true}, // Company patterns {"Valid company", "company", "Acme Corp", false}, {"Valid company with &", "company", "Smith & Sons", false}, {"Invalid company with symbols", "company", "Company$$$", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "Test message", } // Set the field being tested switch tt.field { case "name": req.Name = tt.value case "subject": req.Subject = tt.value case "company": req.Company = tt.value } err := ValidateContactFormV2(req) if tt.wantError { if err == nil { t.Errorf("Expected validation error for invalid pattern") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, tt.field, "pattern") { t.Errorf("Expected pattern error for field %s, got: %v", tt.field, verrs) } } else { if err != nil { verrs, ok := err.(ValidationErrors) if ok && hasFieldError(verrs, tt.field, "pattern") { t.Errorf("Expected valid pattern, got error: %v", err) } } } }) } } func TestValidatorV2_EmailInjection(t *testing.T) { injectionTests := []struct { name string field string value string }{ {"Name injection", "name", "John\nBcc: evil@example.com"}, {"Email injection", "email", "test@example.com\nBcc: evil@example.com"}, {"Subject injection", "subject", "Test\r\nBcc: evil@example.com"}, {"Name with Bcc header", "name", "Bcc: evil@example.com"}, } for _, tt := range injectionTests { t.Run(tt.name, func(t *testing.T) { req := &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "Test message", } // Set the field being tested switch tt.field { case "name": req.Name = tt.value case "email": req.Email = tt.value case "subject": req.Subject = tt.value } err := ValidateContactFormV2(req) if err == nil { t.Errorf("Expected validation error for injection attempt") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, tt.field, "no_injection") { t.Errorf("Expected no_injection error for field %s, got: %v", tt.field, verrs) } }) } } func TestValidatorV2_Honeypot(t *testing.T) { tests := []struct { name string honeypot string wantError bool }{ {"Empty honeypot (human)", "", false}, {"Filled honeypot (bot)", "http://evil.com", true}, {"Any value (bot)", "test", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "Test message", Honeypot: tt.honeypot, } err := ValidateContactFormV2(req) if tt.wantError { if err == nil { t.Errorf("Expected validation error for filled honeypot") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, "website", "honeypot") { t.Errorf("Expected honeypot error, got: %v", verrs) } } else { if err != nil { verrs, ok := err.(ValidationErrors) if ok && hasFieldError(verrs, "website", "honeypot") { t.Errorf("Expected valid honeypot, got error: %v", err) } } } }) } } func TestValidatorV2_Timing(t *testing.T) { now := time.Now().Unix() tests := []struct { name string timestamp int64 wantError bool }{ {"Valid timing (10 seconds ago)", now - 10, false}, {"Valid timing (1 hour ago)", now - 3600, false}, {"Too fast (instant)", now, true}, {"Too fast (1 second)", now - 1, true}, {"Too old (25 hours)", now - 90000, true}, {"Future timestamp", now + 100, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "Test message", Timestamp: tt.timestamp, } err := ValidateContactFormV2(req) if tt.wantError { if err == nil { t.Errorf("Expected validation error for timing") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } if !hasFieldError(verrs, "timestamp", "timing") { t.Errorf("Expected timing error, got: %v", verrs) } } else { if err != nil { verrs, ok := err.(ValidationErrors) if ok && hasFieldError(verrs, "timestamp", "timing") { t.Errorf("Expected valid timing, got error: %v", err) } } } }) } } func TestValidatorV2_TrimTransform(t *testing.T) { req := &ContactFormRequest{ Name: " John Smith ", Email: " test@example.com ", Company: " Acme Corp ", Subject: " Test Subject ", Message: " Test message ", Timestamp: time.Now().Unix() - 10, } err := ValidateContactFormV2(req) if err != nil { t.Errorf("Expected no error, got: %v", err) } // Verify all fields were trimmed if req.Name != "John Smith" { t.Errorf("Name not trimmed: %q", req.Name) } if req.Email != "test@example.com" { t.Errorf("Email not trimmed: %q", req.Email) } if req.Company != "Acme Corp" { t.Errorf("Company not trimmed: %q", req.Company) } if req.Subject != "Test Subject" { t.Errorf("Subject not trimmed: %q", req.Subject) } if req.Message != "Test message" { t.Errorf("Message not trimmed: %q", req.Message) } } func TestValidatorV2_SanitizeTransform(t *testing.T) { req := &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "Test\nmessage\rwith\r\nnewlines", Timestamp: time.Now().Unix() - 10, } err := ValidateContactFormV2(req) if err != nil { t.Errorf("Expected no error, got: %v", err) } // Verify message was sanitized (newlines removed) if strings.Contains(req.Message, "\n") || strings.Contains(req.Message, "\r") { t.Errorf("Message not sanitized (still contains newlines): %q", req.Message) } } func TestValidatorV2_OptionalField(t *testing.T) { // Company is optional - should pass with empty value req := &ContactFormRequest{ Name: "John", Email: "test@example.com", Subject: "Test", Message: "Test message", Company: "", // Optional field Timestamp: time.Now().Unix() - 10, } err := ValidateContactFormV2(req) if err != nil { t.Errorf("Expected no error for optional field, got: %v", err) } // Company should still validate pattern if provided req.Company = "Acme$$$" // Invalid characters err = ValidateContactFormV2(req) if err == nil { t.Errorf("Expected validation error for invalid optional field") } } func TestValidatorV2_MultipleErrors(t *testing.T) { req := &ContactFormRequest{ Name: strings.Repeat("a", 101), // Too long Email: "invalid-email", // Invalid format Subject: "", // Required Message: "", // Valid (will be sanitized) } err := ValidateContactFormV2(req) if err == nil { t.Errorf("Expected validation errors") return } verrs, ok := err.(ValidationErrors) if !ok { t.Errorf("Expected ValidationErrors, got %T", err) return } // Should have at least 3 errors (name max, email format, subject required) if len(verrs) < 3 { t.Errorf("Expected at least 3 errors, got %d: %v", len(verrs), verrs) } } // ============================================================================== // VALIDATION ERRORS TESTS // ============================================================================== func TestValidationErrors_Error(t *testing.T) { errors := ValidationErrors{ {Field: "name", Tag: "required", Message: "Name is required"}, {Field: "email", Tag: "email", Message: "Invalid email"}, } errMsg := errors.Error() if !strings.Contains(errMsg, "name") || !strings.Contains(errMsg, "email") { t.Errorf("Error message should contain all field names: %s", errMsg) } } func TestValidationErrors_GetFieldError(t *testing.T) { errors := ValidationErrors{ {Field: "name", Tag: "required", Message: "Name is required"}, {Field: "email", Tag: "email", Message: "Invalid email"}, } err := errors.GetFieldError("name") if err == nil { t.Errorf("Expected to find name error") } if err != nil && err.Tag != "required" { t.Errorf("Expected required tag, got %s", err.Tag) } noErr := errors.GetFieldError("nonexistent") if noErr != nil { t.Errorf("Expected nil for nonexistent field") } } // ============================================================================== // BENCHMARK TESTS // ============================================================================== func BenchmarkValidatorV2_FirstValidation(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++ { // Create new validator each time (no cache benefit) v := NewValidator() _ = v.Validate(req) } } func BenchmarkValidatorV2_CachedValidation(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, } // First validation to populate cache v := NewValidator() _ = v.Validate(req) b.ResetTimer() for i := 0; i < b.N; i++ { _ = v.Validate(req) } } func BenchmarkValidatorV2_GlobalValidator(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++ { _ = ValidateContactFormV2(req) } } // ============================================================================== // HELPER FUNCTIONS // ============================================================================== // hasFieldError checks if ValidationErrors contains a specific field and tag error func hasFieldError(errors ValidationErrors, field string, tag string) bool { for _, err := range errors { if err.Field == field && err.Tag == tag { return true } } return false }