Many updates

This commit is contained in:
Dan Milne
2025-11-13 14:42:43 +11:00
parent 5e5198f113
commit df94ac9720
41 changed files with 4760 additions and 516 deletions

View File

@@ -0,0 +1,229 @@
# frozen_string_literal: true
require "test_helper"
class Api::EventsControllerTest < ActionDispatch::IntegrationTest
def setup
@dsn = Dsn.create!(name: "Test DSN", key: "test-api-key-1234567890abcdef")
@disabled_dsn = Dsn.create!(name: "Disabled DSN", key: "disabled-key-1234567890abcdef", enabled: false)
@sample_event_data = {
"timestamp" => Time.current.iso8601,
"method" => "GET",
"path" => "/api/test",
"status" => 200,
"ip" => "192.168.1.100",
"user_agent" => "TestAgent/1.0"
}
end
test "should create event with valid DSN via query parameter" do
post api_events_path,
params: @sample_event_data.merge(baffle_key: @dsn.key),
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
assert_not_nil json_response["rule_version"]
assert_not_nil response.headers['X-Rule-Version']
end
test "should create event with valid DSN via Authorization header" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: @sample_event_data,
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
end
test "should create event with valid DSN via X-Baffle-Auth header" do
post api_events_path,
headers: { "X-Baffle-Auth" => "Baffle baffle_key=#{@dsn.key}, baffle_version=1" },
params: @sample_event_data,
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
end
test "should create event with valid DSN via Basic auth" do
credentials = Base64.strict_encode64("#{@dsn.key}:password")
post api_events_path,
headers: { "Authorization" => "Basic #{credentials}" },
params: @sample_event_data,
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
end
test "should create event with form encoded data" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: @sample_event_data,
as: :url_encoded
assert_response :success
end
test "should include rules in response when agent has no version" do
# Create some test rules
Rule.create!(action: "block", pattern_type: "ip", pattern: "192.168.1.0/24", reason: "Test rule")
Rule.create!(action: "allow", pattern_type: "ip", pattern: "10.0.0.0/8", reason: "Allow internal")
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: @sample_event_data,
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
assert json_response["rules_changed"]
assert_not_nil json_response["rules"]
assert_equal 2, json_response["rules"].length
end
test "should include only new rules when agent has old version" do
# Create rules with different versions
old_rule = Rule.create!(action: "block", pattern_type: "ip", pattern: "192.168.1.0/24", reason: "Old rule", version: 1)
new_rule = Rule.create!(action: "block", pattern_type: "ip", pattern: "203.0.113.0/24", reason: "New rule", version: 2)
event_data_with_version = @sample_event_data.merge("last_rule_sync" => 1)
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: event_data_with_version,
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
assert json_response["rules_changed"]
assert_equal 1, json_response["rules"].length
assert_equal "203.0.113.0/24", json_response["rules"].first["pattern"]
end
test "should not include rules when agent has latest version" do
# Create a rule and get its version
rule = Rule.create!(action: "block", pattern_type: "ip", pattern: "192.168.1.0/24", reason: "Test rule")
latest_version = Rule.latest_version
event_data_with_latest_version = @sample_event_data.merge("last_rule_sync" => latest_version)
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: event_data_with_latest_version,
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
assert_not json_response["rules_changed"]
assert_nil json_response["rules"]
end
test "should return unauthorized with invalid DSN key" do
post api_events_path,
headers: { "Authorization" => "Bearer invalid-key-1234567890abcdef" },
params: @sample_event_data,
as: :json
assert_response :unauthorized
end
test "should return unauthorized with disabled DSN" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@disabled_dsn.key}" },
params: @sample_event_data,
as: :json
assert_response :unauthorized
end
test "should return unauthorized with no authentication" do
post api_events_path,
params: @sample_event_data,
as: :json
assert_response :unauthorized
end
test "should return bad request with invalid JSON" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: "invalid json {",
as: :json
assert_response :bad_request
end
test "should handle empty request body gracefully" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: {},
as: :json
assert_response :success
json_response = JSON.parse(response.body)
assert json_response["success"]
end
test "should set sampling headers in response" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: @sample_event_data,
as: :json
assert_response :success
assert_not_nil response.headers['X-Sample-Rate']
assert_not_nil response.headers['X-Sample-Until']
end
test "should set rule version header in response" do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: @sample_event_data,
as: :json
assert_response :success
assert_not_nil response.headers['X-Rule-Version']
assert_match /^\d+$/, response.headers['X-Rule-Version']
end
test "should handle large event payloads" do
large_payload = @sample_event_data.merge(
"large_field" => "x" * 10000, # 10KB of data
"headers" => { "user-agent" => "TestAgent", "accept" => "*/*" },
"custom_data" => Hash[*(1..100).map { |i| ["key#{i}", "value#{i}"] }.flatten]
)
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: large_payload,
as: :json
assert_response :success
end
test "should process event asynchronously" do
# Clear any existing jobs
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = false
assert_difference 'ProcessWafEventJob.jobs.count', 1 do
post api_events_path,
headers: { "Authorization" => "Bearer #{@dsn.key}" },
params: @sample_event_data,
as: :json
end
assert_response :success
end
end

View File

@@ -10,18 +10,21 @@ module Api
key: "test-key-#{SecureRandom.hex(8)}"
)
@rule1_network_range = NetworkRange.create!(cidr: "10.0.0.0/8")
@rule1 = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
source: "manual"
waf_rule_type: "network",
waf_action: "deny",
network_range: @rule1_network_range,
source: "manual",
user: users(:one)
)
@rule2 = Rule.create!(
rule_type: "rate_limit",
action: "rate_limit",
waf_rule_type: "rate_limit",
waf_action: "rate_limit",
conditions: { cidr: "0.0.0.0/0", scope: "global" },
metadata: { limit: 100, window: 60 }
metadata: { limit: 100, window: 60 },
user: users(:one)
)
end
@@ -68,8 +71,8 @@ module Api
assert_equal 2, json["rules"].length
rule = json["rules"].find { |r| r["id"] == @rule1.id }
assert_equal "network_v4", rule["rule_type"]
assert_equal "deny", rule["action"]
assert_equal "network", rule["waf_rule_type"]
assert_equal "deny", rule["waf_action"]
assert_equal({ "cidr" => "10.0.0.0/8" }, rule["conditions"])
assert_equal 8, rule["priority"]
end
@@ -159,24 +162,27 @@ module Api
test "rules are ordered by updated_at for sync" do
# Create rules with different timestamps
oldest_range = NetworkRange.create!(cidr: "192.168.1.0/24")
oldest = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.1.0/24" }
waf_rule_type: "network",
waf_action: "deny",
network_range: oldest_range
)
oldest.update_column(:updated_at, 3.hours.ago)
middle_range = NetworkRange.create!(cidr: "192.168.2.0/24")
middle = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.2.0/24" }
waf_rule_type: "network",
waf_action: "deny",
network_range: middle_range
)
middle.update_column(:updated_at, 2.hours.ago)
newest_range = NetworkRange.create!(cidr: "192.168.3.0/24")
newest = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.3.0/24" }
waf_rule_type: "network",
waf_action: "deny",
network_range: newest_range
)
get "/api/rules?since=#{4.hours.ago.iso8601}"

2
test/fixtures/ipv4_ranges.yml.bak vendored Normal file
View File

@@ -0,0 +1,2 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# Empty fixtures - tests create their own data

2
test/fixtures/ipv6_ranges.yml.bak vendored Normal file
View File

@@ -0,0 +1,2 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
# Empty fixtures - tests create their own data

1
test/fixtures/rule_sets.yml.bak vendored Normal file
View File

@@ -0,0 +1 @@
# Empty fixtures

9
test/fixtures/settings.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
key: MyString1
value: MyString1
two:
key: MyString2
value: MyString2

23
test/fixtures/waf_policies.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: Policy One
description: MyText
policy_type: MyString
policy_action: MyString
targets:
enabled: false
expires_at: 2025-11-10 13:30:53
user: one
additional_data:
two:
name: Policy Two
description: MyText
policy_type: MyString
policy_action: MyString
targets:
enabled: false
expires_at: 2025-11-10 13:30:53
user: two
additional_data:

View File

@@ -0,0 +1,292 @@
# frozen_string_literal: true
require "test_helper"
# Custom test class that avoids fixture loading issues
class WafPolicyBrazilTest < Minitest::Test
def setup
# Clean up any existing data
Event.delete_all
Rule.delete_all
NetworkRange.delete_all
WafPolicy.delete_all
User.delete_all
@user = User.create!(email_address: "test@example.com", password: "password")
# Create a WAF policy to block Brazil
@brazil_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'deny',
user: @user,
name: "Block Brazil"
)
# Sample event data for a Brazilian IP
@brazil_ip = "177.104.144.0" # Known Brazilian IP
@brazil_event_data = {
"request_id" => "brazil-test-123",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => @brazil_ip,
"method" => "GET",
"path" => "/api/test",
"headers" => {
"host" => "example.com",
"user-agent" => "TestAgent/1.0"
}
},
"response" => {
"status_code" => 200,
"duration_ms" => 150
},
"waf_action" => "allow",
"server_name" => "test-server",
"environment" => "test",
"geo" => {
"country_code" => "BR",
"city" => "São Paulo"
},
"agent" => {
"name" => "baffle-agent",
"version" => "1.0.0"
}
}
end
def teardown
Event.delete_all
Rule.delete_all
NetworkRange.delete_all
WafPolicy.delete_all
User.delete_all
end
def test_brazil_waf_policy_generates_block_rule_when_brazilian_event_is_processed
# Process the Brazilian event
event = Event.create_from_waf_payload!("brazil-test", @brazil_event_data)
assert event.persisted?
# Extract country code from payload geo data
country_code = event.payload.dig("geo", "country_code")
assert_equal "BR", country_code
assert_equal @brazil_ip, event.ip_address.to_s
# Ensure network range exists for the Brazilian IP
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
assert network_range.persisted?
assert network_range.contains_ip?(@brazil_ip)
# Set the country on the network range to simulate geo-lookup
network_range.update!(country: 'BR')
# Process WAF policies for this network range
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that a blocking rule was generated
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: @brazil_policy
)
assert_equal 1, generated_rules.count, "Should have generated exactly one blocking rule"
rule = generated_rules.first
assert_equal 'deny', rule.action
assert_equal network_range, rule.network_range
assert_equal @brazil_policy, rule.waf_policy
assert_equal "policy", rule.source
assert rule.enabled?, "Generated rule should be enabled"
# Verify rule metadata contains policy information
metadata = rule.metadata
assert_equal @brazil_policy.id, metadata['generated_by_policy']
assert_equal "Block Brazil", metadata['policy_name']
assert_equal "country", metadata['policy_type']
assert_equal "country", metadata['matched_field']
assert_equal "BR", metadata['matched_value']
end
def test_non_brazilian_event_does_not_generate_block_rule_from_brazil_policy
# Create event data for a US IP
us_ip = "8.8.8.8" # Known US IP
us_event_data = @brazil_event_data.dup
us_event_data["event_id"] = "us-test-123"
us_event_data["request"]["ip"] = us_ip
us_event_data["geo"]["country_code"] = "US"
us_event_data["geo"]["city"] = "Mountain View"
# Process the US event
event = Event.create_from_waf_payload!("us-test", us_event_data)
assert event.persisted?
# Extract country code from payload geo data
country_code = event.payload.dig("geo", "country_code")
assert_equal "US", country_code
assert_equal us_ip, event.ip_address.to_s
# Ensure network range exists for the US IP
network_range = NetworkRangeGenerator.find_or_create_for_ip(us_ip)
assert network_range.persisted?
network_range.update!(country: 'US')
# Process WAF policies for this network range
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that no blocking rule was generated
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: @brazil_policy
)
assert_equal 0, generated_rules.count, "Should not have generated any blocking rules for US IP"
end
def test_multiple_country_policies_generate_rules_for_matching_countries_only
# Create additional policy to block China
china_policy = WafPolicy.create_country_policy(
['CN'],
policy_action: 'deny',
user: @user,
name: "Block China"
)
# Create Chinese IP event
china_ip = "220.181.38.148" # Known Chinese IP
china_event_data = @brazil_event_data.dup
china_event_data["event_id"] = "china-test-123"
china_event_data["request"]["ip"] = china_ip
china_event_data["geo"]["country_code"] = "CN"
china_event_data["geo"]["city"] = "Beijing"
# Process Chinese event
china_event = Event.create_from_waf_payload!("china-test", china_event_data)
china_network_range = NetworkRangeGenerator.find_or_create_for_ip(china_ip)
china_network_range.update!(country: 'CN')
# Process Brazilian event (from setup)
brazil_event = Event.create_from_waf_payload!("brazil-test", @brazil_event_data)
brazil_network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
brazil_network_range.update!(country: 'BR')
# Process WAF policies for both network ranges
ProcessWafPoliciesJob.perform_now(network_range: brazil_network_range, event: brazil_event)
ProcessWafPoliciesJob.perform_now(network_range: china_network_range, event: china_event)
# Verify Brazil IP matched Brazil policy only
brazil_rules = Rule.where(network_range: brazil_network_range)
assert_equal 1, brazil_rules.count
brazil_rule = brazil_rules.first
assert_equal @brazil_policy, brazil_rule.waf_policy
assert_equal "BR", brazil_rule.metadata['matched_value']
# Verify China IP matched China policy only
china_rules = Rule.where(network_range: china_network_range)
assert_equal 1, china_rules.count
china_rule = china_rules.first
assert_equal china_policy, china_rule.waf_policy
assert_equal "CN", china_rule.metadata['matched_value']
end
def test_policy_expiration_prevents_rule_generation
# Create an expired Brazil policy
expired_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'deny',
user: @user,
name: "Expired Brazil Block",
expires_at: 1.day.ago
)
# Process Brazilian event
event = Event.create_from_waf_payload!("expired-test", @brazil_event_data)
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
network_range.update!(country: 'BR')
# Process WAF policies
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that no rule was generated from expired policy
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: expired_policy
)
assert_equal 0, generated_rules.count, "Expired policy should not generate rules"
end
def test_disabled_policy_prevents_rule_generation
# Create a disabled Brazil policy
disabled_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'deny',
user: @user,
name: "Disabled Brazil Block"
)
disabled_policy.update!(enabled: false)
# Process Brazilian event
event = Event.create_from_waf_payload!("disabled-test", @brazil_event_data)
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
network_range.update!(country: 'BR')
# Process WAF policies
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that no rule was generated from disabled policy
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: disabled_policy
)
assert_equal 0, generated_rules.count, "Disabled policy should not generate rules"
end
def test_policy_action_types_are_correctly_applied_to_generated_rules
# Test different policy actions
redirect_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'redirect',
user: @user,
name: "Redirect Brazil",
additional_data: {
'redirect_url' => 'https://example.com/blocked',
'redirect_status' => 302
}
)
challenge_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'challenge',
user: @user,
name: "Challenge Brazil",
additional_data: {
'challenge_type' => 'captcha',
'challenge_message' => 'Please verify you are human'
}
)
# Process Brazilian event for redirect policy
event = Event.create_from_waf_payload!("redirect-test", @brazil_event_data)
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
network_range.update!(country: 'BR')
# Manually create rule for redirect policy to test metadata handling
redirect_rule = redirect_policy.create_rule_for_network_range(network_range)
assert redirect_rule.persisted?
assert_equal 'redirect', redirect_rule.action
assert_equal 'https://example.com/blocked', redirect_rule.redirect_url
assert_equal 302, redirect_rule.redirect_status
# Manually create rule for challenge policy to test metadata handling
challenge_rule = challenge_policy.create_rule_for_network_range(network_range)
assert challenge_rule.persisted?
assert_equal 'challenge', challenge_rule.action
assert_equal 'captcha', challenge_rule.challenge_type
assert_equal 'Please verify you are human', challenge_rule.challenge_message
end
end

