Fix some blocked/allow laggards after migrating. Add DuckDB for outstanding analyitcs performance. Start adding an import for all bot networks
This commit is contained in:
216
test/services/path_rule_matcher_test.rb
Normal file
216
test/services/path_rule_matcher_test.rb
Normal file
@@ -0,0 +1,216 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user