# frozen_string_literal: true require "test_helper" class EventTest < ActiveSupport::TestCase def setup @sample_payload = { "event_id" => "test-event-123", "timestamp" => Time.now.iso8601, "request" => { "ip" => "192.168.1.1", "method" => "GET", "path" => "/api/test", "headers" => { "host" => "example.com", "user-agent" => "TestAgent/1.0", "content-type" => "application/json" }, "query" => { "param" => "value" } }, "response" => { "status_code" => 200, "duration_ms" => 150, "size" => 1024 }, "waf_action" => "allow", "server_name" => "test-server", "environment" => "test", "geo" => { "country_code" => "US", "city" => "Test City" }, "tags" => { "source" => "test" }, "agent" => { "name" => "baffle-agent", "version" => "1.0.0" } } end def teardown Event.delete_all # Delete events first to avoid foreign key constraints end test "create_from_waf_payload! creates event with proper enum values" do event = Event.create_from_waf_payload!("test-123", @sample_payload) assert event.persisted? assert_equal "test-123", event.event_id assert_equal "192.168.1.1", event.ip_address assert_equal "/api/test", event.request_path assert_equal 200, event.response_status assert_equal 150, event.response_time_ms assert_equal "test-server", event.server_name assert_equal "test", event.environment assert_equal "US", event.country_code assert_equal "Test City", event.city assert_equal "baffle-agent", event.agent_name assert_equal "1.0.0", event.agent_version end test "create_from_waf_payload! properly normalizes request_method enum" do test_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] expected_enums = [:get, :post, :put, :patch, :delete, :head, :options] test_methods.each_with_index do |method, index| payload = @sample_payload.dup payload["request"]["method"] = method payload["event_id"] = "test-method-#{method.downcase}" event = Event.create_from_waf_payload!("test-method-#{method.downcase}", payload) assert_equal expected_enums[index].to_s, event.request_method, "Method #{method} should map to enum #{expected_enums[index]}" assert_equal index, event.request_method_before_type_cast, "Method #{method} should be stored as integer #{index}" end end test "create_from_waf_payload! properly normalizes waf_action enum" do test_actions = [ ["allow", :allow, 0], ["pass", :allow, 0], ["deny", :deny, 1], ["block", :deny, 1], ["redirect", :redirect, 2], ["challenge", :challenge, 3], ["unknown", :allow, 0] # Default fallback ] test_actions.each do |action, expected_enum, expected_int| payload = @sample_payload.dup payload["waf_action"] = action payload["event_id"] = "test-action-#{action}" event = Event.create_from_waf_payload!("test-action-#{action}", payload) assert_equal expected_enum.to_s, event.waf_action, "Action #{action} should map to enum #{expected_enum}" assert_equal expected_int, event.waf_action_before_type_cast, "Action #{action} should be stored as integer #{expected_int}" end end test "create_from_waf_payload! handles header case normalization" do payload = @sample_payload.dup payload["request"]["headers"] = { "HOST" => "EXAMPLE.COM", "User-Agent" => "TestAgent/1.0", "CONTENT-TYPE" => "application/json" } event = Event.create_from_waf_payload!("test-headers", payload) assert_equal "TestAgent/1.0", event.user_agent # The normalize_payload_headers method should normalize header keys to lowercase # but keep values as-is assert_equal "EXAMPLE.COM", event.headers["host"] assert_equal "application/json", event.headers["content-type"] end test "enum values persist after save and reload" do event = Event.create_from_waf_payload!("test-persist", @sample_payload) # Verify initial values assert_equal "get", event.request_method assert_equal "allow", event.waf_action assert_equal 0, event.request_method_before_type_cast assert_equal 0, event.waf_action_before_type_cast # Reload from database event.reload # Values should still be correct assert_equal "get", event.request_method assert_equal "allow", event.waf_action assert_equal 0, event.request_method_before_type_cast assert_equal 0, event.waf_action_before_type_cast end test "enum scopes work correctly" do # Create events with different methods and actions using completely separate payloads # Event 1: GET + allow Event.create_from_waf_payload!("get-allow", { "event_id" => "get-allow", "timestamp" => Time.now.iso8601, "request" => { "ip" => "192.168.1.1", "method" => "GET", "path" => "/test", "headers" => { "host" => "example.com" } }, "response" => { "status_code" => 200 }, "waf_action" => "allow" }, @project) # Event 2: POST + allow Event.create_from_waf_payload!("post-allow", { "event_id" => "post-allow", "timestamp" => Time.now.iso8601, "request" => { "ip" => "192.168.1.1", "method" => "POST", "path" => "/test", "headers" => { "host" => "example.com" } }, "response" => { "status_code" => 200 }, "waf_action" => "allow" }, @project) # Event 3: GET + deny Event.create_from_waf_payload!("get-deny", { "event_id" => "get-deny", "timestamp" => Time.now.iso8601, "request" => { "ip" => "192.168.1.1", "method" => "GET", "path" => "/test", "headers" => { "host" => "example.com" } }, "response" => { "status_code" => 200 }, "waf_action" => "deny" }, @project) # Test method scopes - use string values for enum queries get_events = Event.where(request_method: "get") post_events = Event.where(request_method: "post") assert_equal 2, get_events.count assert_equal 1, post_events.count # Test action scopes - use string values for enum queries allowed_events = Event.where(waf_action: "allow") denied_events = Event.where(waf_action: "deny") assert_equal 2, allowed_events.count assert_equal 1, denied_events.count end test "event normalization is triggered when needed" do # Create event without enum values (simulating old data) event = Event.create!( project: @project, event_id: "normalization-test", timestamp: Time.current, payload: @sample_payload, ip_address: "192.168.1.1", request_path: "/test", # Don't set request_method or waf_action to trigger normalization request_method: nil, waf_action: nil ) # Manually set the raw values that would normally be extracted event.instance_variable_set(:@raw_request_method, "POST") event.instance_variable_set(:@raw_action, "deny") # Trigger normalization event.send(:normalize_event_fields) event.save! # Verify normalization worked event.reload assert_equal "post", event.request_method assert_equal "deny", event.waf_action assert_equal 1, event.request_method_before_type_cast # POST = 1 assert_equal 1, event.waf_action_before_type_cast # DENY = 1 end test "payload extraction methods work correctly" do event = Event.create_from_waf_payload!("extraction-test", @sample_payload) # Test request_details request_details = event.request_details assert_equal "192.168.1.1", request_details[:ip] assert_equal "GET", request_details[:method] assert_equal "/api/test", request_details[:path] assert_equal "example.com", request_details[:headers]["host"] # Test response_details response_details = event.response_details assert_equal 200, response_details[:status_code] assert_equal 150, response_details[:duration_ms] assert_equal 1024, response_details[:size] # Test geo_details geo_details = event.geo_details assert_equal "US", geo_details["country_code"] assert_equal "Test City", geo_details["city"] # Test tags tags = event.tags assert_equal "test", tags["source"] end test "helper methods work correctly" do event = Event.create_from_waf_payload!("helper-test", @sample_payload) # Test boolean methods assert event.allowed? assert_not event.blocked? assert_not event.rate_limited? assert_not event.challenged? assert_not event.rule_matched? # Test path methods assert_equal ["api", "test"], event.path_segments assert_equal 2, event.path_depth end test "timestamp parsing works with various formats" do timestamps = [ Time.now.iso8601, (Time.now.to_f * 1000).to_i, # Unix timestamp in milliseconds Time.now.utc # Time object ] timestamps.each_with_index do |timestamp, index| payload = @sample_payload.dup payload["timestamp"] = timestamp payload["event_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" assert_not event.timestamp.nil? end end test "handles missing optional fields gracefully" do minimal_payload = { "event_id" => "minimal-test", "timestamp" => Time.now.iso8601, "request" => { "ip" => "10.0.0.1", "method" => "GET", "path" => "/simple" }, "response" => { "status_code" => 404 } } event = Event.create_from_waf_payload!("minimal-test", minimal_payload) assert event.persisted? assert_equal "10.0.0.1", event.ip_address assert_equal "get", event.request_method assert_equal "/simple", event.request_path assert_equal 404, event.response_status # Optional fields should be nil assert_nil event.user_agent assert_nil event.response_time_ms assert_nil event.country_code assert_nil event.city assert_nil event.agent_name assert_nil event.agent_version end end