View File

@@ -0,0 +1,290 @@
# frozen_string_literal: true
require "test_helper"
class WafPolicyIntegrationTest < ActiveSupport::TestCase
# Don't load any fixtures
self.use_transactional_tests = true
def setup
# Clean up any existing data
Event.delete_all
Rule.delete_all
NetworkRange.delete_all
WafPolicy.delete_all
User.delete_all
Project.delete_all
@user = User.create!(email_address: "test@example.com", password: "password")
@project = Project.create!(name: "Test Project", slug: "test-project", public_key: "test-key-123456")
# Create a WAF policy to block Brazil
@brazil_policy = WafPolicy.create_country_policy(
['BR'],
policy_policy_action: 'deny',
user: @user,
name: "Block Brazil"
)
# Sample event data for a Brazilian IP
@brazil_ip = "177.104.144.10" # Known Brazilian IP
@brazil_event_data = {
"request_id" => "brazil-test-123",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => @brazil_ip,
"method" => "GET",
"path" => "/api/test",
"headers" => {
"host" => "example.com",
"user-agent" => "TestAgent/1.0"
}
},
"response" => {
"status_code" => 200,
"duration_ms" => 150
},
"waf_action" => "allow",
"server_name" => "test-server",
"environment" => "test",
"geo" => {
"country_code" => "BR",
"city" => "São Paulo"
},
"agent" => {
"name" => "baffle-agent",
"version" => "1.0.0"
}
}
end
def teardown
Event.delete_all
Rule.delete_all
NetworkRange.delete_all
WafPolicy.delete_all
User.delete_all
end
test "Brazil WAF policy generates block rule when Brazilian event is processed" do
# Process the Brazilian event
event = Event.create_from_waf_payload!("brazil-test", @brazil_event_data)
assert event.persisted?
assert_equal "BR", event.country_code
assert_equal @brazil_ip, event.ip_address
# Ensure network range exists for the Brazilian IP
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
assert network_range.persisted?
assert network_range.contains_ip?(@brazil_ip)
# Set the country on the network range to simulate geo-lookup
network_range.update!(country: 'BR')
# Process WAF policies for this network range
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that a blocking rule was generated
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: @brazil_policy
)
assert_equal 1, generated_rules.count, "Should have generated exactly one blocking rule"
rule = generated_rules.first
assert_equal 'deny', rule.action
assert_equal network_range, rule.network_range
assert_equal @brazil_policy, rule.waf_policy
assert_equal "policy:Block Brazil", rule.source
assert rule.enabled?, "Generated rule should be enabled"
# Verify rule metadata contains policy information
metadata = rule.metadata
assert_equal @brazil_policy.id, metadata['generated_by_policy']
assert_equal "Block Brazil", metadata['policy_name']
assert_equal "country", metadata['policy_type']
assert_equal "country", metadata['matched_field']
assert_equal "BR", metadata['matched_value']
end
test "Non-Brazilian event does not generate block rule from Brazil policy" do
# Create event data for a US IP
us_ip = "8.8.8.8" # Known US IP
us_event_data = @brazil_event_data.dup
us_event_data["event_id"] = "us-test-123"
us_event_data["request"]["ip"] = us_ip
us_event_data["geo"]["country_code"] = "US"
us_event_data["geo"]["city"] = "Mountain View"
# Process the US event
event = Event.create_from_waf_payload!("us-test", us_event_data)
assert event.persisted?
assert_equal "US", event.country_code
assert_equal us_ip, event.ip_address
# Ensure network range exists for the US IP
network_range = NetworkRangeGenerator.find_or_create_for_ip(us_ip)
assert network_range.persisted?
network_range.update!(country: 'US')
# Process WAF policies for this network range
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that no blocking rule was generated
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: @brazil_policy
)
assert_equal 0, generated_rules.count, "Should not have generated any blocking rules for US IP"
end
test "Multiple country policies generate rules for matching countries only" do
# Create additional policy to block China
china_policy = WafPolicy.create_country_policy(
['CN'],
policy_action: 'deny',
user: @user,
name: "Block China"
)
# Create Chinese IP event
china_ip = "220.181.38.148" # Known Chinese IP
china_event_data = @brazil_event_data.dup
china_event_data["event_id"] = "china-test-123"
china_event_data["request"]["ip"] = china_ip
china_event_data["geo"]["country_code"] = "CN"
china_event_data["geo"]["city"] = "Beijing"
# Process Chinese event
china_event = Event.create_from_waf_payload!("china-test", china_event_data)
china_network_range = NetworkRangeGenerator.find_or_create_for_ip(china_ip)
china_network_range.update!(country: 'CN')
# Process Brazilian event (from setup)
brazil_event = Event.create_from_waf_payload!("brazil-test", @brazil_event_data)
brazil_network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
brazil_network_range.update!(country: 'BR')
# Process WAF policies for both network ranges
ProcessWafPoliciesJob.perform_now(network_range: brazil_network_range, event: brazil_event)
ProcessWafPoliciesJob.perform_now(network_range: china_network_range, event: china_event)
# Verify Brazil IP matched Brazil policy only
brazil_rules = Rule.where(network_range: brazil_network_range)
assert_equal 1, brazil_rules.count
brazil_rule = brazil_rules.first
assert_equal @brazil_policy, brazil_rule.waf_policy
assert_equal "BR", brazil_rule.metadata['matched_value']
# Verify China IP matched China policy only
china_rules = Rule.where(network_range: china_network_range)
assert_equal 1, china_rules.count
china_rule = china_rules.first
assert_equal china_policy, china_rule.waf_policy
assert_equal "CN", china_rule.metadata['matched_value']
end
test "Policy expiration prevents rule generation" do
# Create an expired Brazil policy
expired_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'deny',
user: @user,
name: "Expired Brazil Block",
expires_at: 1.day.ago
)
# Process Brazilian event
event = Event.create_from_waf_payload!("expired-test", @brazil_event_data)
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
network_range.update!(country: 'BR')
# Process WAF policies
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that no rule was generated from expired policy
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: expired_policy
)
assert_equal 0, generated_rules.count, "Expired policy should not generate rules"
end
test "Disabled policy prevents rule generation" do
# Create a disabled Brazil policy
disabled_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'deny',
user: @user,
name: "Disabled Brazil Block"
)
disabled_policy.update!(enabled: false)
# Process Brazilian event
event = Event.create_from_waf_payload!("disabled-test", @brazil_event_data)
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
network_range.update!(country: 'BR')
# Process WAF policies
ProcessWafPoliciesJob.perform_now(network_range: network_range, event: event)
# Verify that no rule was generated from disabled policy
generated_rules = Rule.where(
network_range: network_range,
policy_action: 'deny',
waf_policy: disabled_policy
)
assert_equal 0, generated_rules.count, "Disabled policy should not generate rules"
end
test "Policy action types are correctly applied to generated rules" do
# Test different policy actions
redirect_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'redirect',
user: @user,
name: "Redirect Brazil",
additional_data: {
'redirect_url' => 'https://example.com/blocked',
'redirect_status' => 302
}
)
challenge_policy = WafPolicy.create_country_policy(
['BR'],
policy_action: 'challenge',
user: @user,
name: "Challenge Brazil",
additional_data: {
'challenge_type' => 'captcha',
'challenge_message' => 'Please verify you are human'
}
)
# Process Brazilian event for redirect policy
event = Event.create_from_waf_payload!("redirect-test", @brazil_event_data)
network_range = NetworkRangeGenerator.find_or_create_for_ip(@brazil_ip)
network_range.update!(country: 'BR')
# Manually create rule for redirect policy to test metadata handling
redirect_rule = redirect_policy.create_rule_for_network_range(network_range)
assert redirect_rule.persisted?
assert_equal 'redirect', redirect_rule.action
assert_equal 'https://example.com/blocked', redirect_rule.redirect_url
assert_equal 302, redirect_rule.redirect_status
# Manually create rule for challenge policy to test metadata handling
challenge_rule = challenge_policy.create_rule_for_network_range(network_range)
assert challenge_rule.persisted?
assert_equal 'challenge', challenge_rule.action
assert_equal 'captcha', challenge_rule.challenge_type
assert_equal 'Please verify you are human', challenge_rule.challenge_message
end
end

View File

@@ -4,18 +4,20 @@ require "test_helper"
class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
test "disables expired rules" do
expired_range = NetworkRange.create!(cidr: "10.0.0.0/8")
expired_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: expired_range,
expires_at: 1.hour.ago,
enabled: true
)
active_range = NetworkRange.create!(cidr: "192.168.0.0/16")
active_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.0.0/16" },
waf_rule_type: "network",
waf_action: "deny",
network_range: active_range,
expires_at: 1.hour.from_now,
enabled: true
)
@@ -28,10 +30,11 @@ class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
end
test "does not affect rules without expiration" do
permanent_range = NetworkRange.create!(cidr: "10.0.0.0/8")
permanent_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: permanent_range,
expires_at: nil,
enabled: true
)
@@ -42,10 +45,11 @@ class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
end
test "does not affect already disabled rules" do
disabled_range = NetworkRange.create!(cidr: "10.0.0.0/8")
disabled_expired_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: disabled_range,
expires_at: 1.hour.ago,
enabled: false
)
@@ -57,10 +61,11 @@ class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
end
test "updates updated_at timestamp when disabling" do
expired_range = NetworkRange.create!(cidr: "10.0.0.0/8")
expired_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: expired_range,
expires_at: 1.hour.ago,
enabled: true
)
@@ -75,18 +80,20 @@ class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
end
test "deletes old disabled rules when running at 1am" do
old_range = NetworkRange.create!(cidr: "10.0.0.0/8")
old_disabled_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: old_range,
enabled: false
)
old_disabled_rule.update_column(:updated_at, 31.days.ago)
recent_range = NetworkRange.create!(cidr: "192.168.0.0/16")
recent_disabled_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.0.0/16" },
waf_rule_type: "network",
waf_action: "deny",
network_range: recent_range,
enabled: false
)
@@ -99,10 +106,11 @@ class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
end
test "does not delete old rules when not running at 1am" do
old_range = NetworkRange.create!(cidr: "10.0.0.0/8")
old_disabled_rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: old_range,
enabled: false
)
old_disabled_rule.update_column(:updated_at, 31.days.ago)
@@ -116,10 +124,11 @@ class ExpiredRulesCleanupJobTest < ActiveJob::TestCase
test "returns count of disabled rules" do
3.times do |i|
range = NetworkRange.create!(cidr: "10.#{i}.0.0/16")
Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.#{i}.0.0/16" },
waf_rule_type: "network",
waf_action: "deny",
network_range: range,
expires_at: 1.hour.ago,
enabled: true
)

View File

