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