Files
baffle-hub/test/jobs/fetch_ipapi_data_job_test.rb
2025-11-13 14:42:43 +11:00

388 lines
13 KiB
Ruby

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