@@ -0,0 +1,387 @@
require "test_helper"
class FetchIpapiDataJobTest < ActiveJob::TestCase
setup do
@tracking_network = NetworkRange.create!(
network: "192.168.1.0/24",
source: "auto_generated",
creation_reason: "IPAPI tracking network"
)
@sample_ipapi_data = {
"ip" => "192.168.1.100",
"type" => "ipv4",
"continent_code" => "NA",
"continent_name" => "North America",
"country_code" => "US",
"country_name" => "United States",
"region_code" => "CA",
"region_name" => "California",
"city" => "San Francisco",
"zip" => "94102",
"latitude" => 37.7749,
"longitude" => -122.4194,
"location" => {
"geoname_id" => 5391959,
"capital" => "Washington D.C.",
"languages" => [
{
"code" => "en",
"name" => "English",
"native" => "English"
}
],
"country_flag" => "https://cdn.ipapi.com/flags/us.svg",
"country_flag_emoji" => "🇺🇸",
"country_flag_emoji_unicode" => "U+1F1FA U+1F1F8",
"calling_code" => "1",
"is_eu" => false
},
"time_zone" => {
"id" => "America/Los_Angeles",
"current_time" => "2023-12-07T12:00:00+00:00",
"gmt_offset" => -28800,
"code" => "PST",
"is_dst" => false
},
"currency" => {
"code" => "USD",
"name" => "US Dollar",
"plural" => "US dollars",
"symbol" => "$",
"symbol_native" => "$"
},
"connection" => {
"asn" => 12345,
"isp" => "Test ISP",
"domain" => "test.com",
"type" => "isp"
},
"security" => {
"is_proxy" => false,
"is_crawler" => false,
"is_tor" => false,
"threat_level" => "low",
"threat_types" => []
},
"asn" => {
"asn" => "AS12345 Test ISP",
"domain" => "test.com",
"route" => "192.168.1.0/24",
"type" => "isp"
}
}
end
teardown do
# Clean up any test networks
NetworkRange.where(network: "192.168.1.0/24").delete_all
NetworkRange.where(network: "203.0.113.0/24").delete_all
end
# Successful Data Fetching
test "fetches and stores IPAPI data successfully" do
# Mock Ipapi.lookup
Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
@tracking_network.reload
assert_equal @sample_ipapi_data, @tracking_network.network_data_for(:ipapi)
assert_not_nil @tracking_network.last_api_fetch
assert @tracking_network.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
assert_equal "192.168.1.0/24", @tracking_network.network_data['ipapi_returned_cidr']
end
test "handles IPAPI returning different route than tracking network" do
# IPAPI returns a more specific network
different_route_data = @sample_ipapi_data.dup
different_route_data["asn"]["route"] = "203.0.113.0/25"
Ipapi.expects(:lookup).with("192.168.1.0").returns(different_route_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
# Should create new network range for the correct route
target_network = NetworkRange.find_by(network: "203.0.113.0/25")
assert_not_nil target_network
assert_equal different_route_data, target_network.network_data_for(:ipapi)
assert_equal "api_imported", target_network.source
assert_match /Created from IPAPI lookup/, target_network.creation_reason
# Tracking network should be marked as queried with the returned CIDR
@tracking_network.reload
assert_equal "203.0.113.0/25", @tracking_network.network_data['ipapi_returned_cidr']
end
test "uses existing network when IPAPI returns different route" do
# Create the target network first
existing_network = NetworkRange.create!(
network: "203.0.113.0/25",
source: "manual",
creation_reason: "Pre-existing"
)
different_route_data = @sample_ipapi_data.dup
different_route_data["asn"]["route"] = "203.0.113.0/25"
Ipapi.expects(:lookup).with("192.168.1.0").returns(different_route_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
# Should use existing network, not create new one
existing_network.reload
assert_equal different_route_data, existing_network.network_data_for(:ipapi)
assert_equal 1, NetworkRange.where(network: "203.0.113.0/25").count
end
# Error Handling
test "handles IPAPI returning error gracefully" do
error_data = {
"error" => true,
"reason" => "Invalid IP address",
"ip" => "192.168.1.0"
}
Ipapi.expects(:lookup).with("192.168.1.0").returns(error_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
# Should mark as queried to avoid immediate retry
@tracking_network.reload
assert @tracking_network.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
assert_equal "192.168.1.0/24", @tracking_network.network_data['ipapi_returned_cidr']
# Should not store the error data
assert_empty @tracking_network.network_data_for(:ipapi)
end
test "handles IPAPI returning nil gracefully" do
Ipapi.expects(:lookup).with("192.168.1.0").returns(nil)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
# Should mark as queried to avoid immediate retry
@tracking_network.reload
assert @tracking_network.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
assert_equal "192.168.1.0/24", @tracking_network.network_data['ipapi_returned_cidr']
end
test "handles missing network range gracefully" do
# Use non-existent network range ID
assert_nothing_raised do
FetchIpapiDataJob.perform_now(network_range_id: 99999)
end
end
test "handles IPAPI service errors gracefully" do
Ipapi.expects(:lookup).with("192.168.1.0").raises(StandardError.new("Service unavailable"))
# Should not raise error but should clear fetching status
assert_nothing_raised do
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
end
# Fetching status should be cleared
assert_not @tracking_network.is_fetching_api_data?(:ipapi)
end
# Fetching Status Management
test "clears fetching status when done" do
@tracking_network.mark_as_fetching_api_data!(:ipapi)
Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)
assert @tracking_network.is_fetching_api_data?(:ipapi)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
assert_not @tracking_network.is_fetching_api_data?(:ipapi)
end
test "clears fetching status even on error" do
@tracking_network.mark_as_fetching_api_data!(:ipapi)
Ipapi.expects(:lookup).with("192.168.1.0").raises(StandardError.new("Service error"))
assert @tracking_network.is_fetching_api_data?(:ipapi)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
assert_not @tracking_network.is_fetching_api_data?(:ipapi)
end
test "clears fetching status when network range not found" do
# Create network range and mark as fetching
temp_network = NetworkRange.create!(
network: "10.0.0.0/24",
source: "auto_generated"
)
temp_network.mark_as_fetching_api_data!(:ipapi)
# Try to fetch with non-existent ID
FetchIpapiDataJob.perform_now(network_range_id: 99999)
# Original network should still have fetching status cleared (ensure block runs)
temp_network.reload
assert_not temp_network.is_fetching_api_data?(:ipapi)
end
# Turbo Broadcast
test "broadcasts IPAPI update on success" do
Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)
# Expect Turbo broadcast
Turbo::StreamsChannel.expects(:broadcast_replace_to)
.with("network_range_#{@tracking_network.id}", {
target: "ipapi_data_section",
partial: "network_ranges/ipapi_data",
locals: {
ipapi_data: @sample_ipapi_data,
network_range: @tracking_network,
parent_with_ipapi: nil,
ipapi_loading: false
}
})
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
end
test "does not broadcast on error" do
error_data = { "error" => true, "reason" => "Invalid IP" }
Ipapi.expects(:lookup).with("192.168.1.0").returns(error_data)
# Should not broadcast
Turbo::StreamsChannel.expects(:broadcast_replace_to).never
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
end
# Network Address Extraction
test "extracts correct sample IP from network" do
# Test with different network formats
ipv4_network = NetworkRange.create!(network: "203.0.113.0/24")
Ipapi.expects(:lookup).with("203.0.113.0").returns(@sample_ipapi_data)
FetchIpapiDataJob.perform_now(network_range_id: ipv4_network.id)
ipv6_network = NetworkRange.create!(network: "2001:db8::/64")
Ipapi.expects(:lookup).with("2001:db8::").returns(@sample_ipapi_data)
FetchIpapiDataJob.perform_now(network_range_id: ipv6_network.id)
end
# Data Storage
test "stores complete IPAPI data in network_data" do
Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
stored_data = @tracking_network.reload.network_data_for(:ipapi)
assert_equal @sample_ipapi_data["country_code"], stored_data["country_code"]
assert_equal @sample_ipapi_data["city"], stored_data["city"]
assert_equal @sample_ipapi_data["asn"]["asn"], stored_data["asn"]["asn"]
assert_equal @sample_ipapi_data["security"]["is_proxy"], stored_data["security"]["is_proxy"]
end
test "updates last_api_fetch timestamp" do
original_time = 1.hour.ago
@tracking_network.update!(last_api_fetch: original_time)
Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
@tracking_network.reload
assert @tracking_network.last_api_fetch > original_time
end
# IPv6 Support
test "handles IPv6 networks correctly" do
ipv6_network = NetworkRange.create!(
network: "2001:db8::/64",
source: "auto_generated",
creation_reason: "IPAPI tracking network"
)
ipv6_data = @sample_ipapi_data.dup
ipv6_data["ip"] = "2001:db8::1"
ipv6_data["type"] = "ipv6"
ipv6_data["asn"]["route"] = "2001:db8::/32"
Ipapi.expects(:lookup).with("2001:db8::").returns(ipv6_data)
FetchIpapiDataJob.perform_now(network_range_id: ipv6_network.id)
ipv6_network.reload
assert_equal ipv6_data, ipv6_network.network_data_for(:ipapi)
assert_equal "2001:db8::/32", ipv6_network.network_data['ipapi_returned_cidr']
end
# Logging
test "logs successful fetch" do
log_output = StringIO.new
logger = Logger.new(log_output)
original_logger = Rails.logger
Rails.logger = logger
Ipapi.expects(:lookup).with("192.168.1.0").returns(@sample_ipapi_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
log_content = log_output.string
assert_match /Fetching IPAPI data for 192\.168\.1\.0\/24 using IP 192\.168\.1\.0/, log_content
assert_match /Successfully fetched IPAPI data/, log_content
Rails.logger = original_logger
end
test "logs errors and warnings" do
log_output = StringIO.new
logger = Logger.new(log_output)
original_logger = Rails.logger
Rails.logger = logger
error_data = { "error" => true, "reason" => "Rate limited" }
Ipapi.expects(:lookup).with("192.168.1.0").returns(error_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
log_content = log_output.string
assert_match /IPAPI returned error for 192\.168\.1\.0\/24/, log_content
Rails.logger = original_logger
end
test "logs different route handling" do
log_output = StringIO.new
logger = Logger.new(log_output)
original_logger = Rails.logger
Rails.logger = logger
different_route_data = @sample_ipapi_data.dup
different_route_data["asn"]["route"] = "203.0.113.0/25"
Ipapi.expects(:lookup).with("192.168.1.0").returns(different_route_data)
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
log_content = log_output.string
assert_match /IPAPI returned different route: 203\.0\.113\.0\/25/, log_content
assert_match /Storing IPAPI data on correct network: 203\.0\.113\.0\/25/, log_content
Rails.logger = original_logger
end
test "logs service errors with backtrace" do
log_output = StringIO.new
logger = Logger.new(log_output)
original_logger = Rails.logger
Rails.logger = logger
Ipapi.expects(:lookup).with("192.168.1.0").raises(StandardError.new("Connection failed"))
FetchIpapiDataJob.perform_now(network_range_id: @tracking_network.id)
log_content = log_output.string
assert_match /Failed to fetch IPAPI data for network_range #{@tracking_network.id}/, log_content
assert_match /Connection failed/, log_content
Rails.logger = original_logger
end
end

View File

@@ -18,7 +18,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
["/.env", "/.git", "/wp-admin"].each do |path|
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: path,
@@ -45,7 +45,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
3.times do |i|
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.env",
@@ -71,7 +71,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
paths.each do |path|
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: path,
@@ -95,7 +95,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
2.times do
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.env",
@@ -114,7 +114,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
# Old event (outside lookback window)
old_event = Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: 10.minutes.ago,
ip_address: ip,
request_path: "/.env",
@@ -125,7 +125,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
2.times do
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.git",
@@ -154,7 +154,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
3.times do
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.env",
@@ -173,7 +173,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
3.times do
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.env",
@@ -198,7 +198,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
3.times do
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.env",
@@ -216,7 +216,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
# Create event with invalid IP
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: "invalid-ip",
request_path: "/.env",
@@ -235,7 +235,7 @@ class PathScannerDetectorJobTest < ActiveJob::TestCase
3.times do
Event.create!(
project: @project,
event_id: SecureRandom.uuid,
request_id: SecureRandom.uuid,
timestamp: Time.current,
ip_address: ip,
request_path: "/.env",

View File

@@ -0,0 +1,363 @@
require "test_helper"
class ProcessWafEventJobTest < ActiveJob::TestCase
setup do
@sample_event_data = {
"request_id" => "test-event-123",
"timestamp" => Time.current.iso8601,
"request" => {
"ip" => "192.168.1.100",
"method" => "GET",
"path" => "/api/test",
"headers" => {
"host" => "example.com",
"user-agent" => "TestAgent/1.0"
}
},
"response" => {
"status_code" => 200,
"duration_ms" => 150
},
"waf_action" => "allow",
"server_name" => "test-server",
"environment" => "test"
}
@headers = { "Content-Type" => "application/json" }
end
# Single Event Processing
test "processes single event with request_id" do
assert_difference 'Event.count', 1 do
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
event = Event.last
assert_equal "test-event-123", event.request_id
assert_equal "192.168.1.100", event.ip_address
assert_equal "/api/test", event.request_path
assert_equal "get", event.request_method
assert_equal "allow", event.waf_action
end
test "processes single event with legacy event_id" do
event_data = @sample_event_data.dup
event_data.delete("request_id")
event_data["event_id"] = "legacy-event-456"
assert_difference 'Event.count', 1 do
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
end
event = Event.last
assert_equal "legacy-event-456", event.request_id
end
test "processes single event with correlation_id" do
event_data = @sample_event_data.dup
event_data.delete("request_id")
event_data["correlation_id"] = "correlation-789"
assert_difference 'Event.count', 1 do
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
end
event = Event.last
assert_equal "correlation-789", event.request_id
end
test "generates UUID for events without ID" do
event_data = @sample_event_data.dup
event_data.delete("request_id")
assert_difference 'Event.count', 1 do
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
end
event = Event.last
assert_not_nil event.request_id
assert_match /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/, event.request_id
end
# Multiple Events Processing
test "processes multiple events in events array" do
event1 = @sample_event_data.dup
event1["request_id"] = "event-1"
event1["request"]["ip"] = "192.168.1.1"
event2 = @sample_event_data.dup
event2["request_id"] = "event-2"
event2["request"]["ip"] = "192.168.1.2"
batch_data = {
"events" => [event1, event2]
}
assert_difference 'Event.count', 2 do
ProcessWafEventJob.perform_now(event_data: batch_data, headers: @headers)
end
request_ids = Event.last(2).pluck(:request_id)
assert_includes request_ids, "event-1"
assert_includes request_ids, "event-2"
end
# Duplicate Handling
test "skips duplicate events" do
# Create event first
Event.create_from_waf_payload!("test-event-123", @sample_event_data)
assert_no_difference 'Event.count' do
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
end
test "handles duplicates within batch" do
event1 = @sample_event_data.dup
event1["request_id"] = "duplicate-test"
event2 = @sample_event_data.dup
event2["request_id"] = "duplicate-test"
batch_data = {
"events" => [event1, event2]
}
assert_difference 'Event.count', 1 do
ProcessWafEventJob.perform_now(event_data: batch_data, headers: @headers)
end
end
# Network Range Processing
test "creates tracking network for event IP" do
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
event = Event.last
assert_not_nil event.network_range_id
# Should create /24 tracking network for IPv4
tracking_network = event.network_range
assert_equal "192.168.1.0/24", tracking_network.network.to_s
assert_equal "auto_generated", tracking_network.source
assert_equal "IPAPI tracking network", tracking_network.creation_reason
end
test "queues IPAPI enrichment when needed" do
event_data = @sample_event_data.dup
event_data["request"]["ip"] = "8.8.8.8" # Public IP that needs enrichment
assert_enqueued_jobs 1, only: [FetchIpapiDataJob] do
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
end
end
test "skips IPAPI enrichment when recently queried" do
# Create tracking network with recent query
tracking_network = NetworkRange.create!(
network: "192.168.1.0/24",
source: "auto_generated",
creation_reason: "IPAPI tracking network"
)
tracking_network.mark_ipapi_queried!("192.168.1.0/24")
assert_no_enqueued_jobs only: [FetchIpapiDataJob] do
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
end
test "skips IPAPI enrichment when already fetching" do
tracking_network = NetworkRange.create!(
network: "192.168.1.0/24",
source: "auto_generated",
creation_reason: "IPAPI tracking network"
)
tracking_network.mark_as_fetching_api_data!(:ipapi)
assert_no_enqueued_jobs only: [FetchIpapiDataJob] do
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
end
# WAF Policy Evaluation
test "evaluates WAF policies when needed" do
tracking_network = NetworkRange.create!(
network: "192.168.1.0/24",
source: "auto_generated",
creation_reason: "IPAPI tracking network"
)
# Mock WafPolicyMatcher
WafPolicyMatcher.expects(:evaluate_and_mark!).with(tracking_network).returns({
generated_rules: [],
evaluated_policies: []
})
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
test "skips policy evaluation when not needed" do
tracking_network = NetworkRange.create!(
network: "192.168.1.0/24",
source: "auto_generated",
creation_reason: "IPAPI tracking network",
policies_evaluated_at: 5.minutes.ago
)
# Should not call WafPolicyMatcher
WafPolicyMatcher.expects(:evaluate_and_mark!).never
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
# Error Handling
test "handles invalid event data format gracefully" do
invalid_data = {
"invalid" => "data"
}
assert_no_difference 'Event.count' do
assert_nothing_raised do
ProcessWafEventJob.perform_now(event_data: invalid_data, headers: @headers)
end
end
end
test "handles event creation errors gracefully" do
invalid_event_data = @sample_event_data.dup
invalid_event_data.delete("request") # Missing required request data
assert_no_difference 'Event.count' do
assert_nothing_raised do
ProcessWafEventJob.perform_now(event_data: invalid_event_data, headers: @headers)
end
end
end
test "handles network processing errors gracefully" do
# Create a tracking network that will cause an error
tracking_network = NetworkRange.create!(
network: "192.168.1.0/24",
source: "auto_generated",
creation_reason: "IPAPI tracking network"
)
# Mock WafPolicyMatcher to raise an error
WafPolicyMatcher.expects(:evaluate_and_mark!).with(tracking_network).raises(StandardError.new("Policy evaluation failed"))
# Event should still be created despite policy evaluation error
assert_difference 'Event.count', 1 do
assert_nothing_raised do
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
end
end
end
test "handles events without network ranges" do
event_data = @sample_event_data.dup
event_data["request"]["ip"] = "127.0.0.1" # Private/local IP
assert_difference 'Event.count', 1 do
assert_nothing_raised do
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
end
end
event = Event.last
assert_nil event.network_range_id
end
# Performance Logging
test "logs processing metrics" do
log_output = StringIO.new
logger = Logger.new(log_output)
original_logger = Rails.logger
Rails.logger = logger
ProcessWafEventJob.perform_now(event_data: @sample_event_data, headers: @headers)
log_content = log_output.string
assert_match /Processed WAF event test-event-123 in \d+\.\d+ms/, log_content
assert_match /Processed 1 WAF events/, log_content
Rails.logger = original_logger
end
test "logs IPAPI fetch decisions" do
log_output = StringIO.new
logger = Logger.new(log_output)
original_logger = Rails.logger
Rails.logger = logger
# Use a public IP to trigger IPAPI fetch
event_data = @sample_event_data.dup
event_data["request"]["ip"] = "8.8.8.8"
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
log_content = log_output.string
assert_match /Queueing IPAPI fetch for IP 8\.8\.8\.8/, log_content
Rails.logger = original_logger
end
# IPv6 Support
test "creates /64 tracking network for IPv6 addresses" do
event_data = @sample_event_data.dup
event_data["request"]["ip"] = "2001:db8::1"
ProcessWafEventJob.perform_now(event_data: event_data, headers: @headers)
event = Event.last
tracking_network = event.network_range
assert_equal "2001:db8::/64", tracking_network.network.to_s
end
# Mixed Batch Processing
test "processes mixed valid and invalid events in batch" do
valid_event = @sample_event_data.dup
valid_event["request_id"] = "valid-event"
invalid_event = {
"invalid" => "data",
"request_id" => "invalid-event"
}
batch_data = {
"events" => [valid_event, invalid_event]
}
# Should only create the valid event
assert_difference 'Event.count', 1 do
ProcessWafEventJob.perform_now(event_data: batch_data, headers: @headers)
end
assert_equal "valid-event", Event.last.request_id
end
test "handles very large batches efficiently" do
events = []
100.times do |i|
event = @sample_event_data.dup
event["request_id"] = "batch-event-#{i}"
event["request"]["ip"] = "192.168.#{i / 256}.#{i % 256}"
events << event
end
batch_data = {
"events" => events
}
start_time = Time.current
ProcessWafEventJob.perform_now(event_data: batch_data, headers: @headers)
processing_time = Time.current - start_time
assert_equal 100, Event.count
assert processing_time < 5.seconds, "Processing 100 events should take less than 5 seconds"
end
# Integration with Other Jobs
test "coordinates with BackfillRecentNetworkIntelligenceJob" do
# This would be tested based on how the job enqueues other jobs
# Implementation depends on your specific job coordination logic
end
end

View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
require "test_helper"
class DsnAuthServiceTest < ActiveSupport::TestCase
self.use_transactional_tests = true
def setup
@dsn = Dsn.create!(name: "Test DSN", key: "test-auth-key-1234567890abcdef")
end
def teardown
Dsn.delete_all
end
test "should authenticate via query parameter baffle_key" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => @dsn.key }
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via Authorization Bearer header" do
request = ActionDispatch::TestRequest.create
request.headers["Authorization"] = "Bearer #{@dsn.key}"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via Basic auth with username as key" do
request = ActionDispatch::TestRequest.create
credentials = Base64.strict_encode64("#{@dsn.key}:ignored-password")
request.headers["Authorization"] = "Basic #{credentials}"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should fail authentication with disabled DSN" do
@dsn.update!(enabled: false)
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => @dsn.key }
assert_raises(DsnAuthenticationService::AuthenticationError) do
DsnAuthenticationService.authenticate(request)
end
end
test "should fail authentication with non-existent key" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => "non-existent-key" }
assert_raises(DsnAuthenticationService::AuthenticationError) do
DsnAuthenticationService.authenticate(request)
end
end
test "should fail authentication with no authentication method" do
request = ActionDispatch::TestRequest.create
assert_raises(DsnAuthenticationService::AuthenticationError) do
DsnAuthenticationService.authenticate(request)
end
end
end

View File

@@ -0,0 +1,140 @@
# frozen_string_literal: true
require "test_helper"
class DsnSimpleTest < ActiveSupport::TestCase
# Don't use any fixtures
self.use_transactional_tests = true
def setup
@dsn = Dsn.new(name: "Test DSN")
end
def teardown
Dsn.delete_all
end
test "should be valid with valid attributes" do
assert @dsn.valid?
end
test "should not be valid without name" do
@dsn.name = nil
assert_not @dsn.valid?
assert_includes @dsn.errors[:name], "can't be blank"
end
test "should automatically generate key on create" do
@dsn.save!
assert_not_nil @dsn.key
assert_equal 64, @dsn.key.length # hex(32) = 64 characters
assert_match /\A[a-f0-9]{64}\z/, @dsn.key
end
test "should not override existing key when saved" do
@dsn.key = "existing-key-123"
@dsn.save!
assert_equal "existing-key-123", @dsn.key
end
test "should enforce unique keys" do
@dsn.save!
dsn2 = Dsn.new(name: "Another DSN", key: @dsn.key)
assert_not dsn2.valid?
assert_includes dsn2.errors[:key], "has already been taken"
end
test "should default to enabled" do
@dsn.save!
assert @dsn.enabled?
end
test "should authenticate with valid key" do
@dsn.save!
authenticated_dsn = Dsn.authenticate(@dsn.key)
assert_equal @dsn, authenticated_dsn
end
test "should not authenticate with invalid key" do
@dsn.save!
assert_nil Dsn.authenticate("invalid-key")
end
test "should not authenticate disabled DSNs" do
@dsn.save!
@dsn.update!(enabled: false)
assert_nil Dsn.authenticate(@dsn.key)
end
# URL Generation Tests
test "should generate full DSN URL in development" do
@dsn.key = "test-key-1234567890abcdef"
@dsn.save!
expected = "http://test-key-1234567890abcdef@localhost"
assert_equal expected, @dsn.full_dsn_url
end
test "should generate API endpoint URL in development" do
@dsn.save!
expected = "http://localhost"
assert_equal expected, @dsn.api_endpoint_url
end
test "should use custom host from environment variable" do
ENV['RAILS_HOST'] = 'baffle.example.com'
@dsn.key = "custom-key-1234567890abcdef"
@dsn.save!
assert_equal "http://custom-key-1234567890abcdef@baffle.example.com", @dsn.full_dsn_url
assert_equal "http://baffle.example.com", @dsn.api_endpoint_url
ENV.delete('RAILS_HOST')
end
test "should handle long hex keys in URLs" do
long_key = "c92b7f8ad94ea3400299d8a6ff19e409c2df8c4540022c3167b8ac1002931624"
@dsn.key = long_key
@dsn.save!
expected = "http://#{long_key}@localhost"
assert_equal expected, @dsn.full_dsn_url
end
# Scope Tests
test "enabled scope should return only enabled DSNs" do
enabled_dsn = Dsn.create!(name: "Enabled DSN", enabled: true)
disabled_dsn = Dsn.create!(name: "Disabled DSN", enabled: false)
enabled_dsns = Dsn.enabled
assert_includes enabled_dsns, enabled_dsn
assert_not_includes enabled_dsns, disabled_dsn
end
# Security Tests
test "should generate cryptographically secure keys" do
keys = []
5.times do
dsn = Dsn.create!(name: "Test DSN #{Time.current.to_f}")
keys << dsn.key
end
# All keys should be unique
assert_equal keys.length, keys.uniq.length
# All keys should be valid hex
keys.each do |key|
assert_equal 64, key.length
assert_match /\A[a-f0-9]{64}\z/, key
end
end
test "should not allow nil keys" do
@dsn.key = nil
assert_not @dsn.valid?
assert_includes @dsn.errors[:key], "can't be blank"
end
end

162
test/models/dsn_test.rb Normal file
View File

@@ -0,0 +1,162 @@
# frozen_string_literal: true
require "test_helper"
class DsnTest < ActiveSupport::TestCase
# Disable fixtures since we're creating test data manually
self.use_instantiated_fixtures = false
def setup
@dsn = Dsn.new(name: "Test DSN")
end
test "should be valid with valid attributes" do
assert @dsn.valid?
end
test "should not be valid without name" do
@dsn.name = nil
assert_not @dsn.valid?
assert_includes @dsn.errors[:name], "can't be blank"
end
test "should automatically generate key on create" do
@dsn.save!
assert_not_nil @dsn.key
assert_equal 64, @dsn.key.length # hex(32) = 64 characters
assert_match /\A[a-f0-9]{64}\z/, @dsn.key
end
test "should not override existing key when saved" do
@dsn.key = "existing-key-123"
@dsn.save!
assert_equal "existing-key-123", @dsn.key
end
test "should enforce unique keys" do
@dsn.save!
dsn2 = Dsn.new(name: "Another DSN", key: @dsn.key)
assert_not dsn2.valid?
assert_includes dsn2.errors[:key], "has already been taken"
end
test "should default to enabled" do
@dsn.save!
assert @dsn.enabled?
end
test "should authenticate with valid key" do
@dsn.save!
authenticated_dsn = Dsn.authenticate(@dsn.key)
assert_equal @dsn, authenticated_dsn
end
test "should not authenticate with invalid key" do
@dsn.save!
assert_nil Dsn.authenticate("invalid-key")
end
test "should not authenticate disabled DSNs" do
@dsn.save!
@dsn.update!(enabled: false)
assert_nil Dsn.authenticate(@dsn.key)
end
# URL Generation Tests
test "should generate full DSN URL in development" do
@dsn.key = "test-key-1234567890abcdef"
@dsn.save!
expected = "http://test-key-1234567890abcdef@localhost"
assert_equal expected, @dsn.full_dsn_url
end
test "should generate API endpoint URL in development" do
@dsn.save!
expected = "http://localhost"
assert_equal expected, @dsn.api_endpoint_url
end
test "should use HTTPS in production environment" do
# Temporarily switch to production environment
original_env = Rails.env
Rails.env = "production"
@dsn.key = "prod-key-1234567890abcdef"
@dsn.save!
assert_equal "https://prod-key-1234567890abcdef@localhost", @dsn.full_dsn_url
assert_equal "https://localhost", @dsn.api_endpoint_url
# Restore original environment
Rails.env = original_env
end
test "should use custom host from environment variable" do
ENV['RAILS_HOST'] = 'baffle.example.com'
@dsn.key = "custom-key-1234567890abcdef"
@dsn.save!
assert_equal "http://custom-key-1234567890abcdef@baffle.example.com", @dsn.full_dsn_url
assert_equal "http://baffle.example.com", @dsn.api_endpoint_url
ENV.delete('RAILS_HOST')
end
test "should use action mailer default host if configured" do
Rails.application.config.action_mailer.default_url_options = { host: 'mail.baffle.com' }
@dsn.key = "mail-key-1234567890abcdef"
@dsn.save!
assert_equal "http://mail-key-1234567890abcdef@mail.baffle.com", @dsn.full_dsn_url
assert_equal "http://mail.baffle.com", @dsn.api_endpoint_url
Rails.application.config.action_mailer.default_url_options = {}
end
test "should handle long hex keys in URLs" do
long_key = "c92b7f8ad94ea3400299d8a6ff19e409c2df8c4540022c3167b8ac1002931624"
@dsn.key = long_key
@dsn.save!
expected = "http://#{long_key}@localhost"
assert_equal expected, @dsn.full_dsn_url
end
# Scope Tests
test "enabled scope should return only enabled DSNs" do
enabled_dsn = Dsn.create!(name: "Enabled DSN", enabled: true)
disabled_dsn = Dsn.create!(name: "Disabled DSN", enabled: false)
enabled_dsns = Dsn.enabled
assert_includes enabled_dsns, enabled_dsn
assert_not_includes enabled_dsns, disabled_dsn
end
# Security Tests
test "should generate cryptographically secure keys" do
keys = []
10.times do
dsn = Dsn.create!(name: "Test DSN #{Time.current.to_f}")
keys << dsn.key
end
# All keys should be unique
assert_equal keys.length, keys.uniq.length
# All keys should be valid hex
keys.each do |key|
assert_equal 64, key.length
assert_match /\A[a-f0-9]{64}\z/, key
end
end
test "should not allow nil keys" do
@dsn.key = nil
assert_not @dsn.valid?
assert_includes @dsn.errors[:key], "can't be blank"
end
end

View File

@@ -5,7 +5,7 @@ require "test_helper"
class EventTest < ActiveSupport::TestCase
def setup
@sample_payload = {
"event_id" => "test-event-123",
"request_id" => "test-event-123",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => "192.168.1.1",
@@ -46,7 +46,7 @@ class EventTest < ActiveSupport::TestCase
event = Event.create_from_waf_payload!("test-123", @sample_payload)
assert event.persisted?
assert_equal "test-123", event.event_id
assert_equal "test-123", event.request_id
assert_equal "192.168.1.1", event.ip_address
assert_equal "/api/test", event.request_path
assert_equal 200, event.response_status
@@ -66,7 +66,7 @@ class EventTest < ActiveSupport::TestCase
test_methods.each_with_index do |method, index|
payload = @sample_payload.dup
payload["request"]["method"] = method
payload["event_id"] = "test-method-#{method.downcase}"
payload["request_id"] = "test-method-#{method.downcase}"
event = Event.create_from_waf_payload!("test-method-#{method.downcase}", payload)
@@ -91,7 +91,7 @@ class EventTest < ActiveSupport::TestCase
test_actions.each do |action, expected_enum, expected_int|
payload = @sample_payload.dup
payload["waf_action"] = action
payload["event_id"] = "test-action-#{action}"
payload["request_id"] = "test-action-#{action}"
event = Event.create_from_waf_payload!("test-action-#{action}", payload)
@@ -143,7 +143,7 @@ class EventTest < ActiveSupport::TestCase
# Event 1: GET + allow
Event.create_from_waf_payload!("get-allow", {
"event_id" => "get-allow",
"request_id" => "get-allow",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => "192.168.1.1",
@@ -157,7 +157,7 @@ class EventTest < ActiveSupport::TestCase
# Event 2: POST + allow
Event.create_from_waf_payload!("post-allow", {
"event_id" => "post-allow",
"request_id" => "post-allow",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => "192.168.1.1",
@@ -171,7 +171,7 @@ class EventTest < ActiveSupport::TestCase
# Event 3: GET + deny
Event.create_from_waf_payload!("get-deny", {
"event_id" => "get-deny",
"request_id" => "get-deny",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => "192.168.1.1",
@@ -202,7 +202,7 @@ class EventTest < ActiveSupport::TestCase
# Create event without enum values (simulating old data)
event = Event.create!(
project: @project,
event_id: "normalization-test",
request_id: "normalization-test",
timestamp: Time.current,
payload: @sample_payload,
ip_address: "192.168.1.1",
@@ -279,7 +279,7 @@ class EventTest < ActiveSupport::TestCase
timestamps.each_with_index do |timestamp, index|
payload = @sample_payload.dup
payload["timestamp"] = timestamp
payload["event_id"] = "timestamp-test-#{index}"
payload["request_id"] = "timestamp-test-#{index}"
event = Event.create_from_waf_payload!("timestamp-test-#{index}", payload)
assert event.timestamp.is_a?(Time), "Timestamp #{index} should be parsed as Time"
@@ -289,7 +289,7 @@ class EventTest < ActiveSupport::TestCase
test "handles missing optional fields gracefully" do
minimal_payload = {
"event_id" => "minimal-test",
"request_id" => "minimal-test",
"timestamp" => Time.now.iso8601,
"request" => {
"ip" => "10.0.0.1",

View File

@@ -0,0 +1,675 @@
require "test_helper"
class NetworkRangeTest < ActiveSupport::TestCase
setup do
@ipv4_range = NetworkRange.new(network: "192.168.1.0/24")
@ipv6_range = NetworkRange.new(network: "2001:db8::/32")
@user = users(:jason)
end
# Validations
test "should be valid with network address" do
assert @ipv4_range.valid?
assert @ipv6_range.valid?
end
test "should not be valid without network" do
@ipv4_range.network = nil
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:network], "can't be blank"
end
test "should validate network uniqueness" do
@ipv4_range.save!
duplicate = NetworkRange.new(network: "192.168.1.0/24")
assert_not duplicate.valid?
assert_includes duplicate.errors[:network], "has already been taken"
end
test "should validate source inclusion" do
valid_sources = %w[api_imported user_created manual auto_generated inherited geolite_asn geolite_country]
valid_sources.each do |source|
@ipv4_range.source = source
assert @ipv4_range.valid?, "Source #{source} should be valid"
end
@ipv4_range.source = "invalid_source"
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:source], "is not included in the list"
end
test "should validate ASN numericality" do
@ipv4_range.asn = 12345
assert @ipv4_range.valid?
@ipv4_range.asn = 0
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:asn], "must be greater than 0"
@ipv4_range.asn = -1
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:asn], "must be greater than 0"
@ipv4_range.asn = "not_a_number"
assert_not @ipv4_range.valid?
@ipv4_range.asn = nil
assert @ipv4_range.valid?
end
# Callbacks
test "should set default source before validation" do
range = NetworkRange.new(network: "10.0.0.0/8")
range.valid?
assert_equal "api_imported", range.source
end
test "should not override existing source" do
range = NetworkRange.new(network: "10.0.0.0/8", source: "user_created")
range.valid?
assert_equal "user_created", range.source
end
# Virtual Attributes (CIDR)
test "cidr getter returns network as string" do
@ipv4_range.save!
assert_equal "192.168.1.0/24", @ipv4_range.cidr
assert_equal "192.168.1.0/24", @ipv4_range.network.to_s
end
test "cidr setter sets network from string" do
range = NetworkRange.new
range.cidr = "10.0.0.0/16"
assert_equal "10.0.0.0/16", range.network.to_s
end
# Network Properties
test "prefix_length returns correct network prefix" do
@ipv4_range.save!
@ipv6_range.save!
assert_equal 24, @ipv4_range.prefix_length
assert_equal 32, @ipv6_range.prefix_length
end
test "network_address returns network address" do
@ipv4_range.save!
assert_equal "192.168.1.0", @ipv4_range.network_address
end
test "broadcast_address returns correct broadcast for IPv4" do
@ipv4_range.save!
assert_equal "192.168.1.255", @ipv4_range.broadcast_address
end
test "broadcast_address returns nil for IPv6" do
@ipv6_range.save!
assert_nil @ipv6_range.broadcast_address
end
test "family detection works correctly" do
@ipv4_range.save!
@ipv6_range.save!
assert_equal 4, @ipv4_range.family
assert_equal 6, @ipv6_range.family
end
test "ipv4? and ipv6? predicate methods work" do
@ipv4_range.save!
@ipv6_range.save!
assert @ipv4_range.ipv4?
assert_not @ipv4_range.ipv6?
assert @ipv6_range.ipv6?
assert_not @ipv6_range.ipv4?
end
test "virtual? works correctly" do
range = NetworkRange.new(network: "10.0.0.0/8")
assert range.virtual?
range.save!
assert_not range.virtual?
end
# Network Containment
test "contains_ip? works correctly" do
@ipv4_range.save!
assert @ipv4_range.contains_ip?("192.168.1.1")
assert @ipv4_range.contains_ip?("192.168.1.254")
assert_not @ipv4_range.contains_ip?("192.168.2.1")
assert_not @ipv4_range.contains_ip?("2001:db8::1")
# Test IPv6
@ipv6_range.save!
assert @ipv6_range.contains_ip?("2001:db8::1")
assert @ipv6_range.contains_ip?("2001:db8:ffff::ffff")
assert_not @ipv6_range.contains_ip?("2001:db9::1")
end
test "contains_network? works correctly" do
@ipv4_range.save!
# More specific network
assert @ipv4_range.contains_network?("192.168.1.0/25")
assert @ipv4_range.contains_network?("192.168.1.128/25")
# Same network
assert @ipv4_range.contains_network?("192.168.1.0/24")
# Less specific network
assert_not @ipv4_range.contains_network?("192.168.0.0/16")
# Different network
assert_not @ipv4_range.contains_network?("10.0.0.0/8")
end
test "overlaps? works correctly" do
@ipv4_range.save!
# Overlapping networks
assert @ipv4_range.overlaps?("192.168.1.0/25") # More specific
assert @ipv4_range.overlaps?("192.168.0.0/23") # Less specific
assert @ipv4_range.overlaps?("192.168.1.128/25") # Partial overlap
# Non-overlapping
assert_not @ipv4_range.overlaps?("10.0.0.0/8")
assert_not @ipv4_range.overlaps?("172.16.0.0/12")
end
# Parent/Child Relationships
test "parent_ranges finds containing networks" do
# Create parent and child networks
parent = NetworkRange.create!(network: "192.168.0.0/16")
@ipv4_range.save! # 192.168.1.0/24
child = NetworkRange.create!(network: "192.168.1.0/25")
parents = @ipv4_range.parent_ranges
assert_includes parents, parent
assert_not_includes parents, child
assert_not_includes parents, @ipv4_range
# Should be ordered by specificity (more specific first)
assert_equal parent, parents.first
end
test "child_ranges finds contained networks" do
# Create parent and child networks
parent = NetworkRange.create!(network: "192.168.0.0/16")
@ipv4_range.save! # 192.168.1.0/24
child = NetworkRange.create!(network: "192.168.1.0/25")
children = parent.child_ranges
assert_includes children, @ipv4_range
assert_includes children, child
assert_not_includes children, parent
# Should be ordered by specificity (less specific first)
assert_equal @ipv4_range, children.first
end
test "sibling_ranges finds same-level networks" do
# Create sibling networks
sibling1 = NetworkRange.create!(network: "192.168.0.0/24")
@ipv4_range.save! # 192.168.1.0/24
sibling2 = NetworkRange.create!(network: "192.168.2.0/24")
siblings = @ipv4_range.sibling_ranges
assert_includes siblings, sibling1
assert_includes siblings, sibling2
assert_not_includes siblings, @ipv4_range
end
# Intelligence and Inheritance
test "has_intelligence? detects available intelligence data" do
range = NetworkRange.new(network: "10.0.0.0/8")
assert_not range.has_intelligence?
range.asn = 12345
assert range.has_intelligence?
range.asn = nil
range.company = "Test Company"
assert range.has_intelligence?
range.company = nil
range.country = "US"
assert range.has_intelligence?
range.country = nil
range.is_datacenter = true
assert range.has_intelligence?
end
test "own_intelligence returns correct data structure" do
range = NetworkRange.create!(
network: "10.0.0.0/8",
asn: 12345,
asn_org: "Test ASN",
company: "Test Company",
country: "US",
is_datacenter: true,
is_proxy: false,
is_vpn: false,
source: "manual"
)
intelligence = range.own_intelligence
assert_equal 12345, intelligence[:asn]
assert_equal "Test ASN", intelligence[:asn_org]
assert_equal "Test Company", intelligence[:company]
assert_equal "US", intelligence[:country]
assert_equal true, intelligence[:is_datacenter]
assert_equal false, intelligence[:is_proxy]
assert_equal false, intelligence[:is_vpn]
assert_equal false, intelligence[:inherited]
assert_equal "manual", intelligence[:source]
end
test "inherited_intelligence returns own data when available" do
child = NetworkRange.create!(
network: "192.168.1.0/24",
country: "US",
company: "Test Company"
)
intelligence = child.inherited_intelligence
assert_equal "US", intelligence[:country]
assert_equal "Test Company", intelligence[:company]
assert_equal false, intelligence[:inherited]
end
test "inherited_intelligence inherits from parent when needed" do
parent = NetworkRange.create!(
network: "192.168.0.0/16",
country: "US",
company: "Test Company"
)
child = NetworkRange.create!(network: "192.168.1.0/24")
intelligence = child.inherited_intelligence
assert_equal "US", intelligence[:country]
assert_equal "Test Company", intelligence[:company]
assert_equal true, intelligence[:inherited]
assert_equal parent.cidr, intelligence[:parent_cidr]
end
test "parent_with_intelligence finds nearest parent with data" do
grandparent = NetworkRange.create!(network: "10.0.0.0/8", country: "US")
parent = NetworkRange.create!(network: "10.1.0.0/16")
child = NetworkRange.create!(network: "10.1.1.0/24")
found_parent = child.parent_with_intelligence
assert_equal grandparent, found_parent
assert_not_equal parent, found_parent
end
# API Data Management
test "is_fetching_api_data? tracks active fetches" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_not range.is_fetching_api_data?(:ipapi)
range.mark_as_fetching_api_data!(:ipapi)
assert range.is_fetching_api_data?(:ipapi)
range.clear_fetching_status!(:ipapi)
assert_not range.is_fetching_api_data?(:ipapi)
end
test "should_fetch_api_data? prevents duplicate fetches" do
range = NetworkRange.create!(network: "10.0.0.0/8")
# Should fetch initially
assert range.should_fetch_api_data?(:ipapi)
# Should not fetch while fetching
range.mark_as_fetching_api_data!(:ipapi)
assert_not range.should_fetch_api_data?(:ipapi)
# Should fetch again after clearing
range.clear_fetching_status!(:ipapi)
assert range.should_fetch_api_data?(:ipapi)
end
test "has_ipapi_data_available? checks inheritance" do
parent = NetworkRange.create!(network: "10.0.0.0/8")
parent.set_network_data(:ipapi, { country: "US" })
parent.save!
child = NetworkRange.create!(network: "10.0.1.0/24")
assert child.has_ipapi_data_available?
end
test "should_fetch_ipapi_data? respects parent fetching status" do
parent = NetworkRange.create!(network: "10.0.0.0/8")
child = NetworkRange.create!(network: "10.0.1.0/24")
parent.mark_as_fetching_api_data!(:ipapi)
assert_not child.should_fetch_ipapi_data?
parent.clear_fetching_status!(:ipapi)
assert child.should_fetch_ipapi_data?
end
# Network Data Management
test "network_data_for and set_network_data work correctly" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_equal({}, range.network_data_for(:ipapi))
data = { country: "US", city: "New York" }
range.set_network_data(:ipapi, data)
range.save!
assert_equal data, range.network_data_for(:ipapi)
end
test "has_network_data_from? checks data presence" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_not range.has_network_data_from?(:ipapi)
range.set_network_data(:ipapi, { country: "US" })
range.save!
assert range.has_network_data_from?(:ipapi)
end
# IPAPI Tracking Methods
test "find_or_create_tracking_network_for_ip works correctly" do
# IPv4 - should create /24
tracking_range = NetworkRange.find_or_create_tracking_network_for_ip("192.168.1.100")
assert_equal "192.168.1.0/24", tracking_range.network.to_s
assert_equal "auto_generated", tracking_range.source
assert_equal "IPAPI tracking network", tracking_range.creation_reason
# IPv6 - should create /64
ipv6_tracking = NetworkRange.find_or_create_tracking_network_for_ip("2001:db8::1")
assert_equal "2001:db8::/64", ipv6_tracking.network.to_s
end
test "should_fetch_ipapi_for_ip? works correctly" do
tracking_range = NetworkRange.create!(network: "192.168.1.0/8")
# Should fetch initially
assert NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100")
# Mark as queried recently
tracking_range.mark_ipapi_queried!("192.168.1.0/24")
assert_not NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100")
# Should fetch for old queries
tracking_range.network_data['ipapi_queried_at'] = 2.years.ago.to_i
tracking_range.save!
assert NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100")
end
test "mark_ipapi_queried! stores query metadata" do
range = NetworkRange.create!(network: "192.168.1.0/24")
range.mark_ipapi_queried!("192.168.1.128/25")
assert range.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
assert_equal "192.168.1.128/25", range.network_data['ipapi_returned_cidr']
end
# JSON Helper Methods
test "abuser_scores_hash handles JSON correctly" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_equal({}, range.abuser_scores_hash)
range.abuser_scores_hash = { ipquality: 85, abuseipdb: 92 }
range.save!
assert_equal({ "ipquality" => 85, "abuseipdb" => 92 }, JSON.parse(range.abuser_scores))
assert_equal({ ipquality: 85, abuseipdb: 92 }, range.abuser_scores_hash)
end
test "additional_data_hash handles JSON correctly" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_equal({}, range.additional_data_hash)
range.additional_data_hash = { tags: ["malicious", "botnet"], notes: "Test data" }
range.save!
parsed_data = JSON.parse(range.additional_data)
assert_equal ["malicious", "botnet"], parsed_data["tags"]
assert_equal "Test data", parsed_data["notes"]
end
# Scopes
test "ipv4 and ipv6 scopes work correctly" do
ipv4_range = NetworkRange.create!(network: "192.168.1.0/24")
ipv6_range = NetworkRange.create!(network: "2001:db8::/32")
assert_includes NetworkRange.ipv4, ipv4_range
assert_not_includes NetworkRange.ipv4, ipv6_range
assert_includes NetworkRange.ipv6, ipv6_range
assert_not_includes NetworkRange.ipv6, ipv4_range
end
test "filtering scopes work correctly" do
range1 = NetworkRange.create!(network: "192.168.1.0/24", country: "US", company: "Google", asn: 15169, is_datacenter: true)
range2 = NetworkRange.create!(network: "10.0.0.0/8", country: "BR", company: "Amazon", asn: 16509, is_proxy: true)
assert_includes NetworkRange.by_country("US"), range1
assert_not_includes NetworkRange.by_country("US"), range2
assert_includes NetworkRange.by_company("Google"), range1
assert_not_includes NetworkRange.by_company("Google"), range2
assert_includes NetworkRange.by_asn(15169), range1
assert_not_includes NetworkRange.by_asn(15169), range2
assert_includes NetworkRange.datacenter, range1
assert_not_includes NetworkRange.datacenter, range2
assert_includes NetworkRange.proxy, range2
assert_not_includes NetworkRange.proxy, range1
end
# Class Methods
test "contains_ip class method finds most specific network" do
parent = NetworkRange.create!(network: "192.168.0.0/16")
child = NetworkRange.create!(network: "192.168.1.0/24")
found = NetworkRange.contains_ip("192.168.1.100")
assert_equal child, found.first # More specific should come first
end
test "overlapping class method finds overlapping networks" do
range1 = NetworkRange.create!(network: "192.168.0.0/16")
range2 = NetworkRange.create!(network: "192.168.1.0/24")
range3 = NetworkRange.create!(network: "10.0.0.0/8")
overlapping = NetworkRange.overlapping("192.168.1.0/24")
assert_includes overlapping, range1
assert_includes overlapping, range2
assert_not_includes overlapping, range3
end
test "find_or_create_by_cidr works correctly" do
# Creates new record
range = NetworkRange.find_or_create_by_cidr("10.0.0.0/8", user: @user, source: "manual")
assert range.persisted?
assert_equal "10.0.0.0/8", range.network.to_s
assert_equal @user, range.user
assert_equal "manual", range.source
# Returns existing record
existing = NetworkRange.find_or_create_by_cidr("10.0.0.0/8")
assert_equal range.id, existing.id
end
test "find_by_ip_or_network handles both IP and network inputs" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Find by IP within range
found_by_ip = NetworkRange.find_by_ip_or_network("192.168.1.100")
assert_includes found_by_ip, range
# Find by exact network
found_by_network = NetworkRange.find_by_ip_or_network("192.168.1.0/24")
assert_includes found_by_network, range
# Return none for invalid input
assert_equal NetworkRange.none, NetworkRange.find_by_ip_or_network("")
assert_equal NetworkRange.none, NetworkRange.find_by_ip_or_network("invalid")
end
# Analytics Methods
test "events_count returns counter cache value" do
range = NetworkRange.create!(network: "192.168.1.0/24")
assert_equal 0, range.events_count
# Update counter cache manually for testing
range.update_column(:events_count, 5)
assert_equal 5, range.events_count
end
test "events method finds events within range" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Create test events
matching_event = Event.create!(
request_id: "test-1",
timestamp: Time.current,
payload: {},
ip_address: "192.168.1.100"
)
non_matching_event = Event.create!(
request_id: "test-2",
timestamp: Time.current,
payload: {},
ip_address: "10.0.0.1"
)
found_events = range.events
assert_includes found_events, matching_event
assert_not_includes found_events, non_matching_event
end
test "blocking_rules and active_rules work correctly" do
range = NetworkRange.create!(network: "192.168.1.0/24")
blocking_rule = Rule.create!(
rule_type: "network",
action: "deny",
network_range: range,
user: @user,
enabled: true
)
allow_rule = Rule.create!(
rule_type: "network",
action: "allow",
network_range: range,
user: @user,
enabled: true
)
disabled_rule = Rule.create!(
rule_type: "network",
action: "deny",
network_range: range,
user: @user,
enabled: false
)
assert_includes range.blocking_rules, blocking_rule
assert_not_includes range.blocking_rules, allow_rule
assert_not_includes range.blocking_rules, disabled_rule
assert_includes range.active_rules, blocking_rule
assert_includes range.active_rules, allow_rule
assert_not_includes range.active_rules, disabled_rule
end
# Policy Evaluation
test "needs_policy_evaluation? works correctly" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Should evaluate if never evaluated
assert range.needs_policy_evaluation?
# Should evaluate if policies updated since last evaluation
range.update!(policies_evaluated_at: 1.hour.ago)
WafPolicy.create!(name: "Test Policy", policy_type: "country", targets: ["US"], policy_action: "deny", user: @user)
assert range.needs_policy_evaluation?
# Should not evaluate if up to date
range.update!(policies_evaluated_at: 5.minutes.ago)
assert_not range.needs_policy_evaluation?
end
# String Representations
test "to_s returns cidr" do
@ipv4_range.save!
assert_equal @ipv4_range.cidr, @ipv4_range.to_s
end
test "to_param parameterizes cidr" do
@ipv4_range.save!
assert_equal "192.168.1.0_24", @ipv4_range.to_param
end
# Geographic Lookup
test "geo_lookup_country! updates country when available" do
range = NetworkRange.create!(network: "8.8.8.0/24") # Google's network
# Mock GeoIpService
GeoIpService.expects(:lookup_country).with("8.8.8.0").returns("US")
range.geo_lookup_country!
assert_equal "US", range.reload.country
end
test "geo_lookup_country! handles errors gracefully" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Mock GeoIpService to raise error
GeoIpService.expects(:lookup_country).with("192.168.1.0").raises(StandardError.new("Service error"))
# Should not raise error but log it
assert_nothing_raised do
range.geo_lookup_country!
end
assert_nil range.reload.country
end
# Stats Methods
test "import_stats_by_source returns statistics" do
NetworkRange.create!(network: "10.0.0.0/8", source: "manual")
NetworkRange.create!(network: "192.168.1.0/24", source: "api_imported")
NetworkRange.create!(network: "172.16.0.0/12", source: "api_imported")
stats = NetworkRange.import_stats_by_source
assert_equal 2, stats.count
api_stats = stats.find { |s| s.source == "api_imported" }
assert_equal 2, api_stats.count
end
test "geolite_coverage_stats returns detailed coverage information" do
NetworkRange.create!(network: "10.0.0.0/8", source: "geolite_asn", asn: 12345)
NetworkRange.create!(network: "192.168.1.0/24", source: "geolite_country", country: "US")
NetworkRange.create!(network: "172.16.0.0/12", source: "geolite_asn", country: "BR")
stats = NetworkRange.geolite_coverage_stats
assert_equal 3, stats[:total_networks]
assert_equal 2, stats[:asn_networks]
assert_equal 1, stats[:country_networks]
assert_equal 2, stats[:with_asn_data]
assert_equal 1, stats[:with_country_data]
assert_equal 2, stats[:unique_countries]
assert_equal 2, stats[:unique_asns]
end
end

View File

@@ -3,25 +3,30 @@
require "test_helper"
class RuleTest < ActiveSupport::TestCase
# Validation tests
test "should create valid network_v4 rule" do
test "should create valid network rule" do
network_range = NetworkRange.create!(cidr: "10.0.0.0/8")
rule = Rule.new(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
source: "manual"
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
source: "manual",
user: users(:one)
)
assert rule.valid?
rule.save!
assert_equal 8, rule.priority # Auto-calculated from CIDR prefix
end
test "should create valid network_v6 rule" do
test "should create valid network rule with IPv6" do
network_range = NetworkRange.create!(cidr: "2001:db8::/32")
rule = Rule.new(
rule_type: "network_v6",
action: "deny",
conditions: { cidr: "2001:db8::/32" },
source: "manual"
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
source: "manual",
user: users(:one)
)
assert rule.valid?
rule.save!
@@ -30,53 +35,58 @@ class RuleTest < ActiveSupport::TestCase
test "should create valid rate_limit rule" do
rule = Rule.new(
rule_type: "rate_limit",
action: "rate_limit",
waf_rule_type: "rate_limit",
waf_action: "rate_limit",
conditions: { cidr: "0.0.0.0/0", scope: "global" },
metadata: { limit: 100, window: 60 },
source: "manual"
source: "manual",
user: users(:one)
)
assert rule.valid?
end
test "should create valid path_pattern rule" do
rule = Rule.new(
rule_type: "path_pattern",
action: "log",
waf_rule_type: "path_pattern",
waf_action: "log",
conditions: { patterns: ["/.env", "/.git"] },
source: "default"
source: "default",
user: users(:one)
)
assert rule.valid?
end
test "should require rule_type" do
rule = Rule.new(action: "deny", conditions: { cidr: "10.0.0.0/8" })
test "should require waf_rule_type" do
rule = Rule.new(waf_action: "deny", waf_rule_type: nil, conditions: { patterns: ["/test"] }, user: users(:one))
assert_not rule.valid?
assert_includes rule.errors[:rule_type], "can't be blank"
assert_includes rule.errors[:waf_rule_type], "can't be blank"
end
test "should require action" do
rule = Rule.new(rule_type: "network_v4", conditions: { cidr: "10.0.0.0/8" })
test "should require waf_action" do
rule = Rule.new(waf_rule_type: "path_pattern", waf_action: nil, conditions: { patterns: ["/test"] }, user: users(:one))
assert_not rule.valid?
assert_includes rule.errors[:action], "can't be blank"
assert_includes rule.errors[:waf_action], "can't be blank"
end
test "should validate network_v4 has valid IPv4 CIDR" do
test "should validate network has valid CIDR" do
rule = Rule.new(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "2001:db8::/32" } # IPv6 in IPv4 rule
waf_rule_type: "network",
waf_action: "deny",
conditions: { cidr: "invalid-cidr" }, # Invalid CIDR
user: users(:one)
)
assert_not rule.valid?
assert_includes rule.errors[:conditions], "cidr must be IPv4 for network_v4 rules"
# Network rules now validate differently - they need a network_range
assert_includes rule.errors[:network_range], "is required for network rules"
end
test "should validate rate_limit has limit and window in metadata" do
rule = Rule.new(
rule_type: "rate_limit",
action: "rate_limit",
waf_rule_type: "rate_limit",
waf_action: "rate_limit",
conditions: { cidr: "0.0.0.0/0", scope: "global" },
metadata: { limit: 100 } # Missing window
metadata: { limit: 100 }, # Missing window
user: users(:one)
)
assert_not rule.valid?
assert_includes rule.errors[:metadata], "must include 'limit' and 'window' for rate_limit rules"
@@ -84,46 +94,56 @@ class RuleTest < ActiveSupport::TestCase
# Default value tests
test "should default enabled to true" do
network_range = NetworkRange.create!(cidr: "10.0.0.0/8")
rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" }
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
user: users(:one)
)
assert rule.enabled?
end
# Priority calculation tests
test "should calculate priority from IPv4 CIDR prefix" do
network_range = NetworkRange.create!(cidr: "192.168.1.0/24")
rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.1.0/24" }
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
user: users(:one)
)
assert_equal 24, rule.priority
end
# Scope tests
test "active scope returns enabled and non-expired rules" do
active_range = NetworkRange.create!(cidr: "10.0.0.0/8")
active = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
enabled: true
)
disabled = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "192.168.0.0/16" },
enabled: false
)
expired = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "172.16.0.0/12" },
waf_rule_type: "network",
waf_action: "deny",
network_range: active_range,
enabled: true,
expires_at: 1.hour.ago
user: users(:one)
)
disabled_range = NetworkRange.create!(cidr: "192.168.0.0/16")
disabled = Rule.create!(
waf_rule_type: "network",
waf_action: "deny",
network_range: disabled_range,
enabled: false,
user: users(:one)
)
expired_range = NetworkRange.create!(cidr: "172.16.0.0/12")
expired = Rule.create!(
waf_rule_type: "network",
waf_action: "deny",
network_range: expired_range,
enabled: true,
expires_at: 1.hour.ago,
user: users(:one)
)
results = Rule.active.to_a
@@ -134,20 +154,24 @@ class RuleTest < ActiveSupport::TestCase
# Instance method tests
test "active? returns true for enabled non-expired rule" do
network_range = NetworkRange.create!(cidr: "10.0.0.0/8")
rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
enabled: true
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
enabled: true,
user: users(:one)
)
assert rule.active?
end
test "disable! sets enabled to false and adds metadata" do
network_range = NetworkRange.create!(cidr: "10.0.0.0/8")
rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" }
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
user: users(:one)
)
rule.disable!(reason: "False positive")
@@ -159,20 +183,22 @@ class RuleTest < ActiveSupport::TestCase
# Agent format tests
test "to_agent_format returns correct structure" do
network_range = NetworkRange.create!(cidr: "10.0.0.0/8")
rule = Rule.create!(
rule_type: "network_v4",
action: "deny",
conditions: { cidr: "10.0.0.0/8" },
waf_rule_type: "network",
waf_action: "deny",
network_range: network_range,
expires_at: 1.day.from_now,
source: "manual",
metadata: { reason: "Test" }
metadata: { reason: "Test" },
user: users(:one)
)
format = rule.to_agent_format
assert_equal rule.id, format[:id]
assert_equal "network_v4", format[:rule_type]
assert_equal "deny", format[:action]
assert_equal "network", format[:waf_rule_type]
assert_equal "deny", format[:waf_action]
assert_equal 8, format[:priority]
assert_equal true, format[:enabled]
end

