/
smalloff
/
paramvalidator
Обзор
Документация
Войти
/
smalloff
/
paramvalidator
Код
Задачи
Вики
Пакеты
0
Релизы
1
Аналитика
Безопасность
v1.0.4
paramvalidator_test.go
1 413 строк
34 KB
smalloff
rules with callback
25 сен 2025, 17:26
25 сен 2025, 17:26
36fafd5
Код
Авторство
О чём код?
package paramvalidator import ( "context" "fmt" "strings" "sync" "sync/atomic" "testing" "time" ) func TestNewParamValidator(t *testing.T) { tests := []struct { name string rulesStr string wantValid bool wantError bool }{ { name: "empty rules", rulesStr: "", wantValid: true, wantError: false, }, { name: "global rules", rulesStr: "page=[1-10]&sort=[name,date]", wantValid: true, wantError: false, }, { name: "url rules", rulesStr: "/products?page=[1-10];/users?sort=[name,date]", wantValid: true, wantError: false, }, { name: "invalid rules format", rulesStr: "page=[1-10", wantValid: false, wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rulesStr) if tt.wantError { if err == nil { t.Errorf("NewParamValidator() expected error for rules %q, but got nil", tt.rulesStr) } if pv != nil { t.Error("NewParamValidator() should return nil validator when error occurs") } } else { if err != nil { t.Errorf("NewParamValidator() unexpected error = %v for rules %q", err, tt.rulesStr) } if pv == nil { t.Error("Expected validator to be created") } } }) } } func TestParseRules(t *testing.T) { pv, err := NewParamValidator("") if err != nil { t.Fatalf("Failed to create validator: %v", err) } tests := []struct { name string rulesStr string wantError bool }{ { name: "empty string", rulesStr: "", wantError: false, }, { name: "valid global rules", rulesStr: "page=[1-10]&limit=[5,10,20]", wantError: false, }, { name: "valid url rules", rulesStr: "/products?page=[1-10];/users?limit=[5,10,20]", wantError: false, }, { name: "invalid rules format - unclosed bracket", rulesStr: "page=[1-10", wantError: true, }, { name: "invalid rules format - empty parameter name", rulesStr: "=[1-10]", wantError: true, }, { name: "invalid range format", rulesStr: "page=[1-10-20]", wantError: true, }, { name: "invalid range values", rulesStr: "page=[a-z]", wantError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := pv.ParseRules(tt.rulesStr) if tt.wantError { if err == nil { t.Errorf("ParseRules() expected error for rules %q, but got nil", tt.rulesStr) } else { t.Logf("ParseRules() correctly returned error: %v", err) } } else { if err != nil { t.Errorf("ParseRules() unexpected error = %v for rules %q", err, tt.rulesStr) } } }) } } func TestValidateURL(t *testing.T) { tests := []struct { name string rules string url string expected bool }{ { name: "empty rules - any url invalid", rules: "", url: "/test?param=value", expected: false, }, { name: "allow all params with wildcard", rules: "/api/*?*", url: "/api/test?any=value&other=123", expected: true, }, { name: "valid range parameter", rules: "/products?page=[1-10]", url: "/products?page=5", expected: true, }, { name: "invalid range parameter", rules: "/products?page=[1-10]", url: "/products?page=15", expected: false, }, { name: "valid enum parameter", rules: "/search?sort=[name,date,price]", url: "/search?sort=name", expected: true, }, { name: "invalid enum parameter", rules: "/search?sort=[name,date,price]", url: "/search?sort=invalid", expected: false, }, { name: "key-only parameter", rules: "/filter?active=[]", url: "/filter?active", expected: true, }, { name: "key-only parameter with value", rules: "/filter?active=[]", url: "/filter?active=true", expected: false, }, { name: "multiple valid parameters", rules: "/api?page=[1-10]&limit=[5,10,20]", url: "/api?page=5&limit=10", expected: true, }, { name: "one invalid parameter", rules: "/api?page=[1-10]&limit=[5,10,20]", url: "/api?page=5&limit=15", expected: false, }, { name: "global parameters", rules: "page=[1-10]&sort=[name,date]", url: "/any/path?page=5&sort=name", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.ValidateURL(tt.url) if result != tt.expected { t.Errorf("ValidateURL(%q) with rules %q = %v, expected %v", tt.url, tt.rules, result, tt.expected) } }) } } func TestValidateParam(t *testing.T) { pv, err := NewParamValidator("/products?page=[1-10]&category=[electronics,books]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } tests := []struct { name string urlPath string param string value string expected bool }{ { name: "valid page parameter", urlPath: "/products", param: "page", value: "5", expected: true, }, { name: "invalid page parameter", urlPath: "/products", param: "page", value: "15", expected: false, }, { name: "valid category parameter", urlPath: "/products", param: "category", value: "electronics", expected: true, }, { name: "invalid category parameter", urlPath: "/products", param: "category", value: "invalid", expected: false, }, { name: "unknown parameter", urlPath: "/products", param: "unknown", value: "value", expected: false, }, { name: "wrong url path - should use global rules", urlPath: "/users", param: "page", value: "5", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := pv.ValidateParam(tt.urlPath, tt.param, tt.value) if result != tt.expected { t.Errorf("ValidateParam(%q, %q, %q) = %v, expected %v", tt.urlPath, tt.param, tt.value, result, tt.expected) } }) } } func TestNormalizeURL(t *testing.T) { tests := []struct { name string rules string url string expected string }{ { name: "remove invalid parameters", rules: "/search?page=[1-10]&sort=[name,date]", url: "/search?page=5&sort=name&invalid=value", expected: "/search?page=5&sort=name", }, { name: "filter invalid values - keep valid ones", rules: "/products?page=[1-10]", url: "/products?page=15&page=5", expected: "/products?page=5", }, { name: "no valid parameters - return path only", rules: "/api?token=[valid]", url: "/api?invalid=value", expected: "/api", }, { name: "allow all parameters with wildcard", rules: "/api/*?*", url: "/api?any=value&other=123", expected: "/api?any=value&other=123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.NormalizeURL(tt.url) expected := tt.expected if result != expected { t.Errorf("NormalizeURL(%q) = %q, expected %q", tt.url, result, expected) } }) } } func TestFilterQueryParams(t *testing.T) { pv, err := NewParamValidator("/api?page=[1-10]&limit=[5,10]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } tests := []struct { name string urlPath string query string expected string }{ { name: "valid parameters", urlPath: "/api", query: "page=5&limit=10", expected: "page=5&limit=10", }, { name: "filter invalid parameters", urlPath: "/api", query: "page=5&limit=15&invalid=value", expected: "page=5", }, { name: "empty query", urlPath: "/api", query: "", expected: "", }, { name: "wrong path - no rules apply", urlPath: "/users", query: "page=5", expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := pv.FilterQueryParams(tt.urlPath, tt.query) if result != tt.expected { t.Errorf("FilterQueryParams(%q, %q) = %q, expected %q", tt.urlPath, tt.query, result, tt.expected) } }) } } func TestAddURLRule(t *testing.T) { pv, err := NewParamValidator("") if err != nil { t.Fatalf("Failed to create validator: %v", err) } rule := &ParamRule{ Name: "page", Pattern: PatternRange, Min: 1, Max: 10, } params := map[string]*ParamRule{"page": rule} pv.AddURLRule("/test", params) if !pv.ValidateURL("/test?page=5") { t.Error("Added URL rule should validate correctly") } if pv.ValidateURL("/test?page=15") { t.Error("Added URL rule should reject invalid values") } } func TestClear(t *testing.T) { pv, err := NewParamValidator("/api?page=[1-10]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } if !pv.ValidateURL("/api?page=5") { t.Error("Should validate before clear") } pv.Clear() if pv.ValidateURL("/api?page=5") { t.Error("Should not validate after clear") } } func TestURLPatternMatching(t *testing.T) { tests := []struct { name string rules string url string expected bool }{ { name: "exact match", rules: "/api/v1/users?page=[1-10]", url: "/api/v1/users?page=5", expected: true, }, { name: "wildcard prefix", rules: "/api/*?page=[1-10]", url: "/api/v1/users?page=5", expected: true, }, { name: "wildcard suffix", rules: "/api/v1/*?page=[1-10]", url: "/api/v1/users/list?page=5", expected: true, }, { name: "no match", rules: "/api/v1/users?page=[1-10]", url: "/api/v1/products?page=5", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.ValidateURL(tt.url) if result != tt.expected { t.Errorf("URL pattern matching for %q with rules %q = %v, expected %v", tt.url, tt.rules, result, tt.expected) } }) } } func TestEdgeCases(t *testing.T) { t.Run("uninitialized validator", func(t *testing.T) { pv := &ParamValidator{initialized: false} if pv.ValidateURL("/test") { t.Error("Uninitialized validator should not validate") } if pv.NormalizeURL("/test") != "/test" { t.Error("Uninitialized validator should return original URL") } }) t.Run("invalid URL", func(t *testing.T) { pv, err := NewParamValidator("/test?param=value") if err != nil { t.Fatalf("Failed to create validator: %v", err) } if pv.ValidateURL(":invalid:url:") { t.Error("Invalid URL should not validate") } }) t.Run("empty parameter values", func(t *testing.T) { pv, err := NewParamValidator("/test?param=[value1,value2]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } if pv.ValidateParam("/test", "param", "") { t.Error("Empty value should not validate against enum") } }) t.Run("nil validator", func(t *testing.T) { var pv *ParamValidator result := pv.ValidateURL("/test") if result != false { t.Error("nil validator should return false") } normalized := pv.NormalizeURL("/test") if normalized != "/test" { t.Error("nil validator should return original URL") } }) } func BenchmarkValidateURL(b *testing.B) { pv, err := NewParamValidator("/api/v1/*?page=[1-100]&limit=[10,20,50]&sort=[name,date]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } url := "/api/v1/users/list?page=50&limit=20&sort=name" b.ResetTimer() for i := 0; i < b.N; i++ { pv.ValidateURL(url) } } func BenchmarkNormalizeURL(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } url := "/api/v1/data?page=50&limit=20&invalid=value&extra=param" b.ResetTimer() for i := 0; i < b.N; i++ { pv.NormalizeURL(url) } } func BenchmarkFilterQueryParamsParallel(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } urlPath := "/api/v1/data" query := "page=50&limit=20&invalid=value&extra=param" b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { pv.FilterQueryParams(urlPath, query) } }) } func BenchmarkFilterQueryParams(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } urlPath := "/api/v1/data" query := "page=50&limit=20&invalid=value&extra=param" b.ResetTimer() for i := 0; i < b.N; i++ { pv.FilterQueryParams(urlPath, query) } } func TestConcurrentValidation(t *testing.T) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]&sort=[name,date]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } numGoroutines := 100 var wg sync.WaitGroup wg.Add(numGoroutines) errorCh := make(chan error, numGoroutines) for i := 0; i < numGoroutines; i++ { go func(id int) { defer wg.Done() switch id % 4 { case 0: if !pv.ValidateURL(fmt.Sprintf("/api/users?page=%d&limit=10", id%100+1)) { errorCh <- fmt.Errorf("goroutine %d: URL validation failed", id) } case 1: if !pv.ValidateParam("/api/users", "page", fmt.Sprintf("%d", id%100+1)) { errorCh <- fmt.Errorf("goroutine %d: param validation failed", id) } case 2: normalized := pv.NormalizeURL(fmt.Sprintf("/api/users?page=%d&invalid=value", id%100+1)) if !strings.Contains(normalized, "page=") { errorCh <- fmt.Errorf("goroutine %d: normalization failed: %s", id, normalized) } case 3: filtered := pv.FilterQueryParams("/api/users", fmt.Sprintf("page=%d&limit=10&invalid=value", id%100+1)) if !strings.Contains(filtered, "page=") { errorCh <- fmt.Errorf("goroutine %d: filtering failed: %s", id, filtered) } } }(i) } wg.Wait() close(errorCh) var errors []error for err := range errorCh { errors = append(errors, err) } if len(errors) > 0 { t.Errorf("Concurrent validation failed with %d errors:", len(errors)) for _, err := range errors { t.Error(err) } } } func TestConcurrentRuleUpdates(t *testing.T) { pv, err := NewParamValidator("") if err != nil { t.Fatalf("Failed to create validator: %v", err) } numReaders := 50 numWriters := 10 var wg sync.WaitGroup wg.Add(numReaders + numWriters) errorCh := make(chan error, numReaders+numWriters) stopCh := make(chan struct{}) for i := 0; i < numReaders; i++ { go func(id int) { defer wg.Done() for { select { case <-stopCh: return default: pv.ValidateURL(fmt.Sprintf("/test%d?param=value", id)) pv.ValidateParam("/test", "param", "value") pv.NormalizeURL(fmt.Sprintf("/test%d?param=value", id)) time.Sleep(time.Microsecond * 10) } } }(i) } for i := 0; i < numWriters; i++ { go func(id int) { defer wg.Done() for j := 0; j < 5; j++ { select { case <-stopCh: return default: urlParams := map[string]*ParamRule{ "page": { Name: "page", Pattern: PatternRange, Min: int64(id * 10), Max: int64(id*10 + 5), }, } pv.AddURLRule(fmt.Sprintf("/api%d", id), urlParams) time.Sleep(time.Millisecond) } } }(i) } time.Sleep(time.Second) close(stopCh) wg.Wait() close(errorCh) if len(errorCh) > 0 { t.Errorf("Concurrent rule updates failed with %d errors", len(errorCh)) } } func TestConcurrentParseRules(t *testing.T) { pv, err := NewParamValidator("") if err != nil { t.Fatalf("Failed to create validator: %v", err) } numGoroutines := 20 var wg sync.WaitGroup wg.Add(numGoroutines) for i := 0; i < numGoroutines; i++ { go func(id int) { defer wg.Done() var rules string if id%2 == 0 { rules = fmt.Sprintf("/api%d?page=[1-10]&sort=[name,date]", id) } else { rules = fmt.Sprintf("page=[%d-%d]&limit=[5,10,20]", id, id+10) } if err := pv.ParseRules(rules); err != nil { t.Logf("Goroutine %d: ParseRules error: %v", id, err) } pv.ValidateURL(fmt.Sprintf("/api%d?page=5", id)) }(i) } wg.Wait() if !pv.ValidateURL("/api0?page=5") && !pv.ValidateURL("/any?page=5") { t.Log("Validator state is consistent after concurrent updates") } } func TestRaceConditionDetection(t *testing.T) { pv, err := NewParamValidator("/initial?param=[value1,value2]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } done := make(chan bool) go func() { for i := 0; i < 1000; i++ { pv.ValidateURL("/initial?param=value1") pv.NormalizeURL("/initial?param=value1&extra=value") } done <- true }() go func() { for i := 0; i < 100; i++ { pv.ParseRules(fmt.Sprintf("/updated%d?newparam=[1-10]", i)) } done <- true }() <-done <-done } func TestConcurrentAccessAfterClear(t *testing.T) { pv, err := NewParamValidator("/api?page=[1-10]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } var wg sync.WaitGroup wg.Add(3) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() go func() { defer wg.Done() for i := 0; i < 5; i++ { select { case <-ctx.Done(): return default: pv.Clear() time.Sleep(time.Millisecond * 50) } } }() go func() { defer wg.Done() for i := 0; i < 50; i++ { select { case <-ctx.Done(): return default: pv.ValidateURL(fmt.Sprintf("/api?param%d=a", i%10)) time.Sleep(time.Millisecond * 10) } } }() go func() { defer wg.Done() for i := 0; i < 50; i++ { select { case <-ctx.Done(): return default: pv.NormalizeURL(fmt.Sprintf("/api?param%d=a", i%10)) time.Sleep(time.Millisecond * 10) } } }() done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: t.Log("Concurrent operations completed successfully") case <-ctx.Done(): t.Error("Test timed out - possible deadlock") } } func BenchmarkConcurrentValidation(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { pv.ValidateURL("/api/users?page=50&limit=10") pv.ValidateParam("/api/users", "page", "50") } }) } func BenchmarkConcurrentNormalization(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { pv.NormalizeURL("/api/users?page=50&limit=10&invalid=value") } }) } func TestMultipleRulesWithSemicolon(t *testing.T) { tests := []struct { name string rules string url string expected bool }{ { name: "multiple URL rules with semicolon", rules: "/products?page=[1-10];/users?sort=[name,date];/search?q=[]", url: "/products?page=5", expected: true, }, { name: "second rule in list", rules: "/products?page=[1-10];/users?sort=[name,date]", url: "/users?sort=name", expected: true, }, { name: "third rule in list", rules: "/products?page=[1-10];/users?sort=[name,date];/search?q=[]", url: "/search?q", expected: true, }, { name: "mixed global and URL rules", rules: "page=[1-10];/users?sort=[name,date]", url: "/any/path?page=5", expected: true, }, { name: "global rules work for any URL when mixed", rules: "page=[1-10];/users?sort=[name,date]", url: "/products?page=5", expected: true, }, { name: "URL-specific rules override global for specific path", rules: "page=[1-100];/products?page=[1-10]", url: "/products?page=5", expected: true, }, { name: "URL-specific rules override global - invalid case", rules: "page=[1-100];/products?page=[1-10]", url: "/products?page=50", expected: false, }, { name: "multiple rules with wildcards", rules: "/api/*?page=[1-10];/admin/*?access=[admin,superuser]", url: "/api/v1/users?page=5", expected: true, }, { name: "second wildcard rule", rules: "/api/*?page=[1-10];/admin/*?access=[admin,superuser]", url: "/admin/users?access=admin", expected: true, }, { name: "complex multiple rules", rules: "/products?category=[electronics,books]&price=[10-1000];/users?role=[admin,user]&status=[active,inactive]", url: "/products?category=electronics&price=500", expected: true, }, { name: "another complex rule", rules: "/products?category=[electronics,books]&price=[10-1000];/users?role=[admin,user]&status=[active,inactive]", url: "/users?role=admin&status=active", expected: true, }, { name: "empty rules between semicolons", rules: "/products?page=[1-10];;/users?sort=[name,date]", url: "/users?sort=name", expected: true, }, { name: "trailing semicolon", rules: "/products?page=[1-10];/users?sort=[name,date];", url: "/products?page=5", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.ValidateURL(tt.url) if result != tt.expected { t.Errorf("ValidateURL(%q) with rules %q = %v, expected %v", tt.url, tt.rules, result, tt.expected) } }) } } func TestMultipleRulesNormalization(t *testing.T) { tests := []struct { name string rules string url string expected string }{ { name: "parameters from multiple matching rules", rules: "/api/v1/*?page=[1-10];/api/v1/users?limit=[5,10]", url: "/api/v1/users?page=5&limit=10", expected: "/api/v1/users?page=5&limit=10", }, { name: "same parameter name - more specific wins", rules: "/api/*?page=[1-100];/api/users?page=[1-10]", url: "/api/users?page=5", expected: "/api/users?page=5", }, { name: "same parameter name - more specific wins with invalid value", rules: "/api/*?page=[1-100];/api/users?page=[1-10]", url: "/api/users?page=50", expected: "/api/users", }, { name: "normalize with multiple rules", rules: "/products?page=[1-10];/users?sort=[name,date]", url: "/products?page=5&invalid=value", expected: "/products?page=5", }, { name: "normalize with second rule", rules: "/products?page=[1-10];/users?sort=[name,date]", url: "/users?sort=name&invalid=value", expected: "/users?sort=name", }, { name: "normalize with global and URL rules", rules: "page=[1-10];/users?sort=[name,date]", url: "/any/path?page=5&invalid=value", expected: "/any/path?page=5", }, { name: "multiple rules with different parameters", rules: "/api/*?page=[1-10];/api/users?limit=[5,10]", url: "/api/users?page=5&limit=10&invalid=value", expected: "/api/users?page=5&limit=10", }, { name: "conflicting parameter names", rules: "/api/*?sort=[name,date];/api/users?sort=[name]", url: "/api/users?sort=name", expected: "/api/users?sort=name", }, { name: "conflicting parameter names with invalid value", rules: "/api/*?sort=[name,date];/api/users?sort=[name]", url: "/api/users?sort=date", expected: "/api/users", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.NormalizeURL(tt.url) if result != tt.expected { t.Errorf("NormalizeURL(%q) with rules %q = %q, expected %q", tt.url, tt.rules, result, tt.expected) } }) } } func TestMultipleRulesFilterQueryParams(t *testing.T) { pv, err := NewParamValidator("/api/*?page=[1-10];/api/v1/*?limit=[5,10]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } tests := []struct { name string urlPath string query string expected string }{ { name: "both rules match", urlPath: "/api/v1/users", query: "page=5&limit=10", expected: "page=5&limit=10", }, { name: "only first rule matches", urlPath: "/api/v2/users", query: "page=5&limit=10", expected: "page=5", }, { name: "only second rule matches", urlPath: "/api/v1/users", query: "limit=10&invalid=value", expected: "limit=10", }, { name: "no rules match exact path", urlPath: "/other/path", query: "page=5&limit=10", expected: "", }, { name: "conflicting parameters - more specific wins", urlPath: "/api/v1/users", query: "page=5&page=15", expected: "page=5", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := pv.FilterQueryParams(tt.urlPath, tt.query) if result != tt.expected { t.Errorf("FilterQueryParams(%q, %q) = %q, expected %q", tt.urlPath, tt.query, result, tt.expected) } }) } } func TestMultipleRulesOrderPrecedence(t *testing.T) { tests := []struct { name string rules string url string expected bool }{ { name: "more specific rule takes precedence", rules: "/api/*?page=[1-100];/api/v1/users?page=[1-10]", url: "/api/v1/users?page=5", expected: true, }, { name: "more specific rule rejects invalid value", rules: "/api/*?page=[1-100];/api/v1/users?page=[1-10]", url: "/api/v1/users?page=50", expected: false, }, { name: "less specific rule applies when more specific doesn't match", rules: "/api/*?page=[1-100];/api/v1/users?page=[1-10]", url: "/api/v2/users?page=50", expected: true, }, { name: "exact match takes precedence over wildcard", rules: "/api/v1/*?page=[1-100];/api/v1/users?page=[1-10]", url: "/api/v1/users?page=5", expected: true, }, { name: "longer path takes precedence", rules: "/api/v1?page=[1-100];/api/v1/users?page=[1-10]", url: "/api/v1/users?page=5", expected: true, }, { name: "global rules apply when no URL matches", rules: "page=[1-10];/api/v1/users?sort=[name,date]", url: "/other/path?page=5", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.ValidateURL(tt.url) if result != tt.expected { t.Errorf("ValidateURL(%q) with rules %q = %v, expected %v", tt.url, tt.rules, result, tt.expected) } }) } } func BenchmarkValidateQueryParams(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } urlPath := "/api/v1/data" query := "page=50&limit=20&invalid=value&extra=param" b.ResetTimer() for i := 0; i < b.N; i++ { pv.ValidateQueryParams(urlPath, query) } } func BenchmarkValidateQueryParamsParallel(b *testing.B) { pv, err := NewParamValidator("/api/*?page=[1-100]&limit=[10,20,50]") if err != nil { b.Fatalf("Failed to create validator: %v", err) } urlPath := "/api/v1/data" query := "page=50&limit=20" b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { pv.ValidateQueryParams(urlPath, query) } }) } func TestCallbackPattern(t *testing.T) { // Callback функция для валидации callbackFunc := func(key string, value string) bool { switch key { case "token": return value == "valid_token" case "user_id": return len(value) > 0 && len(value) <= 10 case "timestamp": return len(value) == 10 // предположим, что timestamp из 10 цифр default: return false } } tests := []struct { name string rules string url string expected bool }{ { name: "valid callback parameter", rules: "/api?token=[?]", url: "/api?token=valid_token", expected: true, }, { name: "invalid callback parameter", rules: "/api?token=[?]", url: "/api?token=invalid_token", expected: false, }, { name: "multiple callback parameters", rules: "/auth?token=[?]&user_id=[?]", url: "/auth?token=valid_token&user_id=12345", expected: true, }, { name: "mixed callback and regular rules", rules: "/data?token=[?]&page=[1-10]", url: "/data?token=valid_token&page=5", expected: true, }, { name: "callback with empty value", rules: "/api?token=[?]", url: "/api?token=", expected: false, }, { name: "callback parameter in global rules", rules: "timestamp=[?]", url: "/any/path?timestamp=1234567890", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pv, err := NewParamValidator(tt.rules, callbackFunc) if err != nil { t.Fatalf("Failed to create validator: %v", err) } result := pv.ValidateURL(tt.url) if result != tt.expected { t.Errorf("ValidateURL(%q) with rules %q = %v, expected %v", tt.url, tt.rules, result, tt.expected) } }) } } func TestCallbackWithoutFunction(t *testing.T) { // Создаем валидатор с callback паттерном, но без callback функции pv, err := NewParamValidator("/api?token=[?]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } // Без callback функции параметр с паттерном [?] должен быть невалидным if pv.ValidateURL("/api?token=any_value") { t.Error("Callback parameter should be invalid when no callback function is set") } // Устанавливаем callback функцию позже callbackFunc := func(key string, value string) bool { return value == "valid" } pv.SetCallback(callbackFunc) if !pv.ValidateURL("/api?token=valid") { t.Error("Callback parameter should be valid after setting callback function") } if pv.ValidateURL("/api?token=invalid") { t.Error("Callback parameter should be invalid for wrong values") } } func TestCallbackEdgeCases(t *testing.T) { callbackCalled := false callbackFunc := func(key string, value string) bool { callbackCalled = true return true } pv, err := NewParamValidator("/test?param=[?]", callbackFunc) if err != nil { t.Fatalf("Failed to create validator: %v", err) } t.Run("callback not called for non-callback parameters", func(t *testing.T) { callbackCalled = false // Создаем валидатор с обычным параметром (не callback) pv2, err := NewParamValidator("/test?param=[value1,value2]") if err != nil { t.Fatalf("Failed to create validator: %v", err) } pv2.SetCallback(callbackFunc) pv2.ValidateURL("/test?param=value1") if callbackCalled { t.Error("Callback should not be called for non-callback parameters") } }) t.Run("callback called with correct parameters", func(t *testing.T) { var receivedKey, receivedValue string testCallback := func(key string, value string) bool { receivedKey = key receivedValue = value return true } pv.SetCallback(testCallback) pv.ValidateURL("/test?param=test_value") if receivedKey != "param" { t.Errorf("Callback called with key %q, expected %q", receivedKey, "param") } if receivedValue != "test_value" { t.Errorf("Callback called with value %q, expected %q", receivedValue, "test_value") } }) } func TestCallbackWithNormalization(t *testing.T) { callbackFunc := func(key string, value string) bool { return value == "allowed" } pv, err := NewParamValidator("/api?secret=[?]", callbackFunc) if err != nil { t.Fatalf("Failed to create validator: %v", err) } tests := []struct { name string url string expected string }{ { name: "keep valid callback parameter", url: "/api?secret=allowed&invalid=value", expected: "/api?secret=allowed", }, { name: "remove invalid callback parameter", url: "/api?secret=disallowed&other=value", expected: "/api", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := pv.NormalizeURL(tt.url) if result != tt.expected { t.Errorf("NormalizeURL(%q) = %q, expected %q", tt.url, result, tt.expected) } }) } } func TestCallbackConcurrentAccess(t *testing.T) { var ( callbackCount int64 ) callbackFunc := func(key string, value string) bool { atomic.AddInt64(&callbackCount, 1) return value == "valid" } pv, err := NewParamValidator("/api?param=[?]", callbackFunc) if err != nil { t.Fatalf("Failed to create validator: %v", err) } var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() for i := 0; i < 100; i++ { pv.ValidateURL("/api?param=valid") } }() go func() { defer wg.Done() for i := 0; i < 100; i++ { pv.ValidateURL("/api?param=invalid") } }() wg.Wait() finalCount := atomic.LoadInt64(&callbackCount) if finalCount != 200 { t.Errorf("Callback should be called 200 times, but was called %d times", finalCount) } }