package traefikoidc import ( "strings" "testing" ) func TestInputValidator(t *testing.T) { config := DefaultInputValidationConfig() logger := NewLogger("debug") validator, err := NewInputValidator(config, logger) if err != nil { t.Fatalf("Failed to create validator: %v", err) } t.Run("Valid token validation", func(t *testing.T) { validToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHs3UjpMC6M6FNqI2J-I2NxrragtnDxGxdJUvDERDQVHzeNlVQiuqWDEeO_O-0KptafbfyuGqfQxH_6dp2_MeFpAc" // trufflehog:ignore result := validator.ValidateToken(validToken) if !result.IsValid { t.Errorf("Expected valid token to pass validation, got errors: %v", result.Errors) } }) t.Run("Invalid token validation", func(t *testing.T) { invalidTokens := []string{ "", // Empty token "invalid.token", // Invalid format "a.b", // Too few parts "a.b.c.d", // Too many parts } for _, token := range invalidTokens { result := validator.ValidateToken(token) if result.IsValid { t.Errorf("Expected invalid token '%s' to fail validation", token) } } }) t.Run("Valid email validation", func(t *testing.T) { validEmails := []string{ "user@example.com", "test.email@domain.co.uk", "user123@test-domain.org", } for _, email := range validEmails { result := validator.ValidateEmail(email) if !result.IsValid { t.Errorf("Expected valid email '%s' to pass validation, got errors: %v", email, result.Errors) } } }) t.Run("Invalid email validation", func(t *testing.T) { invalidEmails := []string{ "", // Empty "invalid", // No @ symbol "@domain.com", // No local part "user@", // No domain "user@domain", // No TLD "user..double@domain.com", // Double dots } for _, email := range invalidEmails { result := validator.ValidateEmail(email) if result.IsValid { t.Errorf("Expected invalid email '%s' to fail validation", email) } } }) t.Run("Valid URL validation", func(t *testing.T) { validURLs := []string{ "https://example.com", "https://sub.domain.com/path", "https://localhost:8080/callback", } for _, url := range validURLs { result := validator.ValidateURL(url) if !result.IsValid { t.Errorf("Expected valid URL '%s' to pass validation, got errors: %v", url, result.Errors) } } }) t.Run("Invalid URL validation", func(t *testing.T) { invalidURLs := []string{ "", // Empty "not-a-url", // Invalid format "ftp://example.com", // Wrong scheme "https://", // No host } for _, url := range invalidURLs { result := validator.ValidateURL(url) if result.IsValid { t.Errorf("Expected invalid URL '%s' to fail validation", url) } } }) t.Run("Valid username validation", func(t *testing.T) { validUsernames := []string{ "user123", "test_user", "user-name", } for _, username := range validUsernames { result := validator.ValidateUsername(username) if !result.IsValid { t.Errorf("Expected valid username '%s' to pass validation, got errors: %v", username, result.Errors) } } }) t.Run("Invalid username validation", func(t *testing.T) { invalidUsernames := []string{ "", // Empty "a", // Too short strings.Repeat("a", 100), // Too long "user name", // Spaces } for _, username := range invalidUsernames { result := validator.ValidateUsername(username) if result.IsValid { t.Errorf("Expected invalid username '%s' to fail validation", username) } } }) t.Run("Valid claim validation", func(t *testing.T) { validClaims := map[string]string{ "sub": "user123", "email": "user@example.com", "name": "John Doe", } for key, value := range validClaims { result := validator.ValidateClaim(key, value) if !result.IsValid { t.Errorf("Expected valid claim '%s'='%s' to pass validation, got errors: %v", key, value, result.Errors) } } }) t.Run("Invalid claim validation", func(t *testing.T) { invalidClaims := map[string]string{ "": "value", // Empty key "long_key": strings.Repeat("a", 10000), // Too long value } for key, value := range invalidClaims { result := validator.ValidateClaim(key, value) if result.IsValid { t.Errorf("Expected invalid claim '%s'='%s' to fail validation", key, value) } } }) t.Run("Valid header validation", func(t *testing.T) { validHeaders := map[string]string{ "Authorization": "Bearer token123", "Content-Type": "application/json", "X-Custom": "custom-value", } for key, value := range validHeaders { result := validator.ValidateHeader(key, value) if !result.IsValid { t.Errorf("Expected valid header '%s'='%s' to pass validation, got errors: %v", key, value, result.Errors) } } }) t.Run("Invalid header validation", func(t *testing.T) { invalidHeaders := map[string]string{ "": "value", // Empty key "Invalid\nKey": "value", // Control characters in key "key": "value\r\n", // Control characters in value } for key, value := range invalidHeaders { result := validator.ValidateHeader(key, value) if result.IsValid { t.Errorf("Expected invalid header '%s'='%s' to fail validation", key, value) } } }) } func TestSanitizeInput(t *testing.T) { config := DefaultInputValidationConfig() logger := NewLogger("debug") validator, err := NewInputValidator(config, logger) if err != nil { t.Fatalf("Failed to create validator: %v", err) } tests := []struct { name string input string expected string maxLen int }{ { name: "Normal text", input: "Hello World", maxLen: 100, expected: "Hello World", }, { name: "Control characters", input: "text\x00with\x01control\x02chars", maxLen: 100, expected: "textwithcontrolchars", }, { name: "Truncation", input: "very long text", maxLen: 5, expected: "very ", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := validator.SanitizeInput(tt.input, tt.maxLen) if result != tt.expected { t.Errorf("Expected sanitized input '%s', got '%s'", tt.expected, result) } }) } } func TestValidateBoundaryValues(t *testing.T) { config := DefaultInputValidationConfig() logger := NewLogger("debug") validator, err := NewInputValidator(config, logger) if err != nil { t.Fatalf("Failed to create validator: %v", err) } t.Run("Valid boundary values", func(t *testing.T) { validValues := []interface{}{ int(50), int64(100), float64(75.5), } for _, value := range validValues { result := validator.ValidateBoundaryValues(value, 1, 1000) if !result.IsValid { t.Errorf("Expected valid boundary value %v to pass validation, got errors: %v", value, result.Errors) } } }) t.Run("Invalid boundary values", func(t *testing.T) { invalidValues := []interface{}{ int(-1), int64(2000), "not a number", } for _, value := range invalidValues { result := validator.ValidateBoundaryValues(value, 1, 1000) if result.IsValid { t.Errorf("Expected invalid boundary value %v to fail validation", value) } } }) } func TestDefaultInputValidationConfig(t *testing.T) { config := DefaultInputValidationConfig() if config.MaxTokenLength <= 0 { t.Error("Expected positive MaxTokenLength") } if config.MaxEmailLength <= 0 { t.Error("Expected positive MaxEmailLength") } if config.MaxUsernameLength <= 0 { t.Error("Expected positive MaxUsernameLength") } if config.MaxClaimLength <= 0 { t.Error("Expected positive MaxClaimLength") } if config.MaxHeaderLength <= 0 { t.Error("Expected positive MaxHeaderLength") } if !config.StrictMode { t.Error("Expected StrictMode to be true by default") } } func TestInputValidationHelpers(t *testing.T) { config := DefaultInputValidationConfig() logger := NewLogger("debug") validator, err := NewInputValidator(config, logger) if err != nil { t.Fatalf("Failed to create validator: %v", err) } t.Run("isValidBase64URL", func(t *testing.T) { validBase64URL := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" if !validator.isValidBase64URL(validBase64URL) { t.Error("Expected valid base64url to be recognized") } invalidBase64URL := "invalid+base64/with+padding=" if validator.isValidBase64URL(invalidBase64URL) { t.Error("Expected invalid base64url to be rejected") } }) t.Run("containsNullBytes", func(t *testing.T) { withNull := "text\x00with\x00null" if !validator.containsNullBytes(withNull) { t.Error("Expected string with null bytes to be detected") } withoutNull := "normal text" if validator.containsNullBytes(withoutNull) { t.Error("Expected string without null bytes to pass") } }) t.Run("containsControlCharacters", func(t *testing.T) { withControl := "text\x01with\x02control" if !validator.containsControlCharacters(withControl) { t.Error("Expected string with control characters to be detected") } withoutControl := "normal text" if validator.containsControlCharacters(withoutControl) { t.Error("Expected string without control characters to pass") } }) t.Run("containsPathTraversal", func(t *testing.T) { withTraversal := "../../../etc/passwd" if !validator.containsPathTraversal(withTraversal) { t.Error("Expected path traversal to be detected") } normalPath := "/normal/path" if validator.containsPathTraversal(normalPath) { t.Error("Expected normal path to pass") } }) t.Run("detectSecurityRisk", func(t *testing.T) { riskyInputs := []string{ "", "'; DROP TABLE users; --", "javascript:alert('xss')", } for _, input := range riskyInputs { if validator.detectSecurityRisk(input) == "" { t.Errorf("Expected security risk to be detected in: %s", input) } } safeInput := "normal safe text" if validator.detectSecurityRisk(safeInput) != "" { t.Error("Expected safe input to pass security check") } }) } func TestInputValidationEdgeCases(t *testing.T) { config := DefaultInputValidationConfig() logger := NewLogger("debug") validator, err := NewInputValidator(config, logger) if err != nil { t.Fatalf("Failed to create validator: %v", err) } t.Run("Empty inputs", func(t *testing.T) { // Most validations should reject empty inputs if result := validator.ValidateToken(""); result.IsValid { t.Error("Expected empty token to be rejected") } if result := validator.ValidateEmail(""); result.IsValid { t.Error("Expected empty email to be rejected") } if result := validator.ValidateURL(""); result.IsValid { t.Error("Expected empty URL to be rejected") } if result := validator.ValidateUsername(""); result.IsValid { t.Error("Expected empty username to be rejected") } }) t.Run("Very long inputs", func(t *testing.T) { longString := strings.Repeat("a", 10000) if result := validator.ValidateEmail(longString + "@domain.com"); result.IsValid { t.Error("Expected very long email to be rejected") } if result := validator.ValidateUsername(longString); result.IsValid { t.Error("Expected very long username to be rejected") } }) t.Run("Unicode handling", func(t *testing.T) { unicodeEmail := "用户@example.com" // Should handle unicode gracefully validator.ValidateEmail(unicodeEmail) // Don't fail on unicode unicodeUsername := "用户名" validator.ValidateUsername(unicodeUsername) // Don't fail on unicode }) } // TestInputValidatorValidateToken tests comprehensive token validation func TestInputValidatorValidateToken(t *testing.T) { config := DefaultInputValidationConfig() validator, _ := NewInputValidator(config, newNoOpLogger()) tests := []struct { name string token string description string expectValid bool }{ { name: "ValidJWTToken", token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyLCJpYXQiOjE1MTYyMzkwMjJ9.signature", // trufflehog:ignore expectValid: true, description: "Valid JWT token should pass validation", }, { name: "InvalidOpaqueToken", token: "opaque_access_token_that_is_long_enough_to_pass", expectValid: false, description: "Opaque token (non-JWT) should fail validation", }, { name: "EmptyToken", token: "", expectValid: false, description: "Empty token should fail validation", }, { name: "TokenWithNullBytes", token: "token_with_null\x00byte", expectValid: false, description: "Token with null bytes should fail validation", }, { name: "TokenTooLong", token: strings.Repeat("a", config.MaxTokenLength+1), expectValid: false, description: "Token exceeding max length should fail validation", }, { name: "TokenWithControlCharacters", token: "token_with_control\x01character", expectValid: false, description: "Token with control characters should fail validation", }, { name: "TokenWithHighUnicode", token: "token_with_unicode_\uffff", expectValid: false, description: "Token with high unicode characters should fail validation", }, { name: "MaliciousJWTWithExtraData", token: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig.malicious_extra", // trufflehog:ignore expectValid: false, description: "JWT with extra malicious data should fail validation", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := validator.ValidateToken(tt.token) if result.IsValid != tt.expectValid { t.Errorf("Expected valid=%v, got %v. %s", tt.expectValid, result.IsValid, tt.description) } }) } } // TestInputValidatorValidateEmail tests email validation edge cases func TestInputValidatorValidateEmail(t *testing.T) { config := DefaultInputValidationConfig() validator, _ := NewInputValidator(config, newNoOpLogger()) tests := []struct { name string email string description string expectValid bool }{ { name: "ValidEmail", email: "user@example.com", expectValid: true, description: "Valid email should pass validation", }, { name: "ValidEmailWithSubdomain", email: "user@mail.example.com", expectValid: true, description: "Valid email with subdomain should pass validation", }, { name: "EmptyEmail", email: "", expectValid: false, description: "Empty email should fail validation", }, { name: "EmailWithoutAtSign", email: "userexample.com", expectValid: false, description: "Email without @ sign should fail validation", }, { name: "EmailWithNullBytes", email: "user@example\x00.com", expectValid: false, description: "Email with null bytes should fail validation", }, { name: "EmailTooLong", email: strings.Repeat("a", config.MaxEmailLength-10) + "@example.com", expectValid: false, description: "Email exceeding max length should fail validation", }, { name: "EmailWithControlCharacters", email: "user\x01@example.com", expectValid: false, description: "Email with control characters should fail validation", }, { name: "MaliciousEmailWithScriptTag", email: "user", expectValid: false, description: "Claim with HTML/script should fail validation", }, { name: "ClaimWithExcessiveUnicode", claimName: "test", claimValue: strings.Repeat("🚀", 100), // Many unicode chars expectValid: false, description: "Claim with excessive unicode should fail validation", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := validator.ValidateClaim(tt.claimName, tt.claimValue) if result.IsValid != tt.expectValid { t.Errorf("Expected valid=%v, got %v. %s", tt.expectValid, result.IsValid, tt.description) } }) } } // TestInputValidatorValidateHeader tests HTTP header validation func TestInputValidatorValidateHeader(t *testing.T) { config := DefaultInputValidationConfig() validator, _ := NewInputValidator(config, newNoOpLogger()) tests := []struct { name string headerName string headerValue string description string expectValid bool }{ { name: "ValidHeader", headerName: "Authorization", headerValue: "Bearer token123", expectValid: true, description: "Valid header should pass validation", }, { name: "ValidContentType", headerName: "Content-Type", headerValue: "application/json", expectValid: true, description: "Valid content type header should pass validation", }, { name: "EmptyHeaderName", headerName: "", headerValue: "value", expectValid: false, description: "Empty header name should fail validation", }, { name: "HeaderWithNullBytes", headerName: "test", headerValue: "value\x00with_null", expectValid: false, description: "Header with null bytes should fail validation", }, { name: "HeaderValueTooLong", headerName: "test", headerValue: strings.Repeat("a", config.MaxHeaderLength+1), expectValid: false, description: "Header value exceeding max length should fail validation", }, { name: "HeaderWithCRLF", headerName: "test", headerValue: "value\r\nMalicious: header", expectValid: false, description: "Header with CRLF should fail validation to prevent injection", }, { name: "HeaderWithControlCharacters", headerName: "test", headerValue: "value\x01with_control", expectValid: false, description: "Header with control characters should fail validation", }, { name: "MaliciousHeaderWithHTML", headerName: "test", headerValue: "", expectValid: false, description: "Header with HTML/script should fail validation", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := validator.ValidateHeader(tt.headerName, tt.headerValue) if result.IsValid != tt.expectValid { t.Errorf("Expected valid=%v, got %v. %s", tt.expectValid, result.IsValid, tt.description) } }) } } // TestInputValidatorValidateUsername tests username validation func TestInputValidatorValidateUsername(t *testing.T) { config := DefaultInputValidationConfig() validator, _ := NewInputValidator(config, newNoOpLogger()) tests := []struct { name string username string description string expectValid bool }{ { name: "ValidUsername", username: "john_doe", expectValid: true, description: "Valid username should pass validation", }, { name: "ValidUsernameWithNumbers", username: "user123", expectValid: true, description: "Valid username with numbers should pass validation", }, { name: "EmptyUsername", username: "", expectValid: false, description: "Empty username should fail validation", }, { name: "UsernameWithNullBytes", username: "user\x00name", expectValid: false, description: "Username with null bytes should fail validation", }, { name: "UsernameTooLong", username: strings.Repeat("a", config.MaxUsernameLength+1), expectValid: false, description: "Username exceeding max length should fail validation", }, { name: "UsernameWithSpecialChars", username: "user@name", expectValid: false, description: "Username with special characters should fail validation", }, { name: "UsernameWithSpaces", username: "user name", expectValid: false, description: "Username with spaces should fail validation", }, { name: "UsernameWithControlCharacters", username: "user\x01name", expectValid: false, description: "Username with control characters should fail validation", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := validator.ValidateUsername(tt.username) if result.IsValid != tt.expectValid { t.Errorf("Expected valid=%v, got %v. %s", tt.expectValid, result.IsValid, tt.description) } }) } }