View File

@@ -0,0 +1,7 @@
require "test_helper"
class SettingTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View File

@@ -0,0 +1,474 @@
require "test_helper"
class WafPolicyTest < ActiveSupport::TestCase
setup do
@user = users(:jason)
@policy = WafPolicy.new(
name: "Block Malicious IPs",
policy_type: "country",
targets: ["BR", "CN"],
policy_action: "deny",
user: @user
)
end
# Validations
test "should be valid with all required attributes" do
assert @policy.valid?
end
test "should not be valid without name" do
@policy.name = nil
assert_not @policy.valid?
assert_includes @policy.errors[:name], "can't be blank"
end
test "should not be valid without unique name" do
@policy.name = waf_policies(:one).name
assert_not @policy.valid?
assert_includes @policy.errors[:name], "has already been taken"
end
test "should validate policy_type inclusion" do
@policy.policy_type = "invalid_type"
assert_not @policy.valid?
assert_includes @policy.errors[:policy_type], "is not included in the list"
end
test "should validate policy_action inclusion" do
@policy.policy_action = "invalid_action"
assert_not @policy.valid?
assert_includes @policy.errors[:policy_action], "is not included in the list"
end
test "should not be valid without targets" do
@policy.targets = []
assert_not @policy.valid?
assert_includes @policy.errors[:targets], "can't be blank"
end
test "should validate targets is an array" do
@policy.targets = "not an array"
assert_not @policy.valid?
assert_includes @policy.errors[:targets], "must be an array"
end
test "should validate country targets format" do
@policy.policy_type = "country"
# Valid country codes
@policy.targets = ["US", "BR", "CN"]
assert @policy.valid?
# Invalid country codes
@policy.targets = ["USA", "123", "B"]
assert_not @policy.valid?
assert_includes @policy.errors[:targets], "must be valid ISO country codes"
end
test "should validate ASN targets format" do
@policy.policy_type = "asn"
# Valid ASNs
@policy.targets = [12345, 67890]
assert @policy.valid?
# Invalid ASNs
@policy.targets = ["AS12345", -1, 0]
assert_not @policy.valid?
assert_includes @policy.errors[:targets], "must be valid ASNs"
end
test "should validate company targets format" do
@policy.policy_type = "company"
# Valid company names
@policy.targets = ["Google", "Amazon Web Services", "Microsoft"]
assert @policy.valid?
# Invalid company names
@policy.targets = ["", nil, " "]
assert_not @policy.valid?
assert_includes @policy.errors[:targets], "must be valid company names"
end
test "should validate network_type targets format" do
@policy.policy_type = "network_type"
# Valid network types
@policy.targets = ["datacenter", "proxy", "vpn", "standard"]
assert @policy.valid?
# Invalid network types
@policy.targets = ["invalid", "malicious", "botnet"]
assert_not @policy.valid?
assert_includes @policy.errors[:targets], "must be one of: datacenter, proxy, vpn, standard"
end
test "should validate redirect configuration" do
@policy.policy_action = "redirect"
# Valid redirect config
@policy.additional_data = { "redirect_url" => "https://example.com/blocked" }
assert @policy.valid?
# Missing redirect URL
@policy.additional_data = { "other_config" => "value" }
assert_not @policy.valid?
assert_includes @policy.errors[:additional_data], "must include 'redirect_url' for redirect action"
end
test "should validate challenge configuration" do
@policy.policy_action = "challenge"
# Valid challenge types
@policy.additional_data = { "challenge_type" => "captcha" }
assert @policy.valid?
@policy.additional_data = { "challenge_type" => "javascript" }
assert @policy.valid?
# Invalid challenge type
@policy.additional_data = { "challenge_type" => "invalid" }
assert_not @policy.valid?
assert_includes @policy.errors[:additional_data], "challenge_type must be one of: captcha, javascript, proof_of_work"
# No challenge type (should be valid, uses defaults)
@policy.additional_data = {}
assert @policy.valid?
end
# Defaults and Callbacks
test "should default to enabled" do
@policy.enabled = nil
@policy.save!
assert @policy.enabled?
end
test "should default targets to empty array" do
policy = WafPolicy.new(
name: "Test Policy",
policy_type: "country",
policy_action: "deny",
user: @user
)
# before_validation should set defaults
policy.valid?
assert_equal [], policy.targets
end
test "should default additional_data to empty hash" do
policy = WafPolicy.new(
name: "Test Policy",
policy_type: "country",
targets: ["US"],
policy_action: "deny",
user: @user
)
policy.valid?
assert_equal({}, policy.additional_data)
end
# Policy Type Methods
test "policy type predicate methods work correctly" do
country_policy = WafPolicy.new(policy_type: "country")
assert country_policy.country_policy?
assert_not country_policy.asn_policy?
assert_not country_policy.company_policy?
assert_not country_policy.network_type_policy?
asn_policy = WafPolicy.new(policy_type: "asn")
assert_not asn_policy.country_policy?
assert asn_policy.asn_policy?
assert_not asn_policy.company_policy?
assert_not asn_policy.network_type_policy?
company_policy = WafPolicy.new(policy_type: "company")
assert_not company_policy.country_policy?
assert_not company_policy.asn_policy?
assert company_policy.company_policy?
assert_not company_policy.network_type_policy?
network_type_policy = WafPolicy.new(policy_type: "network_type")
assert_not network_type_policy.country_policy?
assert_not network_type_policy.asn_policy?
assert_not network_type_policy.company_policy?
assert network_type_policy.network_type_policy?
end
# Action Methods
test "action predicate methods work correctly" do
allow_policy = WafPolicy.new(policy_action: "allow")
assert allow_policy.allow_action?
assert_not allow_policy.deny_action?
assert_not allow_policy.redirect_action?
assert_not allow_policy.challenge_action?
deny_policy = WafPolicy.new(policy_action: "deny")
assert_not deny_policy.allow_action?
assert deny_policy.deny_action?
assert_not deny_policy.redirect_action?
assert_not deny_policy.challenge_action?
redirect_policy = WafPolicy.new(policy_action: "redirect")
assert_not redirect_policy.allow_action?
assert_not redirect_policy.deny_action?
assert redirect_policy.redirect_action?
assert_not redirect_policy.challenge_action?
challenge_policy = WafPolicy.new(policy_action: "challenge")
assert_not challenge_policy.allow_action?
assert_not challenge_policy.deny_action?
assert_not challenge_policy.redirect_action?
assert challenge_policy.challenge_action?
end
# Policy action methods (to avoid Rails conflicts)
test "policy action predicate methods work correctly" do
policy = WafPolicy.new(policy_action: "deny")
assert policy.deny_policy_action?
assert_not policy.allow_policy_action?
assert_not policy.redirect_policy_action?
assert_not policy.challenge_policy_action?
end
# Lifecycle Methods
test "active? works correctly" do
# Active policy
active_policy = WafPolicy.new(enabled: true, expires_at: nil)
assert active_policy.active?
# Enabled but expired
expired_policy = WafPolicy.new(enabled: true, expires_at: 1.day.ago)
assert_not expired_policy.active?
# Disabled with future expiration
disabled_policy = WafPolicy.new(enabled: false, expires_at: 1.day.from_now)
assert_not disabled_policy.active?
# Disabled with no expiration
disabled_no_exp = WafPolicy.new(enabled: false, expires_at: nil)
assert_not disabled_no_exp.active?
# Enabled with future expiration
future_exp = WafPolicy.new(enabled: true, expires_at: 1.day.from_now)
assert future_exp.active?
end
test "expired? works correctly" do
assert_not WafPolicy.new(expires_at: nil).expired?
assert_not WafPolicy.new(expires_at: 1.day.from_now).expired?
assert WafPolicy.new(expires_at: 1.day.ago).expired?
assert WafPolicy.new(expires_at: Time.current).expired?
end
test "activate! enables policy" do
@policy.enabled = false
@policy.save!
@policy.activate!
assert @policy.reload.enabled?
end
test "deactivate! disables policy" do
@policy.enabled = true
@policy.save!
@policy.deactivate!
assert_not @policy.reload.enabled?
end
test "expire! sets expiration to now" do
@policy.expire!
assert @policy.reload.expires_at <= Time.current
end
# Scopes
test "enabled scope returns only enabled policies" do
enabled_policy = WafPolicy.create!(
name: "Enabled Policy",
policy_type: "country",
targets: ["US"],
policy_action: "deny",
user: @user,
enabled: true
)
disabled_policy = WafPolicy.create!(
name: "Disabled Policy",
policy_type: "country",
targets: ["US"],
policy_action: "deny",
user: @user,
enabled: false
)
enabled_policies = WafPolicy.enabled
assert_includes enabled_policies, enabled_policy
assert_not_includes enabled_policies, disabled_policy
end
test "active scope returns only active policies" do
active_policy = WafPolicy.create!(
name: "Active Policy",
policy_type: "country",
targets: ["US"],
policy_action: "deny",
user: @user,
enabled: true,
expires_at: 1.day.from_now
)
expired_policy = WafPolicy.create!(
name: "Expired Policy",
policy_type: "country",
targets: ["US"],
policy_action: "deny",
user: @user,
enabled: true,
expires_at: 1.day.ago
)
disabled_policy = WafPolicy.create!(
name: "Disabled Policy",
policy_type: "country",
targets: ["US"],
policy_action: "deny",
user: @user,
enabled: false
)
active_policies = WafPolicy.active
assert_includes active_policies, active_policy
assert_not_includes active_policies, expired_policy
assert_not_includes active_policies, disabled_policy
end
# Class Factory Methods
test "create_country_policy works correctly" do
policy = WafPolicy.create_country_policy(
["US", "CA"],
policy_action: "deny",
user: @user,
name: "Custom Name"
)
assert policy.persisted?
assert_equal "Custom Name", policy.name
assert_equal "country", policy.policy_type
assert_equal "deny", policy.policy_action
assert_equal ["US", "CA"], policy.targets
assert_equal @user, policy.user
end
test "create_asn_policy works correctly" do
policy = WafPolicy.create_asn_policy(
[12345, 67890],
policy_action: "challenge",
user: @user
)
assert policy.persisted?
assert_equal "challenge ASNs 12345, 67890", policy.name
assert_equal "asn", policy.policy_type
assert_equal "challenge", policy.policy_action
assert_equal [12345, 67890], policy.targets
end
test "create_company_policy works correctly" do
policy = WafPolicy.create_company_policy(
["Google", "Amazon"],
policy_action: "deny",
user: @user
)
assert policy.persisted?
assert_equal "deny Google, Amazon", policy.name
assert_equal "company", policy.policy_type
assert_equal ["Google", "Amazon"], policy.targets
end
test "create_network_type_policy works correctly" do
policy = WafPolicy.create_network_type_policy(
["datacenter", "proxy"],
policy_action: "redirect",
user: @user,
additional_data: { redirect_url: "https://example.com/blocked" }
)
assert policy.persisted?
assert_equal "redirect datacenter, proxy", policy.name
assert_equal "network_type", policy.policy_type
assert_equal ["datacenter", "proxy"], policy.targets
end
# Redirect/Challenge Specific Methods
test "redirect_url and redirect_status methods work" do
policy = WafPolicy.new(
policy_action: "redirect",
additional_data: {
"redirect_url" => "https://example.com/blocked",
"redirect_status" => 301
}
)
assert_equal "https://example.com/blocked", policy.redirect_url
assert_equal 301, policy.redirect_status
# Default status
policy.additional_data = { "redirect_url" => "https://example.com/blocked" }
assert_equal 302, policy.redirect_status
end
test "challenge_type and challenge_message methods work" do
policy = WafPolicy.new(
policy_action: "challenge",
additional_data: {
"challenge_type" => "javascript",
"challenge_message" => "Please verify you are human"
}
)
assert_equal "javascript", policy.challenge_type
assert_equal "Please verify you are human", policy.challenge_message
# Default challenge type
policy.additional_data = {}
assert_equal "captcha", policy.challenge_type
end
# Statistics
test "generated_rules_count works" do
@policy.save!
# Initially no rules
assert_equal 0, @policy.generated_rules_count
# Create some rules
network_range = NetworkRange.create!(ip_range: "192.168.1.0/24")
@policy.create_rule_for_network_range(network_range)
assert_equal 1, @policy.generated_rules_count
end
test "effectiveness_stats returns correct data" do
@policy.save!
stats = @policy.effectiveness_stats
assert_equal 0, stats[:total_rules_generated]
assert_equal 0, stats[:active_rules]
assert_equal 0, stats[:rules_last_7_days]
assert_equal "country", stats[:policy_type]
assert_equal "deny", stats[:policy_action]
assert_equal 2, stats[:targets_count]
end
# String representations
test "to_s returns name" do
assert_equal @policy.name, @policy.to_s
end
test "to_param parameterizes name" do
@policy.name = "Block Brazil & China"
expected = "block-brazil-china"
assert_equal expected, @policy.to_param
end
end

