package policy_engine import ( "testing" ) func TestMatchesWildcard(t *testing.T) { tests := []struct { name string pattern string str string expected bool }{ // Basic functionality tests { name: "Exact match", pattern: "test", str: "test", expected: true, }, { name: "Single wildcard", pattern: "*", str: "anything", expected: true, }, { name: "Empty string with wildcard", pattern: "*", str: "", expected: true, }, // Star (*) wildcard tests { name: "Prefix wildcard", pattern: "test*", str: "test123", expected: true, }, { name: "Suffix wildcard", pattern: "*test", str: "123test", expected: true, }, { name: "Middle wildcard", pattern: "test*123", str: "testABC123", expected: true, }, { name: "Multiple wildcards", pattern: "test*abc*123", str: "testXYZabcDEF123", expected: true, }, { name: "No match", pattern: "test*", str: "other", expected: false, }, // Question mark (?) wildcard tests { name: "Single question mark", pattern: "test?", str: "test1", expected: true, }, { name: "Multiple question marks", pattern: "test??", str: "test12", expected: true, }, { name: "Question mark no match", pattern: "test?", str: "test12", expected: false, }, { name: "Mixed wildcards", pattern: "test*abc?def", str: "testXYZabc1def", expected: true, }, // Edge cases { name: "Empty pattern", pattern: "", str: "", expected: true, }, { name: "Empty pattern with string", pattern: "", str: "test", expected: false, }, { name: "Pattern with string empty", pattern: "test", str: "", expected: false, }, // Special characters { name: "Pattern with regex special chars", pattern: "test[abc]", str: "test[abc]", expected: true, }, { name: "Pattern with dots", pattern: "test.txt", str: "test.txt", expected: true, }, { name: "Pattern with dots and wildcard", pattern: "*.txt", str: "test.txt", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := MatchesWildcard(tt.pattern, tt.str) if result != tt.expected { t.Errorf("Pattern %s against %s: expected %v, got %v", tt.pattern, tt.str, tt.expected, result) } }) } } func TestWildcardMatcher(t *testing.T) { tests := []struct { name string pattern string strings []string expected []bool }{ { name: "Simple star pattern", pattern: "test*", strings: []string{"test", "test123", "testing", "other"}, expected: []bool{true, true, true, false}, }, { name: "Question mark pattern", pattern: "test?", strings: []string{"test1", "test2", "test", "test12"}, expected: []bool{true, true, false, false}, }, { name: "Mixed pattern", pattern: "*.txt", strings: []string{"file.txt", "test.txt", "file.doc", "txt"}, expected: []bool{true, true, false, false}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { matcher, err := NewWildcardMatcher(tt.pattern) if err != nil { t.Fatalf("Failed to create matcher: %v", err) } for i, str := range tt.strings { result := matcher.Match(str) if result != tt.expected[i] { t.Errorf("Pattern %s against %s: expected %v, got %v", tt.pattern, str, tt.expected[i], result) } } }) } } func TestCompileWildcardPattern(t *testing.T) { tests := []struct { name string pattern string input string want bool }{ {"Star wildcard", "s3:Get*", "s3:GetObject", true}, {"Question mark wildcard", "s3:Get?bject", "s3:GetObject", true}, {"Mixed wildcards", "s3:*Object*", "s3:GetObjectAcl", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { regex, err := CompileWildcardPattern(tt.pattern) if err != nil { t.Errorf("CompileWildcardPattern() error = %v", err) return } got := regex.MatchString(tt.input) if got != tt.want { t.Errorf("CompileWildcardPattern() = %v, want %v", got, tt.want) } }) } } // BenchmarkWildcardMatchingPerformance demonstrates the performance benefits of caching func BenchmarkWildcardMatchingPerformance(b *testing.B) { patterns := []string{ "s3:Get*", "s3:Put*", "s3:Delete*", "s3:List*", "arn:aws:s3:::bucket/*", "arn:aws:s3:::bucket/prefix*", "user:*", "user:admin-*", } inputs := []string{ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "arn:aws:s3:::bucket/file.txt", "arn:aws:s3:::bucket/prefix/file.txt", "user:admin", "user:admin-john", } b.Run("WithoutCache", func(b *testing.B) { for i := 0; i < b.N; i++ { for _, pattern := range patterns { for _, input := range inputs { MatchesWildcard(pattern, input) } } } }) b.Run("WithCache", func(b *testing.B) { for i := 0; i < b.N; i++ { for _, pattern := range patterns { for _, input := range inputs { FastMatchesWildcard(pattern, input) } } } }) } // BenchmarkWildcardMatcherReuse demonstrates the performance benefits of reusing WildcardMatcher instances func BenchmarkWildcardMatcherReuse(b *testing.B) { pattern := "s3:Get*" input := "s3:GetObject" b.Run("NewMatcherEveryTime", func(b *testing.B) { for i := 0; i < b.N; i++ { matcher, _ := NewWildcardMatcher(pattern) matcher.Match(input) } }) b.Run("CachedMatcher", func(b *testing.B) { for i := 0; i < b.N; i++ { matcher, _ := GetCachedWildcardMatcher(pattern) matcher.Match(input) } }) } // TestWildcardMatcherCaching verifies that caching works correctly func TestWildcardMatcherCaching(t *testing.T) { pattern := "s3:Get*" // Get the first matcher matcher1, err := GetCachedWildcardMatcher(pattern) if err != nil { t.Fatalf("Failed to get cached matcher: %v", err) } // Get the second matcher - should be the same instance matcher2, err := GetCachedWildcardMatcher(pattern) if err != nil { t.Fatalf("Failed to get cached matcher: %v", err) } // Check that they're the same instance (same pointer) if matcher1 != matcher2 { t.Errorf("Expected same matcher instance, got different instances") } // Test that both matchers work correctly testInput := "s3:GetObject" if !matcher1.Match(testInput) { t.Errorf("First matcher failed to match %s", testInput) } if !matcher2.Match(testInput) { t.Errorf("Second matcher failed to match %s", testInput) } } // TestFastMatchesWildcard verifies that the fast matching function works correctly func TestFastMatchesWildcard(t *testing.T) { tests := []struct { pattern string input string want bool }{ {"s3:Get*", "s3:GetObject", true}, {"s3:Put*", "s3:GetObject", false}, {"arn:aws:s3:::bucket/*", "arn:aws:s3:::bucket/file.txt", true}, {"user:admin-*", "user:admin-john", true}, {"user:admin-*", "user:guest-john", false}, } for _, tt := range tests { t.Run(tt.pattern+"_"+tt.input, func(t *testing.T) { got := FastMatchesWildcard(tt.pattern, tt.input) if got != tt.want { t.Errorf("FastMatchesWildcard(%q, %q) = %v, want %v", tt.pattern, tt.input, got, tt.want) } }) } } // TestWildcardMatcherCacheBounding tests the bounded cache functionality func TestWildcardMatcherCacheBounding(t *testing.T) { // Clear cache before test wildcardMatcherCache.ClearCache() // Get original max size originalMaxSize := wildcardMatcherCache.maxSize // Set a small max size for testing wildcardMatcherCache.maxSize = 3 defer func() { wildcardMatcherCache.maxSize = originalMaxSize wildcardMatcherCache.ClearCache() }() // Add patterns up to max size patterns := []string{"pattern1", "pattern2", "pattern3"} for _, pattern := range patterns { _, err := GetCachedWildcardMatcher(pattern) if err != nil { t.Fatalf("Failed to get cached matcher for %s: %v", pattern, err) } } // Verify cache size size, maxSize := wildcardMatcherCache.GetCacheStats() if size != 3 { t.Errorf("Expected cache size 3, got %d", size) } if maxSize != 3 { t.Errorf("Expected max size 3, got %d", maxSize) } // Add another pattern, should evict the least recently used _, err := GetCachedWildcardMatcher("pattern4") if err != nil { t.Fatalf("Failed to get cached matcher for pattern4: %v", err) } // Cache should still be at max size size, _ = wildcardMatcherCache.GetCacheStats() if size != 3 { t.Errorf("Expected cache size 3 after eviction, got %d", size) } // The first pattern should have been evicted wildcardMatcherCache.mu.RLock() if _, exists := wildcardMatcherCache.matchers["pattern1"]; exists { t.Errorf("Expected pattern1 to be evicted, but it still exists") } if _, exists := wildcardMatcherCache.matchers["pattern4"]; !exists { t.Errorf("Expected pattern4 to be in cache, but it doesn't exist") } wildcardMatcherCache.mu.RUnlock() } // TestWildcardMatcherCacheLRU tests the LRU eviction policy func TestWildcardMatcherCacheLRU(t *testing.T) { // Clear cache before test wildcardMatcherCache.ClearCache() // Get original max size originalMaxSize := wildcardMatcherCache.maxSize // Set a small max size for testing wildcardMatcherCache.maxSize = 3 defer func() { wildcardMatcherCache.maxSize = originalMaxSize wildcardMatcherCache.ClearCache() }() // Add patterns to fill cache patterns := []string{"pattern1", "pattern2", "pattern3"} for _, pattern := range patterns { _, err := GetCachedWildcardMatcher(pattern) if err != nil { t.Fatalf("Failed to get cached matcher for %s: %v", pattern, err) } } // Access pattern1 to make it most recently used _, err := GetCachedWildcardMatcher("pattern1") if err != nil { t.Fatalf("Failed to access pattern1: %v", err) } // Add another pattern, should evict pattern2 (now least recently used) _, err = GetCachedWildcardMatcher("pattern4") if err != nil { t.Fatalf("Failed to get cached matcher for pattern4: %v", err) } // pattern1 should still be in cache (was accessed recently) // pattern2 should be evicted (was least recently used) wildcardMatcherCache.mu.RLock() if _, exists := wildcardMatcherCache.matchers["pattern1"]; !exists { t.Errorf("Expected pattern1 to remain in cache (most recently used)") } if _, exists := wildcardMatcherCache.matchers["pattern2"]; exists { t.Errorf("Expected pattern2 to be evicted (least recently used)") } if _, exists := wildcardMatcherCache.matchers["pattern3"]; !exists { t.Errorf("Expected pattern3 to remain in cache") } if _, exists := wildcardMatcherCache.matchers["pattern4"]; !exists { t.Errorf("Expected pattern4 to be in cache") } wildcardMatcherCache.mu.RUnlock() } // TestWildcardMatcherCacheClear tests the cache clearing functionality func TestWildcardMatcherCacheClear(t *testing.T) { // Add some patterns to cache patterns := []string{"pattern1", "pattern2", "pattern3"} for _, pattern := range patterns { _, err := GetCachedWildcardMatcher(pattern) if err != nil { t.Fatalf("Failed to get cached matcher for %s: %v", pattern, err) } } // Verify cache has patterns size, _ := wildcardMatcherCache.GetCacheStats() if size == 0 { t.Errorf("Expected cache to have patterns before clearing") } // Clear cache wildcardMatcherCache.ClearCache() // Verify cache is empty size, _ = wildcardMatcherCache.GetCacheStats() if size != 0 { t.Errorf("Expected cache to be empty after clearing, got size %d", size) } }