# frozen_string_literal: true require "test_helper" class PathRuleMatcherTest < ActiveSupport::TestCase setup do @user = User.create!(email_address: "test@example.com", password: "password123") # Create path segments @admin_segment = PathSegment.find_or_create_segment("admin") @wp_login_segment = PathSegment.find_or_create_segment("wp-login.php") @api_segment = PathSegment.find_or_create_segment("api") @v1_segment = PathSegment.find_or_create_segment("v1") @users_segment = PathSegment.find_or_create_segment("users") @dashboard_segment = PathSegment.find_or_create_segment("dashboard") end test "exact match - matches exact path only" do rule = Rule.create_path_pattern_rule( pattern: "/wp-login.php", match_type: "exact", action: "deny", user: @user ) # Create matching event matching_event = create_event_with_segments([@wp_login_segment.id]) assert PathRuleMatcher.matches?(rule, matching_event), "Should match exact path" # Create non-matching event (extra segment) non_matching_event = create_event_with_segments([@admin_segment.id, @wp_login_segment.id]) refute PathRuleMatcher.matches?(rule, non_matching_event), "Should not match path with extra segments" end test "prefix match - matches paths starting with pattern" do rule = Rule.create_path_pattern_rule( pattern: "/admin", match_type: "prefix", action: "deny", user: @user ) # Should match /admin event1 = create_event_with_segments([@admin_segment.id]) assert PathRuleMatcher.matches?(rule, event1), "Should match exact prefix" # Should match /admin/dashboard event2 = create_event_with_segments([@admin_segment.id, @dashboard_segment.id]) assert PathRuleMatcher.matches?(rule, event2), "Should match prefix with additional segments" # Should match /admin/users/123 event3 = create_event_with_segments([@admin_segment.id, @users_segment.id, create_segment("123").id]) assert PathRuleMatcher.matches?(rule, event3), "Should match prefix with multiple additional segments" # Should NOT match /api/admin (admin not at start) event4 = create_event_with_segments([@api_segment.id, @admin_segment.id]) refute PathRuleMatcher.matches?(rule, event4), "Should not match when pattern not at start" end test "suffix match - matches paths ending with pattern" do rule = Rule.create_path_pattern_rule( pattern: "/wp-login.php", match_type: "suffix", action: "deny", user: @user ) # Should match /wp-login.php event1 = create_event_with_segments([@wp_login_segment.id]) assert PathRuleMatcher.matches?(rule, event1), "Should match exact suffix" # Should match /admin/wp-login.php event2 = create_event_with_segments([@admin_segment.id, @wp_login_segment.id]) assert PathRuleMatcher.matches?(rule, event2), "Should match suffix with prefix segments" # Should match /backup/admin/wp-login.php backup_seg = create_segment("backup") event3 = create_event_with_segments([backup_seg.id, @admin_segment.id, @wp_login_segment.id]) assert PathRuleMatcher.matches?(rule, event3), "Should match suffix with multiple prefix segments" # Should NOT match /wp-login.php/test (suffix has extra segment) test_seg = create_segment("test") event4 = create_event_with_segments([@wp_login_segment.id, test_seg.id]) refute PathRuleMatcher.matches?(rule, event4), "Should not match when pattern not at end" end test "contains match - matches paths containing pattern" do rule = Rule.create_path_pattern_rule( pattern: "/admin", match_type: "contains", action: "deny", user: @user ) # Should match /admin event1 = create_event_with_segments([@admin_segment.id]) assert PathRuleMatcher.matches?(rule, event1), "Should match exact contains" # Should match /api/admin/users event2 = create_event_with_segments([@api_segment.id, @admin_segment.id, @users_segment.id]) assert PathRuleMatcher.matches?(rule, event2), "Should match contains in middle" # Should match /super/secret/admin/panel super_seg = create_segment("super") secret_seg = create_segment("secret") panel_seg = create_segment("panel") event3 = create_event_with_segments([super_seg.id, secret_seg.id, @admin_segment.id, panel_seg.id]) assert PathRuleMatcher.matches?(rule, event3), "Should match contains with prefix and suffix" # Should NOT match /administrator (different segment) administrator_seg = create_segment("administrator") event4 = create_event_with_segments([administrator_seg.id]) refute PathRuleMatcher.matches?(rule, event4), "Should not match different segment" end test "contains match with multi-segment pattern" do rule = Rule.create_path_pattern_rule( pattern: "/api/admin", match_type: "contains", action: "deny", user: @user ) # Should match /api/admin event1 = create_event_with_segments([@api_segment.id, @admin_segment.id]) assert PathRuleMatcher.matches?(rule, event1), "Should match exact contains" # Should match /v1/api/admin/users event2 = create_event_with_segments([@v1_segment.id, @api_segment.id, @admin_segment.id, @users_segment.id]) assert PathRuleMatcher.matches?(rule, event2), "Should match consecutive segments in middle" # Should NOT match /api/v1/admin (segments not consecutive) event3 = create_event_with_segments([@api_segment.id, @v1_segment.id, @admin_segment.id]) refute PathRuleMatcher.matches?(rule, event3), "Should not match non-consecutive segments" end test "case insensitive matching through PathSegment normalization" do # PathSegment.find_or_create_segment normalizes to lowercase rule = Rule.create_path_pattern_rule( pattern: "/Admin/Users", # Mixed case match_type: "exact", action: "deny", user: @user ) # Event with lowercase path should match event = create_event_with_segments([@admin_segment.id, @users_segment.id]) assert PathRuleMatcher.matches?(rule, event), "Should match case-insensitively" end test "matching_rules returns all matching rules" do rule1 = Rule.create_path_pattern_rule(pattern: "/admin", match_type: "prefix", action: "deny", user: @user) rule2 = Rule.create_path_pattern_rule(pattern: "/admin/users", match_type: "exact", action: "allow", user: @user) rule3 = Rule.create_path_pattern_rule(pattern: "/api", match_type: "prefix", action: "deny", user: @user) event = create_event_with_segments([@admin_segment.id, @users_segment.id]) matching = PathRuleMatcher.matching_rules(event) assert_includes matching, rule1, "Should include prefix rule" assert_includes matching, rule2, "Should include exact rule" refute_includes matching, rule3, "Should not include non-matching rule" end test "evaluate returns first matching action" do Rule.create_path_pattern_rule(pattern: "/admin", match_type: "prefix", action: "deny", user: @user) event = create_event_with_segments([@admin_segment.id, @dashboard_segment.id]) action = PathRuleMatcher.evaluate(event) assert_equal "deny", action, "Should return deny action" end test "evaluate returns allow for non-matching event" do Rule.create_path_pattern_rule(pattern: "/admin", match_type: "exact", action: "deny", user: @user) event = create_event_with_segments([@api_segment.id]) action = PathRuleMatcher.evaluate(event) assert_equal "allow", action, "Should return allow for non-matching event" end test "does not match disabled rules" do rule = Rule.create_path_pattern_rule(pattern: "/admin", match_type: "exact", action: "deny", user: @user) rule.update!(enabled: false) event = create_event_with_segments([@admin_segment.id]) matching = PathRuleMatcher.matching_rules(event) assert_empty matching, "Should not match disabled rules" end test "does not match expired rules" do rule = Rule.create_path_pattern_rule(pattern: "/admin", match_type: "exact", action: "deny", user: @user) rule.update!(expires_at: 1.hour.ago) event = create_event_with_segments([@admin_segment.id]) matching = PathRuleMatcher.matching_rules(event) assert_empty matching, "Should not match expired rules" end private def create_event_with_segments(segment_ids) Event.new( request_id: SecureRandom.uuid, timestamp: Time.current, request_segment_ids: segment_ids, ip_address: "1.2.3.4" ) end def create_segment(text) PathSegment.find_or_create_segment(text) end end