To gitea and beyond, let's go(-yco)
This commit is contained in:
56
internal/validation/fuzz_test.go
Normal file
56
internal/validation/fuzz_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"goyco/internal/fuzz"
|
||||
)
|
||||
|
||||
func FuzzValidateEmail(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidateEmail)
|
||||
}
|
||||
|
||||
func FuzzValidateUsername(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidateUsername)
|
||||
}
|
||||
|
||||
func FuzzValidatePassword(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidatePassword)
|
||||
}
|
||||
|
||||
func FuzzValidateURL(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidateURL)
|
||||
}
|
||||
|
||||
func FuzzValidateTitle(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidateTitle)
|
||||
}
|
||||
|
||||
func FuzzValidateContent(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidateContent)
|
||||
}
|
||||
|
||||
func FuzzValidateSearchQuery(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunValidationFuzzTest(f, ValidateSearchQuery)
|
||||
}
|
||||
|
||||
func FuzzSanitizeString(f *testing.F) {
|
||||
helper := fuzz.NewFuzzTestHelper()
|
||||
helper.RunSanitizationFuzzTestWithValidation(f,
|
||||
SanitizeString,
|
||||
func(result string) bool {
|
||||
return !containsNullBytes(result)
|
||||
})
|
||||
}
|
||||
|
||||
func containsNullBytes(s string) bool {
|
||||
return strings.Contains(s, "\x00")
|
||||
}
|
||||
413
internal/validation/validation.go
Normal file
413
internal/validation/validation.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var EmailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
var UsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]{3,50}$`)
|
||||
var URLRegex = regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`)
|
||||
|
||||
func ValidateEmail(email string) error {
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return &ValidationError{Field: "email", Message: "Email is required"}
|
||||
}
|
||||
if len(email) > 254 {
|
||||
return &ValidationError{Field: "email", Message: "Email is too long"}
|
||||
}
|
||||
if !EmailRegex.MatchString(email) {
|
||||
return &ValidationError{Field: "email", Message: "Invalid email format"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateUsername(username string) error {
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return &ValidationError{Field: "username", Message: "Username is required"}
|
||||
}
|
||||
if len(username) < 3 {
|
||||
return &ValidationError{Field: "username", Message: "Username must be at least 3 characters"}
|
||||
}
|
||||
if len(username) > 50 {
|
||||
return &ValidationError{Field: "username", Message: "Username must be at most 50 characters"}
|
||||
}
|
||||
if !UsernameRegex.MatchString(username) {
|
||||
return &ValidationError{Field: "username", Message: "Username can only contain letters, numbers, underscores, and hyphens"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
if password == "" {
|
||||
return &ValidationError{Field: "password", Message: "Password is required"}
|
||||
}
|
||||
if len(password) < 8 {
|
||||
return &ValidationError{Field: "password", Message: "Password must be at least 8 characters long"}
|
||||
}
|
||||
if len(password) > 128 {
|
||||
return &ValidationError{Field: "password", Message: "Password must be no more than 128 characters long"}
|
||||
}
|
||||
|
||||
hasLetter := false
|
||||
for _, char := range password {
|
||||
if unicode.IsLetter(char) {
|
||||
hasLetter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLetter {
|
||||
return &ValidationError{Field: "password", Message: "Password must contain at least one letter"}
|
||||
}
|
||||
|
||||
hasNumber := false
|
||||
for _, char := range password {
|
||||
if unicode.IsNumber(char) {
|
||||
hasNumber = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasNumber {
|
||||
return &ValidationError{Field: "password", Message: "Password must contain at least one number"}
|
||||
}
|
||||
|
||||
hasSpecial := false
|
||||
specialChars := "!@#$%^&*()_+-=[]{}|\\:\";'<>?,./~`"
|
||||
for _, char := range password {
|
||||
if strings.ContainsRune(specialChars, char) {
|
||||
hasSpecial = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSpecial {
|
||||
return &ValidationError{Field: "password", Message: "Password must contain at least one special character"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateTitle(title string) error {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return &ValidationError{Field: "title", Message: "Title is required"}
|
||||
}
|
||||
if len(title) < 3 {
|
||||
return &ValidationError{Field: "title", Message: "Title must be at least 3 characters"}
|
||||
}
|
||||
if len(title) > 200 {
|
||||
return &ValidationError{Field: "title", Message: "Title must be at most 200 characters"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateContent(content string) error {
|
||||
content = strings.TrimSpace(content)
|
||||
if len(content) > 10000 {
|
||||
return &ValidationError{Field: "content", Message: "Content must be at most 10000 characters"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateURL(url string) error {
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return &ValidationError{Field: "url", Message: "URL is required"}
|
||||
}
|
||||
if len(url) > 2048 {
|
||||
return &ValidationError{Field: "url", Message: "URL is too long"}
|
||||
}
|
||||
if !URLRegex.MatchString(url) {
|
||||
return &ValidationError{Field: "url", Message: "Invalid URL format. Only HTTP and HTTPS URLs are allowed"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateSearchQuery(query string) error {
|
||||
query = strings.TrimSpace(query)
|
||||
if len(query) > 100 {
|
||||
return &ValidationError{Field: "query", Message: "Search query is too long"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SanitizeString(input string) string {
|
||||
input = strings.ReplaceAll(input, "\x00", "")
|
||||
input = strings.ReplaceAll(input, "\r", "")
|
||||
input = strings.ReplaceAll(input, "\n", "")
|
||||
input = strings.ReplaceAll(input, "\t", "")
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
type StructValidationError struct {
|
||||
Errors []FieldValidationError
|
||||
}
|
||||
|
||||
func (e *StructValidationError) Error() string {
|
||||
if len(e.Errors) == 0 {
|
||||
return "validation failed"
|
||||
}
|
||||
messages := make([]string, len(e.Errors))
|
||||
for i, err := range e.Errors {
|
||||
messages[i] = err.Message
|
||||
}
|
||||
return strings.Join(messages, "; ")
|
||||
}
|
||||
|
||||
type FieldValidationError struct {
|
||||
Field string
|
||||
Tag string
|
||||
Param string
|
||||
Message string
|
||||
}
|
||||
|
||||
func ValidateStruct(s interface{}) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(s)
|
||||
typ := reflect.TypeOf(s)
|
||||
|
||||
if val.Kind() == reflect.Ptr {
|
||||
if val.IsNil() {
|
||||
return nil
|
||||
}
|
||||
val = val.Elem()
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("ValidateStruct: expected struct, got %s", val.Kind())
|
||||
}
|
||||
|
||||
var errors []FieldValidationError
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
fieldVal := val.Field(i)
|
||||
|
||||
if !fieldVal.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
validateTag := field.Tag.Get("validate")
|
||||
if validateTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := strings.Split(validateTag, ",")
|
||||
omitempty := false
|
||||
|
||||
for _, tag := range tags {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if tag == "omitempty" {
|
||||
omitempty = true
|
||||
continue
|
||||
}
|
||||
|
||||
var tagName, param string
|
||||
if idx := strings.Index(tag, "="); idx != -1 {
|
||||
tagName = tag[:idx]
|
||||
param = tag[idx+1:]
|
||||
} else {
|
||||
tagName = tag
|
||||
}
|
||||
|
||||
if err := validateField(field.Name, fieldVal, tagName, param, omitempty); err != nil {
|
||||
errors = append(errors, FieldValidationError{
|
||||
Field: field.Name,
|
||||
Tag: tagName,
|
||||
Param: param,
|
||||
Message: err.Message,
|
||||
})
|
||||
if tagName == "required" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return &StructValidationError{Errors: errors}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateField(fieldName string, fieldVal reflect.Value, tagName, param string, omitempty bool) *ValidationError {
|
||||
if omitempty && isEmptyValue(fieldVal) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch tagName {
|
||||
case "required":
|
||||
if isEmptyValue(fieldVal) {
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " is required"}
|
||||
}
|
||||
case "min":
|
||||
if err := validateMin(fieldName, fieldVal, param); err != nil {
|
||||
return err
|
||||
}
|
||||
case "max":
|
||||
if err := validateMax(fieldName, fieldVal, param); err != nil {
|
||||
return err
|
||||
}
|
||||
case "email":
|
||||
if err := validateEmailTag(fieldName, fieldVal); err != nil {
|
||||
return err
|
||||
}
|
||||
case "url":
|
||||
if err := validateURLTag(fieldName, fieldVal); err != nil {
|
||||
return err
|
||||
}
|
||||
case "oneof":
|
||||
if err := validateOneOf(fieldName, fieldVal, param); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " is invalid"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
return v.String() == ""
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateMin(fieldName string, v reflect.Value, param string) *ValidationError {
|
||||
min, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " has invalid min parameter"}
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
if len(v.String()) < min {
|
||||
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d characters", fieldName, min)}
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if v.Int() < int64(min) {
|
||||
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d", fieldName, min)}
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if v.Uint() < uint64(min) {
|
||||
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at least %d", fieldName, min)}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMax(fieldName string, v reflect.Value, param string) *ValidationError {
|
||||
max, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " has invalid max parameter"}
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
if len(v.String()) > max {
|
||||
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d characters", fieldName, max)}
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if v.Int() > int64(max) {
|
||||
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d", fieldName, max)}
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if v.Uint() > uint64(max) {
|
||||
return &ValidationError{Field: fieldName, Message: fmt.Sprintf("%s must be at most %d", fieldName, max)}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEmailTag(fieldName string, v reflect.Value) *ValidationError {
|
||||
if v.Kind() != reflect.String {
|
||||
return nil
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(v.String())
|
||||
if email == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !EmailRegex.MatchString(email) {
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " must be a valid email address"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateURLTag(fieldName string, v reflect.Value) *ValidationError {
|
||||
if v.Kind() != reflect.String {
|
||||
return nil
|
||||
}
|
||||
|
||||
url := strings.TrimSpace(v.String())
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(url) > 2048 {
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " must be at most 2048 characters"}
|
||||
}
|
||||
|
||||
if !URLRegex.MatchString(url) {
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " must be a valid URL"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOneOf(fieldName string, v reflect.Value, param string) *ValidationError {
|
||||
if v.Kind() != reflect.String {
|
||||
return nil
|
||||
}
|
||||
|
||||
value := v.String()
|
||||
allowedValues := strings.Split(param, " ")
|
||||
|
||||
for _, allowed := range allowedValues {
|
||||
if value == allowed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &ValidationError{Field: fieldName, Message: fieldName + " must be one of: " + param}
|
||||
}
|
||||
437
internal/validation/validation_test.go
Normal file
437
internal/validation/validation_test.go
Normal file
@@ -0,0 +1,437 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid email", "test@example.com", false},
|
||||
{"valid email with subdomain", "user@mail.example.com", false},
|
||||
{"empty email", "", true},
|
||||
{"invalid format", "invalid-email", true},
|
||||
{"missing @", "testexample.com", true},
|
||||
{"missing domain", "test@", true},
|
||||
{"multiple @ symbols", "test@@example.com", true},
|
||||
{"consecutive dots", "test..user@example.com", false},
|
||||
{"dot at start", ".test@example.com", false},
|
||||
{"dot at end", "test.@example.com", false},
|
||||
{"too long", "a" + strings.Repeat("x", 250) + "@example.com", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateEmail(tt.email)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateEmail() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid username", "testuser", false},
|
||||
{"valid with underscore", "test_user", false},
|
||||
{"valid with hyphen", "test-user", false},
|
||||
{"valid with numbers", "test123", false},
|
||||
{"empty username", "", true},
|
||||
{"too short", "ab", true},
|
||||
{"too long", "a" + strings.Repeat("x", 50), true},
|
||||
{"invalid characters", "test@user", true},
|
||||
{"spaces", "test user", true},
|
||||
{"exactly 3 chars", "abc", false},
|
||||
{"exactly 20 chars", strings.Repeat("a", 20), false},
|
||||
{"exactly 50 chars", strings.Repeat("a", 50), false},
|
||||
{"exactly 51 chars", strings.Repeat("a", 51), true},
|
||||
{"starts with number", "123user", false},
|
||||
{"starts with underscore", "_user", false},
|
||||
{"starts with hyphen", "-user", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateUsername(tt.username)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateUsername() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid password", "Password123!", false},
|
||||
{"valid with special chars", "Pass@123", false},
|
||||
{"valid with underscore", "Password123_", false},
|
||||
{"valid with hyphen", "Password123-", false},
|
||||
{"valid with tilde", "Password123~", false},
|
||||
{"valid with backtick", "Password123`", false},
|
||||
{"valid with pipe", "Password123|", false},
|
||||
{"valid with brackets", "Password123[]", false},
|
||||
{"valid with braces", "Password123{}", false},
|
||||
{"valid with colon", "Password123:", false},
|
||||
{"valid with semicolon", "Password123;", false},
|
||||
{"valid with quotes", "Password123\"'", false},
|
||||
{"valid with angle brackets", "Password123<>", false},
|
||||
{"valid with comma and period", "Password123,.", false},
|
||||
{"valid with question mark and slash", "Password123?/", false},
|
||||
{"empty password", "", true},
|
||||
{"too short", "Pass1!", true},
|
||||
{"exactly 7 chars", "Pass12!", true},
|
||||
{"exactly 8 chars", "Pass123!", false},
|
||||
{"exactly 128 chars", createValidPassword(128), false},
|
||||
{"exactly 129 chars", createValidPassword(129), true},
|
||||
{"no letters", "12345678!", true},
|
||||
{"no numbers", "Password!", true},
|
||||
{"no special chars", "Password123", true},
|
||||
{"too long", strings.Repeat("a", 130), true},
|
||||
{"unicode letters", "Pássw0rd123!", false},
|
||||
{"unicode numbers", "Password123!", false},
|
||||
{"unicode special", "Password123!", false},
|
||||
{"mixed unicode", "Pássw0rd123!", false},
|
||||
{"only unicode letters", "Pásswórd", true},
|
||||
{"only unicode numbers", "12345678", true},
|
||||
{"only unicode special", "!@#$%^&*", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePassword(tt.password)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidatePassword() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTitle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
title string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid title", "Test Post Title", false},
|
||||
{"empty title", "", true},
|
||||
{"too short", "ab", true},
|
||||
{"too long", strings.Repeat("a", 201), true},
|
||||
{"whitespace only", " ", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateTitle(tt.title)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateTitle() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid content", "This is a valid content", false},
|
||||
{"empty content", "", false},
|
||||
{"short content", "ab", false},
|
||||
{"whitespace only", " ", false},
|
||||
{"exactly max length", strings.Repeat("a", 10000), false},
|
||||
{"over max length", strings.Repeat("a", 10001), true},
|
||||
{"very long content", strings.Repeat("a", 20000), true},
|
||||
{"content with newlines", "Line 1\nLine 2\nLine 3", false},
|
||||
{"content with special chars", "Content with special chars: !@#$%^&*()", false},
|
||||
{"unicode content", "Content with unicode: 你好世界", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateContent(tt.content)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateContent() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid HTTP URL", "http://example.com", false},
|
||||
{"valid HTTPS URL", "https://example.com", false},
|
||||
{"valid with path", "https://example.com/path", false},
|
||||
{"valid with query params", "https://example.com/search?q=test", false},
|
||||
{"valid with fragment", "https://example.com/page#section", false},
|
||||
{"valid with port", "https://example.com:8080/path", false},
|
||||
{"empty URL", "", true},
|
||||
{"invalid protocol", "ftp://example.com", true},
|
||||
{"no protocol", "example.com", true},
|
||||
{"too long", "https://example.com/" + strings.Repeat("a", 2040), true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateURL(tt.url)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateURL() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSearchQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid query", "search term", false},
|
||||
{"empty query", "", false},
|
||||
{"short query", "a", false},
|
||||
{"whitespace only", " ", false},
|
||||
{"exactly max length", strings.Repeat("a", 100), false},
|
||||
{"over max length", strings.Repeat("a", 101), true},
|
||||
{"very long query", strings.Repeat("a", 200), true},
|
||||
{"query with special chars", "search with !@#$%^&*()", false},
|
||||
{"unicode query", "search with unicode: 你好世界", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSearchQuery(tt.query)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateSearchQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"normal string", "hello world", "hello world"},
|
||||
{"with newlines", "hello\nworld", "helloworld"},
|
||||
{"with tabs", "hello\tworld", "helloworld"},
|
||||
{"with carriage returns", "hello\rworld", "helloworld"},
|
||||
{"with null bytes", "hello\x00world", "helloworld"},
|
||||
{"with spaces", " hello world ", "hello world"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeString(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("SanitizeString() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStruct(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Username string `validate:"required,min=3,max=20"`
|
||||
Email string `validate:"required,email"`
|
||||
Age int `validate:"min=18,max=120"`
|
||||
URL string `validate:"url"`
|
||||
Status string `validate:"oneof=active inactive pending"`
|
||||
Optional string `validate:"omitempty,min=1"`
|
||||
}
|
||||
|
||||
t.Run("valid struct", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateStruct() error = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing required field", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err == nil {
|
||||
t.Error("ValidateStruct() expected error, got nil")
|
||||
}
|
||||
if structErr, ok := err.(*StructValidationError); ok {
|
||||
if len(structErr.Errors) == 0 {
|
||||
t.Error("Expected validation errors, got none")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid email", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "invalid-email",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err == nil {
|
||||
t.Error("ValidateStruct() expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid min", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "ab",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err == nil {
|
||||
t.Error("ValidateStruct() expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid max", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: strings.Repeat("a", 21),
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err == nil {
|
||||
t.Error("ValidateStruct() expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid URL", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "invalid-url",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err == nil {
|
||||
t.Error("ValidateStruct() expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid oneof", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "invalid",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err == nil {
|
||||
t.Error("ValidateStruct() expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("omitempty with empty value", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
Optional: "",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateStruct() error = %v, want nil (empty optional field should be allowed)", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("omitempty with valid value", func(t *testing.T) {
|
||||
s := TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
Optional: "valid",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateStruct() error = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pointer to struct", func(t *testing.T) {
|
||||
s := &TestStruct{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
URL: "https://example.com",
|
||||
Status: "active",
|
||||
}
|
||||
err := ValidateStruct(s)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateStruct() error = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var s *TestStruct = nil
|
||||
err := ValidateStruct(s)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateStruct() error = %v, want nil (nil should be allowed)", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createValidPassword(length int) string {
|
||||
if length < 8 {
|
||||
return "Pass123!"
|
||||
}
|
||||
|
||||
password := make([]byte, length)
|
||||
|
||||
for i := range length {
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
password[i] = 'P'
|
||||
case 1:
|
||||
password[i] = 'a'
|
||||
case 2:
|
||||
password[i] = '1'
|
||||
case 3:
|
||||
password[i] = '!'
|
||||
}
|
||||
}
|
||||
|
||||
return string(password)
|
||||
}
|
||||
Reference in New Issue
Block a user