Files
juanatsap 6c7595b041 feat: add tag-based validation system with reflection caching
Implement a declarative struct tag validation system for Go:

- Add validator.go with sync.Map caching for reflection metadata
- Add rules.go with 11 built-in validation rules (required, email,
  pattern, honeypot, timing, etc.)
- Add errors.go with FieldError and ValidationErrors types
- Update ContactFormRequest with validate tags
- Add ValidateContactFormV2() using the new tag-based validator

Rules implemented:
- required/optional: field presence validation
- trim/sanitize: automatic value transformations
- min/max: UTF-8 aware length validation
- email: RFC 5322 email format validation
- pattern: predefined regex patterns (name, subject, company)
- no_injection: email header injection prevention
- honeypot: bot trap (must be empty)
- timing: timestamp validation for bot detection

Documentation:
- docs/go-validation-system.md: complete validation guide
- docs/go-template-system.md: template manager documentation
- docs/go-routes-api.md: routes and API reference
- docs/README.md: documentation index
2025-12-06 15:20:45 +00:00

242 lines
6.4 KiB
Go

package validation_test
import (
"fmt"
"time"
"github.com/juanatsap/cv-site/internal/validation"
)
// Example_basicValidation demonstrates basic usage of the struct tag validator
func Example_basicValidation() {
req := &validation.ContactFormRequest{
Name: "John Smith",
Email: "john@example.com",
Company: "Acme Corp",
Subject: "Inquiry about services",
Message: "I would like to know more about your services.",
Honeypot: "",
Timestamp: time.Now().Unix() - 10, // 10 seconds ago
}
err := validation.ValidateContactFormV2(req)
if err != nil {
fmt.Println("Validation failed:", err)
return
}
fmt.Println("Validation successful!")
// Output: Validation successful!
}
// Example_validationErrors demonstrates error handling
func Example_validationErrors() {
req := &validation.ContactFormRequest{
Name: "", // Empty - will fail required
Email: "invalid-email",
Subject: "Test",
Message: "Test message",
Timestamp: time.Now().Unix() - 10, // Valid timestamp
}
err := validation.ValidateContactFormV2(req)
if err != nil {
if verrs, ok := err.(validation.ValidationErrors); ok {
fmt.Printf("Found %d validation errors:\n", len(verrs))
for _, e := range verrs {
fmt.Printf("- %s: %s\n", e.Field, e.Message)
}
}
}
// Output:
// Found 2 validation errors:
// - name: name is required
// - email: Invalid email address format
}
// Example_trimTransform demonstrates automatic trimming
func Example_trimTransform() {
req := &validation.ContactFormRequest{
Name: " John Smith ",
Email: " john@example.com ",
Subject: " Test ",
Message: " Test message ",
Timestamp: time.Now().Unix() - 10,
}
err := validation.ValidateContactFormV2(req)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Name: '%s'\n", req.Name)
fmt.Printf("Email: '%s'\n", req.Email)
fmt.Printf("Subject: '%s'\n", req.Subject)
fmt.Printf("Message: '%s'\n", req.Message)
// Output:
// Name: 'John Smith'
// Email: 'john@example.com'
// Subject: 'Test'
// Message: 'Test message'
}
// Example_securityValidation demonstrates security features
func Example_securityValidation() {
// Email injection attempt via subject field
req := &validation.ContactFormRequest{
Name: "John Smith",
Email: "test@example.com",
Subject: "Test\nBcc: attacker@evil.com",
Message: "Test",
Timestamp: time.Now().Unix() - 10,
}
err := validation.ValidateContactFormV2(req)
if err != nil {
if verrs, ok := err.(validation.ValidationErrors); ok {
for _, e := range verrs {
if e.Tag == "no_injection" {
fmt.Printf("Security violation detected: %s\n", e.Message)
break
}
}
}
}
// Output: Security violation detected: subject contains invalid characters (possible injection attempt)
}
// Example_botDetection demonstrates honeypot and timing validation
func Example_botDetection() {
// Bot filled honeypot field
req1 := &validation.ContactFormRequest{
Name: "Bot",
Email: "bot@example.com",
Subject: "Spam",
Message: "Spam message",
Honeypot: "http://evil.com",
}
err1 := validation.ValidateContactFormV2(req1)
if err1 != nil {
if verrs, ok := err1.(validation.ValidationErrors); ok {
if honeypotErr := verrs.GetFieldError("website"); honeypotErr != nil {
fmt.Println("Bot detected via honeypot:", honeypotErr.Message)
}
}
}
// Bot submitted too quickly
req2 := &validation.ContactFormRequest{
Name: "John",
Email: "john@example.com",
Subject: "Test",
Message: "Test",
Timestamp: time.Now().Unix(), // Just now (< 2 seconds)
}
err2 := validation.ValidateContactFormV2(req2)
if err2 != nil {
if verrs, ok := err2.(validation.ValidationErrors); ok {
if timingErr := verrs.GetFieldError("timestamp"); timingErr != nil {
fmt.Println("Bot detected via timing:", timingErr.Message)
}
}
}
// Output:
// Bot detected via honeypot: Bot detected
// Bot detected via timing: Form submitted too quickly (bot detected)
}
// Example_internationalNames demonstrates UTF-8 support
func Example_internationalNames() {
names := []struct {
name string
valid bool
}{
{"José María", true}, // Spanish
{"François Dubois", true}, // French
{"Müller", true}, // German
{"Anne-Marie", true}, // Hyphenated
{"O'Connor", true}, // Apostrophe
{"John123", false}, // Numbers (invalid)
{"John@Smith", false}, // Special chars (invalid)
}
for _, tc := range names {
req := &validation.ContactFormRequest{
Name: tc.name,
Email: "test@example.com",
Subject: "Test",
Message: "Test",
Timestamp: time.Now().Unix() - 10,
}
err := validation.ValidateContactFormV2(req)
isValid := err == nil || !containsPatternError(err, "name")
if isValid == tc.valid {
fmt.Printf("'%s': %v ✓\n", tc.name, tc.valid)
} else {
fmt.Printf("'%s': unexpected result\n", tc.name)
}
}
// Output:
// 'José María': true ✓
// 'François Dubois': true ✓
// 'Müller': true ✓
// 'Anne-Marie': true ✓
// 'O'Connor': true ✓
// 'John123': false ✓
// 'John@Smith': false ✓
}
// Example_multipleErrors demonstrates handling multiple validation errors
func Example_multipleErrors() {
req := &validation.ContactFormRequest{
Name: "", // Required
Email: "invalid", // Invalid format
Subject: "", // Required
Message: "", // Required
Timestamp: time.Now().Unix() - 10,
}
err := validation.ValidateContactFormV2(req)
if err != nil {
if verrs, ok := err.(validation.ValidationErrors); ok {
fmt.Printf("Total errors: %d\n", len(verrs))
// Get errors by field
fields := []string{"name", "email", "subject", "message"}
for _, field := range fields {
if fieldErr := verrs.GetFieldError(field); fieldErr != nil {
fmt.Printf("- %s: %s (tag: %s)\n", fieldErr.Field, fieldErr.Message, fieldErr.Tag)
}
}
}
}
// Output:
// Total errors: 4
// - name: name is required (tag: required)
// - email: Invalid email address format (tag: email)
// - subject: subject is required (tag: required)
// - message: message is required (tag: required)
}
// Helper function for example
func containsPatternError(err error, field string) bool {
if err == nil {
return false
}
verrs, ok := err.(validation.ValidationErrors)
if !ok {
return false
}
for _, e := range verrs {
if e.Field == field && e.Tag == "pattern" {
return true
}
}
return false
}