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
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user