View File

@@ -0,0 +1,158 @@
# frozen_string_literal: true
require "test_helper"
class DsnAuthenticationServiceTest < ActiveSupport::TestCase
def setup
@dsn = Dsn.create!(name: "Test DSN", key: "test-auth-key-1234567890abcdef")
end
test "should authenticate via query parameter baffle_key" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => @dsn.key }
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via query parameter sentry_key" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "sentry_key" => @dsn.key }
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via query parameter glitchtip_key" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "glitchtip_key" => @dsn.key }
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via X-Baffle-Auth header" do
request = ActionDispatch::TestRequest.create
request.headers["X-Baffle-Auth"] = "Baffle baffle_key=#{@dsn.key}, baffle_version=1"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via X-Sentry-Auth header" do
request = ActionDispatch::TestRequest.create
request.headers["X-Sentry-Auth"] = "Sentry sentry_key=#{@dsn.key}, sentry_version=7"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via Authorization Bearer header" do
request = ActionDispatch::TestRequest.create
request.headers["Authorization"] = "Bearer #{@dsn.key}"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should authenticate via Basic auth with username as key" do
request = ActionDispatch::TestRequest.create
credentials = Base64.strict_encode64("#{@dsn.key}:ignored-password")
request.headers["Authorization"] = "Basic #{credentials}"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should prioritize query parameter over other methods" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => @dsn.key }
request.headers["Authorization"] = "Bearer wrong-key"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should fail authentication with disabled DSN" do
@dsn.update!(enabled: false)
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => @dsn.key }
assert_raises(DsnAuthenticationService::AuthenticationError) do
DsnAuthenticationService.authenticate(request)
end
end
test "should fail authentication with non-existent key" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => "non-existent-key" }
assert_raises(DsnAuthenticationService::AuthenticationError) do
DsnAuthenticationService.authenticate(request)
end
end
test "should fail authentication with no authentication method" do
request = ActionDispatch::TestRequest.create
assert_raises(DsnAuthenticationService::AuthenticationError) do
DsnAuthenticationService.authenticate(request)
end
end
test "should handle malformed Authorization header" do
request = ActionDispatch::TestRequest.create
request.headers["Authorization"] = "InvalidHeader"
assert_nil DsnAuthenticationService.authenticate(request)
end
test "should handle malformed Basic auth" do
request = ActionDispatch::TestRequest.create
request.headers["Authorization"] = "Basic invalid-base64"
assert_nil DsnAuthenticationService.authenticate(request)
end
test "should handle malformed X-Baffle-Auth header" do
request = ActionDispatch::TestRequest.create
request.headers["X-Baffle-Auth"] = "Invalid format"
assert_nil DsnAuthenticationService.authenticate(request)
end
test "should handle empty query parameters" do
request = ActionDispatch::TestRequest.create
request.query_parameters = { "baffle_key" => "" }
assert_nil DsnAuthenticationService.authenticate(request)
end
test "should extract key from complex X-Baffle-Auth header" do
request = ActionDispatch::TestRequest.create
request.headers["X-Baffle-Auth"] = "Baffle baffle_key=#{@dsn.key}, baffle_version=1, baffle_client=ruby-2.0.0"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should handle URL-style DSN in Basic auth" do
# This simulates using the full DSN URL: https://key@domain.com
request = ActionDispatch::TestRequest.create
credentials = Base64.strict_encode64("#{@dsn.key}:")
request.headers["Authorization"] = "Basic #{credentials}"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal @dsn, authenticated_dsn
end
test "should handle special characters in DSN keys" do
special_dsn = Dsn.create!(name: "Special DSN", key: "special-key-with-dashes_123")
request = ActionDispatch::TestRequest.create
request.headers["Authorization"] = "Bearer #{special_dsn.key}"
authenticated_dsn = DsnAuthenticationService.authenticate(request)
assert_equal special_dsn, authenticated_dsn
end
end

