6c7595b041
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
242 lines
6.4 KiB
Go
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
|
|
}
|