Many updates
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
387
test/jobs/fetch_ipapi_data_job_test.rb
Normal file
387
test/jobs/fetch_ipapi_data_job_test.rb
Normal 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
|
||||
@@ -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",
|
||||
|
||||
363
test/jobs/process_waf_event_job_test.rb
Normal file
363
test/jobs/process_waf_event_job_test.rb
Normal 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
|
||||
Reference in New Issue
Block a user