package validation import ( "strings" "testing" ) func TestValidateRequired(t *testing.T) { v := NewValidator() tests := []struct { name string value string wantError bool }{ {"Valid non-empty string", "test", false}, {"Empty string", "", true}, {"Whitespace only", " ", true}, {"Tab only", "\t", true}, {"Newline only", "\n", true}, {"Valid with spaces", "hello world", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateRequired(tt.value, "test_field") if (err != nil) != tt.wantError { t.Errorf("ValidateRequired() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateLength(t *testing.T) { v := NewValidator() tests := []struct { name string value string min int max int wantError bool }{ {"Valid length", "hello", 3, 10, false}, {"Too short", "ab", 3, 10, true}, {"Too long", "hello world this is too long", 3, 10, true}, {"Exact minimum", "abc", 3, 10, false}, {"Exact maximum", "0123456789", 3, 10, false}, {"No maximum (0)", "very long string here", 3, 0, false}, {"Whitespace counted correctly", " test ", 4, 10, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateLength(tt.value, "test_field", tt.min, tt.max) if (err != nil) != tt.wantError { t.Errorf("ValidateLength() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateEmail(t *testing.T) { v := NewValidator() tests := []struct { name string email string wantError bool }{ // Valid emails {"Valid email", "user@example.com", false}, {"Valid email with plus", "user+tag@example.com", false}, {"Valid email with dot", "first.last@example.com", false}, {"Valid email with hyphen", "user-name@example-domain.com", false}, {"Valid email with numbers", "user123@example456.com", false}, {"Valid email with subdomain", "user@sub.example.com", false}, // Invalid emails {"Empty email", "", true}, {"Whitespace only", " ", true}, {"Missing @", "userexample.com", true}, {"Missing domain", "user@", true}, {"Missing local part", "@example.com", true}, {"No TLD", "user@localhost", true}, {"Consecutive dots in local", "user..name@example.com", true}, {"Leading dot in local", ".user@example.com", true}, {"Trailing dot in local", "user.@example.com", true}, {"Double @", "user@@example.com", true}, {"Spaces in email", "user name@example.com", true}, {"Invalid characters", "user<>@example.com", true}, {"Too long", strings.Repeat("a", 320) + "@example.com", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateEmail(tt.email, "email") if (err != nil) != tt.wantError { t.Errorf("ValidateEmail() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateURL(t *testing.T) { v := NewValidator() tests := []struct { name string url string wantError bool }{ // Valid URLs {"Valid HTTP URL", "http://example.com", false}, {"Valid HTTPS URL", "https://example.com", false}, {"Valid URL with path", "https://example.com/path/to/resource", false}, {"Valid URL with query", "https://example.com?param=value", false}, {"Valid URL with port", "https://example.com:8080", false}, {"Valid URL with subdomain", "https://sub.example.com", false}, // Invalid URLs {"Empty URL", "", true}, {"Whitespace only", " ", true}, {"Missing scheme", "example.com", true}, {"Invalid scheme", "ftp://example.com", true}, {"Missing host", "https://", true}, {"Invalid characters", "https://exam ple.com", true}, {"Too long", "https://" + strings.Repeat("a", 2048) + ".com", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateURL(tt.url, "url") if (err != nil) != tt.wantError { t.Errorf("ValidateURL() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateHTTPSURL(t *testing.T) { v := NewValidator() tests := []struct { name string url string wantError bool }{ {"Valid HTTPS URL", "https://example.com", false}, {"HTTP URL (should fail)", "http://example.com", true}, {"FTP URL (should fail)", "ftp://example.com", true}, {"Invalid URL", "not-a-url", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateHTTPSURL(tt.url, "url") if (err != nil) != tt.wantError { t.Errorf("ValidateHTTPSURL() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateDomain(t *testing.T) { v := NewValidator() tests := []struct { name string domain string wantError bool }{ // Valid domains {"Valid domain", "example.com", false}, {"Valid subdomain", "sub.example.com", false}, {"Valid deep subdomain", "deep.sub.example.com", false}, {"Valid with hyphen", "my-site.example.com", false}, {"Valid with numbers", "site123.example456.com", false}, // Invalid domains {"Empty domain", "", true}, {"Whitespace only", " ", true}, {"Too short", "a.b", true}, {"Too long", strings.Repeat("a", 254) + ".com", true}, {"Label too long", strings.Repeat("a", 64) + ".example.com", true}, {"No TLD", "localhost", true}, {"Leading hyphen", "-example.com", true}, {"Trailing hyphen", "example-.com", true}, {"Double dot", "example..com", true}, {"Leading dot", ".example.com", true}, {"Trailing dot", "example.com.", true}, {"Underscore", "my_site.example.com", true}, {"Spaces", "my site.example.com", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateDomain(tt.domain, "domain") if (err != nil) != tt.wantError { t.Errorf("ValidateDomain() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateSlug(t *testing.T) { v := NewValidator() tests := []struct { name string slug string wantError bool }{ // Valid slugs {"Valid slug", "my-company", false}, {"Valid slug with numbers", "company123", false}, {"Valid slug all lowercase", "testcompany", false}, {"Valid slug with multiple hyphens", "my-test-company", false}, // Invalid slugs {"Empty slug", "", true}, {"Whitespace only", " ", true}, {"Too short", "ab", true}, {"Too long", strings.Repeat("a", 64), true}, {"Uppercase letters", "MyCompany", true}, {"Leading hyphen", "-company", true}, {"Trailing hyphen", "company-", true}, {"Double hyphen", "my--company", true}, {"Underscore", "my_company", true}, {"Spaces", "my company", true}, {"Special characters", "my@company", true}, // Reserved slugs {"Reserved: api", "api", true}, {"Reserved: admin", "admin", true}, {"Reserved: www", "www", true}, {"Reserved: login", "login", true}, {"Reserved: register", "register", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateSlug(tt.slug, "slug") if (err != nil) != tt.wantError { t.Errorf("ValidateSlug() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateUUID(t *testing.T) { v := NewValidator() tests := []struct { name string uuid string wantError bool }{ {"Valid UUID v4", "550e8400-e29b-41d4-a716-446655440000", false}, {"Valid UUID v4 lowercase", "123e4567-e89b-42d3-a456-426614174000", false}, {"Empty UUID", "", true}, {"Invalid format", "not-a-uuid", true}, {"Invalid version", "550e8400-e29b-21d4-a716-446655440000", true}, {"Missing hyphens", "550e8400e29b41d4a716446655440000", true}, {"Too short", "550e8400-e29b-41d4-a716", true}, {"With uppercase", "550E8400-E29B-41D4-A716-446655440000", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateUUID(tt.uuid, "id") if (err != nil) != tt.wantError { t.Errorf("ValidateUUID() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateEnum(t *testing.T) { v := NewValidator() allowedValues := []string{"free", "basic", "pro", "enterprise"} tests := []struct { name string value string wantError bool }{ {"Valid: free", "free", false}, {"Valid: basic", "basic", false}, {"Valid: pro", "pro", false}, {"Valid: enterprise", "enterprise", false}, {"Invalid: premium", "premium", true}, {"Invalid: empty", "", true}, {"Invalid: wrong case", "FREE", true}, {"Invalid: typo", "basi", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateEnum(tt.value, "plan_tier", allowedValues) if (err != nil) != tt.wantError { t.Errorf("ValidateEnum() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateRange(t *testing.T) { v := NewValidator() tests := []struct { name string value int min int max int wantError bool }{ {"Valid within range", 5, 1, 10, false}, {"Valid at minimum", 1, 1, 10, false}, {"Valid at maximum", 10, 1, 10, false}, {"Below minimum", 0, 1, 10, true}, {"Above maximum", 11, 1, 10, true}, {"No maximum (0)", 1000, 1, 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateRange(tt.value, "count", tt.min, tt.max) if (err != nil) != tt.wantError { t.Errorf("ValidateRange() error = %v, wantError %v", err, tt.wantError) } }) } } func TestValidateNoHTML(t *testing.T) { v := NewValidator() tests := []struct { name string value string wantError bool }{ {"Plain text", "Hello world", false}, {"Text with punctuation", "Hello, world!", false}, {"HTML tag ", true}, {"HTML tag ", "", true}, {"HTML tag
", "
content
", true}, {"HTML tag ", "link", true}, {"Less than symbol", "5 < 10", false}, {"Greater than symbol", "10 > 5", false}, {"Both symbols", "5 < x < 10", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidateNoHTML(tt.value, "content") if (err != nil) != tt.wantError { t.Errorf("ValidateNoHTML() error = %v, wantError %v", err, tt.wantError) } }) } } func TestSanitizeString(t *testing.T) { v := NewValidator() tests := []struct { name string input string expected string }{ {"Trim whitespace", " hello ", "hello"}, {"Remove null bytes", "hello\x00world", "helloworld"}, {"Already clean", "hello", "hello"}, {"Empty string", "", ""}, {"Only whitespace", " ", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := v.SanitizeString(tt.input) if result != tt.expected { t.Errorf("SanitizeString() = %q, want %q", result, tt.expected) } }) } } func TestStripHTML(t *testing.T) { v := NewValidator() tests := []struct { name string input string expected string }{ {"Remove script tag", "", "alert('xss')"}, {"Remove div tag", "
content
", "content"}, {"Remove multiple tags", "

Hello world

", "Hello world"}, {"No tags", "plain text", "plain text"}, {"Empty string", "", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := v.StripHTML(tt.input) if result != tt.expected { t.Errorf("StripHTML() = %q, want %q", result, tt.expected) } }) } } func TestValidateAndSanitizeString(t *testing.T) { v := NewValidator() tests := []struct { name string input string minLen int maxLen int wantValue string wantError bool }{ {"Valid and clean", "hello", 3, 10, "hello", false}, {"Trim and validate", " hello ", 3, 10, "hello", false}, {"Too short after trim", " a ", 3, 10, "", true}, {"Too long", "hello world this is too long", 3, 10, "", true}, {"Empty after trim", " ", 3, 10, "", true}, {"Valid with null byte removed", "hel\x00lo", 3, 10, "hello", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := v.ValidateAndSanitizeString(tt.input, "test_field", tt.minLen, tt.maxLen) if (err != nil) != tt.wantError { t.Errorf("ValidateAndSanitizeString() error = %v, wantError %v", err, tt.wantError) } if !tt.wantError && result != tt.wantValue { t.Errorf("ValidateAndSanitizeString() = %q, want %q", result, tt.wantValue) } }) } } func TestValidatePrintable(t *testing.T) { v := NewValidator() tests := []struct { name string value string wantError bool }{ {"All printable", "Hello World 123!", false}, {"With tabs and newlines", "Hello\tWorld\n", false}, {"With control character", "Hello\x01World", true}, {"With bell character", "Hello\x07", true}, {"Empty string", "", false}, {"Unicode printable", "Hello 世界", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := v.ValidatePrintable(tt.value, "test_field") if (err != nil) != tt.wantError { t.Errorf("ValidatePrintable() error = %v, wantError %v", err, tt.wantError) } }) } }