69012bb1ae
New test files: - config/config_test.go (100% coverage) - constants/constants_test.go (100% coverage) - httputil/response_test.go (100% coverage) - validation/rules_test.go (91.9% coverage) - middleware/logger_test.go, security_test.go, security_logger_test.go - handlers/errors_test.go Updated documentation: - doc/27-GO-TESTING.md: Complete testing guide - doc/00-GO-DOCUMENTATION-INDEX.md: Added testing section - doc/01-ARCHITECTURE.md: Updated package structure - doc/DECISIONS.md: Added ADR-004 caching decision - PROJECT-MEMORY.md: Added Go testing section
183 lines
4.9 KiB
Go
183 lines
4.9 KiB
Go
package validation
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRuleOptional(t *testing.T) {
|
|
// Optional rule should always return nil
|
|
result := ruleOptional("field", "", "")
|
|
if result != nil {
|
|
t.Error("ruleOptional should always return nil")
|
|
}
|
|
|
|
result = ruleOptional("field", "value", "")
|
|
if result != nil {
|
|
t.Error("ruleOptional should always return nil")
|
|
}
|
|
}
|
|
|
|
func TestRuleTrim(t *testing.T) {
|
|
// Trim rule is a marker, should always return nil
|
|
result := ruleTrim("field", " value ", "")
|
|
if result != nil {
|
|
t.Error("ruleTrim should always return nil")
|
|
}
|
|
}
|
|
|
|
func TestRuleSanitize(t *testing.T) {
|
|
// Sanitize rule is a marker, should always return nil
|
|
result := ruleSanitize("field", "<script>alert('xss')</script>", "")
|
|
if result != nil {
|
|
t.Error("ruleSanitize should always return nil")
|
|
}
|
|
}
|
|
|
|
func TestRuleMin(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
field string
|
|
value string
|
|
param string
|
|
hasError bool
|
|
}{
|
|
{"Valid - meets minimum", "msg", "hello", "5", false},
|
|
{"Valid - exceeds minimum", "msg", "hello world", "5", false},
|
|
{"Invalid - too short", "msg", "hi", "5", true},
|
|
{"Invalid - empty", "msg", "", "1", true},
|
|
{"Invalid param", "msg", "hello", "invalid", true},
|
|
{"UTF-8 aware - valid", "name", "José", "4", false},
|
|
{"UTF-8 aware - valid", "name", "日本語", "3", false},
|
|
{"UTF-8 aware - invalid", "name", "日", "3", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ruleMin(tt.field, tt.value, tt.param)
|
|
if (result != nil) != tt.hasError {
|
|
t.Errorf("ruleMin(%q, %q, %q) error = %v, wantError %v", tt.field, tt.value, tt.param, result != nil, tt.hasError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuleTiming(t *testing.T) {
|
|
now := time.Now().Unix()
|
|
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
param string
|
|
hasError bool
|
|
}{
|
|
{"Empty value", "", "2:86400", false},
|
|
{"Valid timing", strconv.FormatInt(now-10, 10), "2:86400", false},
|
|
{"Too quick", strconv.FormatInt(now-1, 10), "2:86400", true},
|
|
{"Too old", strconv.FormatInt(now-100000, 10), "2:86400", true},
|
|
{"Invalid param format", strconv.FormatInt(now-10, 10), "invalid", true},
|
|
{"Invalid min param", strconv.FormatInt(now-10, 10), "abc:100", true},
|
|
{"Invalid max param", strconv.FormatInt(now-10, 10), "2:xyz", true},
|
|
{"Invalid timestamp", "not_a_number", "2:86400", true},
|
|
{"Future timestamp", strconv.FormatInt(now+1000, 10), "2:86400", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ruleTiming("timestamp", tt.value, tt.param)
|
|
if (result != nil) != tt.hasError {
|
|
t.Errorf("ruleTiming(%q, %q) error = %v, wantError %v", tt.value, tt.param, result != nil, tt.hasError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFieldError_Error(t *testing.T) {
|
|
t.Run("With param", func(t *testing.T) {
|
|
err := FieldError{
|
|
Field: "email",
|
|
Tag: "max",
|
|
Param: "100",
|
|
Message: "too long",
|
|
}
|
|
errStr := err.Error()
|
|
if !strings.Contains(errStr, "email") {
|
|
t.Error("Error should contain field name")
|
|
}
|
|
if !strings.Contains(errStr, "max=100") {
|
|
t.Error("Error should contain tag=param")
|
|
}
|
|
})
|
|
|
|
t.Run("Without param", func(t *testing.T) {
|
|
err := FieldError{
|
|
Field: "email",
|
|
Tag: "required",
|
|
Message: "is required",
|
|
}
|
|
errStr := err.Error()
|
|
if !strings.Contains(errStr, "email") {
|
|
t.Error("Error should contain field name")
|
|
}
|
|
if strings.Contains(errStr, "(") {
|
|
t.Error("Error without param should not contain parentheses")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidationErrors_HasErrors(t *testing.T) {
|
|
t.Run("No errors", func(t *testing.T) {
|
|
var ve ValidationErrors
|
|
if ve.HasErrors() {
|
|
t.Error("HasErrors should return false for empty errors")
|
|
}
|
|
})
|
|
|
|
t.Run("Has errors", func(t *testing.T) {
|
|
ve := ValidationErrors{
|
|
{Field: "email", Message: "required"},
|
|
}
|
|
if !ve.HasErrors() {
|
|
t.Error("HasErrors should return true when errors exist")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidationErrors_GetFieldErrors(t *testing.T) {
|
|
ve := ValidationErrors{
|
|
{Field: "email", Tag: "required", Message: "required"},
|
|
{Field: "email", Tag: "email", Message: "invalid format"},
|
|
{Field: "name", Tag: "required", Message: "required"},
|
|
}
|
|
|
|
t.Run("Get multiple errors for field", func(t *testing.T) {
|
|
errors := ve.GetFieldErrors("email")
|
|
if len(errors) != 2 {
|
|
t.Errorf("GetFieldErrors(email) returned %d errors, want 2", len(errors))
|
|
}
|
|
})
|
|
|
|
t.Run("Get single error for field", func(t *testing.T) {
|
|
errors := ve.GetFieldErrors("name")
|
|
if len(errors) != 1 {
|
|
t.Errorf("GetFieldErrors(name) returned %d errors, want 1", len(errors))
|
|
}
|
|
})
|
|
|
|
t.Run("No errors for field", func(t *testing.T) {
|
|
errors := ve.GetFieldErrors("nonexistent")
|
|
if len(errors) != 0 {
|
|
t.Errorf("GetFieldErrors(nonexistent) returned %d errors, want 0", len(errors))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidationErrors_Error_Empty(t *testing.T) {
|
|
var ve ValidationErrors
|
|
if ve.Error() != "" {
|
|
t.Error("Error() should return empty string for no errors")
|
|
}
|
|
}
|