View File

@@ -0,0 +1,530 @@
require "test_helper"
class WafPolicyMatcherTest < ActiveSupport::TestCase
setup do
@user = users(:jason)
@network_range = NetworkRange.create!(
network: "192.168.1.0/24",
country: "BR",
company: "Test Company",
asn: 12345,
is_datacenter: false
)
@matcher = WafPolicyMatcher.new(network_range: @network_range)
end
teardown do
WafPolicy.delete_all
Rule.delete_all
NetworkRange.delete_all
end
# Initialization
test "initializes with network range" do
assert_equal @network_range, @matcher.network_range
assert_equal [], @matcher.matching_policies
assert_equal [], @matcher.generated_rules
end
test "handles nil network range" do
matcher = WafPolicyMatcher.new(network_range: nil)
assert_nil matcher.network_range
end
# Policy Matching
test "find_matching_policies returns policies that match network range" do
# Create policies that should match
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
test_asn_policy = WafPolicy.create_asn_policy([12345], 'challenge', @user, "Challenge ASN")
test_company_policy = WafPolicy.create_company_policy(['Test Company'], 'redirect', @user, "Redirect Company")
# Create policies that should not match
us_policy = WafPolicy.create_country_policy(['US'], 'deny', @user, "Block US")
other_asn_policy = WafPolicy.create_asn_policy([67890], 'deny', @user, "Block Other ASN")
matching_policies = @matcher.find_matching_policies
assert_includes matching_policies, brazil_policy
assert_includes matching_policies, test_asn_policy
assert_includes matching_policies, test_company_policy
assert_not_includes matching_policies, us_policy
assert_not_includes matching_policies, other_asn_policy
end
test "find_matching_policies sorts by policy type priority" do
# Create different policy types with same creation time
base_time = 1.hour.ago
# Country policy (highest priority)
country_policy = WafPolicy.create!(
name: "Country Policy",
policy_type: "country",
targets: ["BR"],
policy_action: "deny",
user: @user,
created_at: base_time
)
# ASN policy (second priority)
asn_policy = WafPolicy.create!(
name: "ASN Policy",
policy_type: "asn",
targets: [12345],
policy_action: "deny",
user: @user,
created_at: base_time
)
# Company policy (third priority)
company_policy = WafPolicy.create!(
name: "Company Policy",
policy_type: "company",
targets: ["Test Company"],
policy_action: "deny",
user: @user,
created_at: base_time
)
# Network type policy (lowest priority)
network_type_policy = WafPolicy.create!(
name: "Network Type Policy",
policy_type: "network_type",
targets: ["standard"],
policy_action: "deny",
user: @user,
created_at: base_time
)
matching_policies = @matcher.find_matching_policies
# Should be ordered by priority: country > asn > company > network_type
assert_equal country_policy, matching_policies[0]
assert_equal asn_policy, matching_policies[1]
assert_equal company_policy, matching_policies[2]
assert_equal network_type_policy, matching_policies[3]
end
test "find_matching_policies sorts by creation date for same priority" do
# Create two country policies with different creation times
older_policy = WafPolicy.create!(
name: "Older Policy",
policy_type: "country",
targets: ["BR"],
policy_action: "deny",
user: @user,
created_at: 2.hours.ago
)
newer_policy = WafPolicy.create!(
name: "Newer Policy",
policy_type: "country",
targets: ["BR"],
policy_action: "deny",
user: @user,
created_at: 1.hour.ago
)
matching_policies = @matcher.find_matching_policies
# Newer policy should come first
assert_equal newer_policy, matching_policies[0]
assert_equal older_policy, matching_policies[1]
end
test "find_matching_policies skips inactive policies" do
# Create active policy
active_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Active Policy")
# Create disabled policy
disabled_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Disabled Policy")
disabled_policy.update!(enabled: false)
# Create expired policy
expired_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Expired Policy")
expired_policy.update!(expires_at: 1.hour.ago)
matching_policies = @matcher.find_matching_policies
assert_includes matching_policies, active_policy
assert_not_includes matching_policies, disabled_policy
assert_not_includes matching_policies, expired_policy
end
test "find_matching_policies returns empty array for nil network range" do
matcher = WafPolicyMatcher.new(network_range: nil)
matching_policies = matcher.find_matching_policies
assert_equal [], matching_policies
end
# Rule Generation
test "generate_rules creates rules for matching policies" do
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
@matcher.instance_variable_set(:@matching_policies, [brazil_policy])
generated_rules = @matcher.generate_rules
assert_equal 1, generated_rules.length
rule = generated_rules.first
assert_equal brazil_policy, rule.waf_policy
assert_equal @network_range, rule.network_range
assert_equal "deny", rule.action
end
test "generate_rules handles multiple matching policies" do
policies = [
WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil"),
WafPolicy.create_asn_policy([12345], 'challenge', @user, "Challenge ASN"),
WafPolicy.create_company_policy(['Test Company'], 'redirect', @user, "Redirect Company")
]
@matcher.instance_variable_set(:@matching_policies, policies)
generated_rules = @matcher.generate_rules
assert_equal 3, generated_rules.length
assert_equal "deny", generated_rules[0].action
assert_equal "challenge", generated_rules[1].action
assert_equal "redirect", generated_rules[2].action
end
test "generate_rules returns existing rules instead of duplicates" do
policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
# Create existing rule
existing_rule = Rule.create!(
rule_type: "network",
action: "deny",
network_range: @network_range,
waf_policy: policy,
user: @user,
enabled: true
)
@matcher.instance_variable_set(:@matching_policies, [policy])
generated_rules = @matcher.generate_rules
assert_equal 1, generated_rules.length
assert_equal existing_rule, generated_rules.first
end
test "generate_rules handles policy that fails to create rule" do
policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
# Mock policy to return nil for rule creation (e.g., expired policy)
policy.expects(:create_rule_for_network_range).with(@network_range).returns(nil)
@matcher.instance_variable_set(:@matching_policies, [policy])
generated_rules = @matcher.generate_rules
assert_equal [], generated_rules
end
test "generate_rules returns empty array for no matching policies" do
@matcher.instance_variable_set(:@matching_policies, [])
generated_rules = @matcher.generate_rules
assert_equal [], generated_rules
end
# Combined Operations
test "match_and_generate_rules does both operations" do
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
result = @matcher.match_and_generate_rules
assert_equal 1, result[:matching_policies].length
assert_equal 1, result[:generated_rules].length
assert_includes result[:matching_policies], brazil_policy
assert_equal brazil_policy, result[:generated_rules].first.waf_policy
end
# Class Methods
test "self.process_network_range creates matcher and processes" do
policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
result = WafPolicyMatcher.process_network_range(@network_range)
assert_equal 1, result[:matching_policies].length
assert_equal 1, result[:generated_rules].length
end
test "self.evaluate_and_mark! processes and marks as evaluated" do
policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
original_evaluated_at = @network_range.policies_evaluated_at
result = WafPolicyMatcher.evaluate_and_mark!(@network_range)
assert_equal 1, result[:matching_policies].length
assert_equal 1, result[:generated_rules].length
@network_range.reload
assert @network_range.policies_evaluated_at > original_evaluated_at
end
test "self.evaluate_and_mark! handles nil network range" do
result = WafPolicyMatcher.evaluate_and_mark!(nil)
assert_equal({ matching_policies: [], generated_rules: [] }, result)
end
test "self.batch_process_network_ranges processes multiple ranges" do
# Create multiple network ranges
range1 = NetworkRange.create!(network: "192.168.1.0/24", country: "BR")
range2 = NetworkRange.create!(network: "192.168.2.0/24", country: "US")
# Create policies
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
us_policy = WafPolicy.create_country_policy(['US'], 'deny', @user, "Block US")
results = WafPolicyMatcher.batch_process_network_ranges([range1, range2])
assert_equal 2, results.length
assert_equal range1, results[0][:network_range]
assert_equal range2, results[1][:network_range]
assert_equal 1, results[0][:matching_policies].length
assert_equal 1, results[1][:matching_policies].length
end
test "self.process_ranges_without_policy_rules finds ranges needing evaluation" do
# Create range with intelligence but no rules
intelligent_range = NetworkRange.create!(
network: "192.168.1.0/24",
country: "BR",
asn: 12345
)
# Create range with no intelligence
dumb_range = NetworkRange.create!(
network: "192.168.2.0/24"
)
# Create range with existing rules
range_with_rules = NetworkRange.create!(
network: "192.168.3.0/24",
country: "US"
)
policy = WafPolicy.create_country_policy(['US'], 'deny', @user, "Block US")
Rule.create!(
rule_type: "network",
action: "deny",
network_range: range_with_rules,
waf_policy: policy,
user: @user
)
results = WafPolicyMatcher.process_ranges_without_policy_rules(limit: 10)
# Should only process the intelligent range without rules
assert_equal 1, results.length
assert_equal intelligent_range, results[0][:network_range]
end
test "self.reprocess_all_for_policy finds potential ranges for policy" do
# Create country policy
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
# Create matching and non-matching ranges
brazil_range = NetworkRange.create!(network: "192.168.1.0/24", country: "BR")
us_range = NetworkRange.create!(network: "192.168.2.0/24", country: "US")
results = WafPolicyMatcher.reprocess_all_for_policy(brazil_policy)
assert_equal 1, results.length
assert_equal brazil_range, results[0][:network_range]
assert_not_nil results[0][:generated_rule]
end
test "self.reprocess_all_for_policy handles different policy types" do
# Test ASN policy
asn_policy = WafPolicy.create_asn_policy([12345], 'deny', @user, "Block ASN")
asn_range = NetworkRange.create!(network: "192.168.1.0/24", asn: 12345)
# Test company policy
company_policy = WafPolicy.create_company_policy(['Test Corp'], 'deny', @user, "Block Company")
company_range = NetworkRange.create!(network: "192.168.2.0/24", company: "Test Corporation")
# Test network type policy
network_type_policy = WafPolicy.create_network_type_policy(['datacenter'], 'deny', @user, "Block Datacenter")
dc_range = NetworkRange.create!(network: "192.168.3.0/24", is_datacenter: true)
asn_results = WafPolicyMatcher.reprocess_all_for_policy(asn_policy)
company_results = WafPolicyMatcher.reprocess_all_for_policy(company_policy)
network_type_results = WafPolicyMatcher.reprocess_all_for_policy(network_type_policy)
assert_equal 1, asn_results.length
assert_equal 1, company_results.length
assert_equal 1, network_type_results.length
end
# Statistics and Reporting
test "self.matching_policies_for_network_range returns matching policies" do
policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
matching_policies = WafPolicyMatcher.matching_policies_for_network_range(@network_range)
assert_equal 1, matching_policies.length
assert_includes matching_policies, policy
end
test "self.policy_effectiveness_stats returns correct statistics" do
policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
# Create some rules for the policy
range1 = NetworkRange.create!(network: "192.168.1.0/24", country: "BR")
range2 = NetworkRange.create!(network: "192.168.2.0/24", country: "BR")
rule1 = Rule.create!(
rule_type: "network",
action: "deny",
network_range: range1,
waf_policy: policy,
user: @user
)
rule2 = Rule.create!(
rule_type: "network",
action: "deny",
network_range: range2,
waf_policy: policy,
user: @user,
enabled: false # Disabled rule
)
stats = WafPolicyMatcher.policy_effectiveness_stats(policy, days: 30)
assert_equal policy.name, stats[:policy_name]
assert_equal "country", stats[:policy_type]
assert_equal "deny", stats[:action]
assert_equal 2, stats[:rules_generated]
assert_equal 1, stats[:active_rules] # Only enabled rules
assert_equal 2, stats[:networks_protected]
assert_equal 30, stats[:period_days]
assert_equal 2.0 / 30, stats[:generation_rate]
end
# Network Intelligence Matching
test "matches country policies based on network range country" do
range_with_country = NetworkRange.create!(network: "192.168.1.0/24", country: "BR")
range_without_country = NetworkRange.create!(network: "192.168.2.0/24")
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
matcher1 = WafPolicyMatcher.new(network_range: range_with_country)
matcher2 = WafPolicyMatcher.new(network_range: range_without_country)
matching1 = matcher1.find_matching_policies
matching2 = matcher2.find_matching_policies
assert_includes matching1, brazil_policy
assert_not_includes matching2, brazil_policy
end
test "matches network type policies based on intelligence flags" do
dc_range = NetworkRange.create!(network: "192.168.1.0/24", is_datacenter: true)
proxy_range = NetworkRange.create!(network: "192.168.2.0/24", is_proxy: true)
standard_range = NetworkRange.create!(network: "192.168.3.0/24") # All flags false
dc_policy = WafPolicy.create_network_type_policy(['datacenter'], 'deny', @user, "Block Datacenter")
proxy_policy = WafPolicy.create_network_type_policy(['proxy'], 'deny', @user, "Block Proxy")
standard_policy = WafPolicy.create_network_type_policy(['standard'], 'deny', @user, "Block Standard")
dc_matcher = WafPolicyMatcher.new(network_range: dc_range)
proxy_matcher = WafPolicyMatcher.new(network_range: proxy_range)
standard_matcher = WafPolicyMatcher.new(network_range: standard_range)
assert_includes dc_matcher.find_matching_policies, dc_policy
assert_includes proxy_matcher.find_matching_policies, proxy_policy
assert_includes standard_matcher.find_matching_policies, standard_policy
end
# Inheritance Support
test "matches policies based on inherited intelligence" do
# Create parent network with intelligence
parent = NetworkRange.create!(
network: "192.168.0.0/16",
country: "BR",
company: "Test Corp"
)
# Create child network without its own intelligence
child = NetworkRange.create!(network: "192.168.1.0/24")
brazil_policy = WafPolicy.create_country_policy(['BR'], 'deny', @user, "Block Brazil")
company_policy = WafPolicy.create_company_policy(['Test Corp'], 'challenge', @user, "Challenge Corp")
matcher = WafPolicyMatcher.new(network_range: child)
matching_policies = matcher.find_matching_policies
# Should match based on inherited intelligence
assert_includes matching_policies, brazil_policy
assert_includes matching_policies, company_policy
end
# Performance and Edge Cases
test "handles large numbers of policies efficiently" do
# Create many policies
policies = []
100.times do |i|
policies << WafPolicy.create_country_policy(
["US"], "deny", @user, "Policy #{i}"
)
end
# Only one should match (our network is BR, not US)
matching_policies = @matcher.find_matching_policies
assert_equal 0, matching_policies.length
end
test "handles policies with complex additional_data" do
redirect_policy = WafPolicy.create!(
name: "Complex Redirect",
policy_type: "country",
targets: ["BR"],
policy_action: "redirect",
user: @user,
additional_data: {
"redirect_url" => "https://example.com/blocked",
"redirect_status" => 301,
"custom_headers" => {
"X-Block-Reason" => "Country blocked"
}
}
)
rule = redirect_policy.create_rule_for_network_range(@network_range)
assert_not_nil rule
assert_equal "redirect", rule.action
assert rule.metadata['redirect_url'].present?
end
test "handles company name case-insensitive matching" do
range = NetworkRange.create!(
network: "192.168.1.0/24",
company: "Google LLC"
)
# Policies with different case variations
policy1 = WafPolicy.create_company_policy(['google'], 'deny', @user, "Block Google")
policy2 = WafPolicy.create_company_policy(['GOOGLE LLC'], 'deny', @user, "Block Google LLC")
policy3 = WafPolicy.create_company_policy(['Microsoft'], 'deny', @user, "Block Microsoft")
matcher = WafPolicyMatcher.new(network_range: range)
matching_policies = matcher.find_matching_policies
assert_includes matching_policies, policy1
assert_includes matching_policies, policy2
assert_not_includes matching_policies, policy3
end
test "handles partial company name matching" do
range = NetworkRange.create!(
network: "192.168.1.0/24",
company: "Amazon Web Services"
)
# Policy with partial match
policy = WafPolicy.create_company_policy(['Amazon'], 'deny', @user, "Block Amazon")
matcher = WafPolicyMatcher.new(network_range: range)
matching_policies = matcher.find_matching_policies
assert_includes matching_policies, policy
end
end