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 end