require "test_helper" class NetworkRangeTest < ActiveSupport::TestCase setup do @ipv4_range = NetworkRange.new(network: "192.168.1.0/24") @ipv6_range = NetworkRange.new(network: "2001:db8::/32") @user = users(:jason) end # Validations test "should be valid with network address" do assert @ipv4_range.valid? assert @ipv6_range.valid? end test "should not be valid without network" do @ipv4_range.network = nil assert_not @ipv4_range.valid? assert_includes @ipv4_range.errors[:network], "can't be blank" end test "should validate network uniqueness" do @ipv4_range.save! duplicate = NetworkRange.new(network: "192.168.1.0/24") assert_not duplicate.valid? assert_includes duplicate.errors[:network], "has already been taken" end test "should validate source inclusion" do valid_sources = %w[api_imported user_created manual auto_generated inherited geolite_asn geolite_country] valid_sources.each do |source| @ipv4_range.source = source assert @ipv4_range.valid?, "Source #{source} should be valid" end @ipv4_range.source = "invalid_source" assert_not @ipv4_range.valid? assert_includes @ipv4_range.errors[:source], "is not included in the list" end test "should validate ASN numericality" do @ipv4_range.asn = 12345 assert @ipv4_range.valid? @ipv4_range.asn = 0 assert_not @ipv4_range.valid? assert_includes @ipv4_range.errors[:asn], "must be greater than 0" @ipv4_range.asn = -1 assert_not @ipv4_range.valid? assert_includes @ipv4_range.errors[:asn], "must be greater than 0" @ipv4_range.asn = "not_a_number" assert_not @ipv4_range.valid? @ipv4_range.asn = nil assert @ipv4_range.valid? end # Callbacks test "should set default source before validation" do range = NetworkRange.new(network: "10.0.0.0/8") range.valid? assert_equal "api_imported", range.source end test "should not override existing source" do range = NetworkRange.new(network: "10.0.0.0/8", source: "user_created") range.valid? assert_equal "user_created", range.source end # Virtual Attributes (CIDR) test "cidr getter returns network as string" do @ipv4_range.save! assert_equal "192.168.1.0/24", @ipv4_range.cidr assert_equal "192.168.1.0/24", @ipv4_range.network.to_s end test "cidr setter sets network from string" do range = NetworkRange.new range.cidr = "10.0.0.0/16" assert_equal "10.0.0.0/16", range.network.to_s end # Network Properties test "prefix_length returns correct network prefix" do @ipv4_range.save! @ipv6_range.save! assert_equal 24, @ipv4_range.prefix_length assert_equal 32, @ipv6_range.prefix_length end test "network_address returns network address" do @ipv4_range.save! assert_equal "192.168.1.0", @ipv4_range.network_address end test "broadcast_address returns correct broadcast for IPv4" do @ipv4_range.save! assert_equal "192.168.1.255", @ipv4_range.broadcast_address end test "broadcast_address returns nil for IPv6" do @ipv6_range.save! assert_nil @ipv6_range.broadcast_address end test "family detection works correctly" do @ipv4_range.save! @ipv6_range.save! assert_equal 4, @ipv4_range.family assert_equal 6, @ipv6_range.family end test "ipv4? and ipv6? predicate methods work" do @ipv4_range.save! @ipv6_range.save! assert @ipv4_range.ipv4? assert_not @ipv4_range.ipv6? assert @ipv6_range.ipv6? assert_not @ipv6_range.ipv4? end test "virtual? works correctly" do range = NetworkRange.new(network: "10.0.0.0/8") assert range.virtual? range.save! assert_not range.virtual? end # Network Containment test "contains_ip? works correctly" do @ipv4_range.save! assert @ipv4_range.contains_ip?("192.168.1.1") assert @ipv4_range.contains_ip?("192.168.1.254") assert_not @ipv4_range.contains_ip?("192.168.2.1") assert_not @ipv4_range.contains_ip?("2001:db8::1") # Test IPv6 @ipv6_range.save! assert @ipv6_range.contains_ip?("2001:db8::1") assert @ipv6_range.contains_ip?("2001:db8:ffff::ffff") assert_not @ipv6_range.contains_ip?("2001:db9::1") end test "contains_network? works correctly" do @ipv4_range.save! # More specific network assert @ipv4_range.contains_network?("192.168.1.0/25") assert @ipv4_range.contains_network?("192.168.1.128/25") # Same network assert @ipv4_range.contains_network?("192.168.1.0/24") # Less specific network assert_not @ipv4_range.contains_network?("192.168.0.0/16") # Different network assert_not @ipv4_range.contains_network?("10.0.0.0/8") end test "overlaps? works correctly" do @ipv4_range.save! # Overlapping networks assert @ipv4_range.overlaps?("192.168.1.0/25") # More specific assert @ipv4_range.overlaps?("192.168.0.0/23") # Less specific assert @ipv4_range.overlaps?("192.168.1.128/25") # Partial overlap # Non-overlapping assert_not @ipv4_range.overlaps?("10.0.0.0/8") assert_not @ipv4_range.overlaps?("172.16.0.0/12") end # Parent/Child Relationships test "parent_ranges finds containing networks" do # Create parent and child networks parent = NetworkRange.create!(network: "192.168.0.0/16") @ipv4_range.save! # 192.168.1.0/24 child = NetworkRange.create!(network: "192.168.1.0/25") parents = @ipv4_range.parent_ranges assert_includes parents, parent assert_not_includes parents, child assert_not_includes parents, @ipv4_range # Should be ordered by specificity (more specific first) assert_equal parent, parents.first end test "child_ranges finds contained networks" do # Create parent and child networks parent = NetworkRange.create!(network: "192.168.0.0/16") @ipv4_range.save! # 192.168.1.0/24 child = NetworkRange.create!(network: "192.168.1.0/25") children = parent.child_ranges assert_includes children, @ipv4_range assert_includes children, child assert_not_includes children, parent # Should be ordered by specificity (less specific first) assert_equal @ipv4_range, children.first end test "child_ranges works with Apple network hierarchy - 17.240.0.0/14" do # This test demonstrates the current bug in child_ranges method # Expected: 17.240.0.0/14 should have parents but no children in this test setup # Create the target network target_network = NetworkRange.create!(network: "17.240.0.0/14", source: "manual") # Create parent networks parent1 = NetworkRange.create!(network: "17.240.0.0/13", source: "manual") # Should contain 17.240.0.0/14 parent2 = NetworkRange.create!(network: "17.128.0.0/9", source: "manual") # Should also contain 17.240.0.0/14 # Create some child networks (more specific networks contained by 17.240.0.0/14) child1 = NetworkRange.create!(network: "17.240.0.0/15", source: "manual") # First half of /14 child2 = NetworkRange.create!(network: "17.242.0.0/15", source: "manual") # Second half of /14 child3 = NetworkRange.create!(network: "17.240.0.0/16", source: "manual") # More specific child4 = NetworkRange.create!(network: "17.241.0.0/16", source: "manual") # More specific # Test parent_ranges works correctly parents = target_network.parent_ranges assert_includes parents, parent1, "17.240.0.0/13 should be a parent of 17.240.0.0/14" assert_includes parents, parent2, "17.128.0.0/9 should be a parent of 17.240.0.0/14" # Test child_ranges - this is currently failing due to the bug children = target_network.child_ranges assert_includes children, child1, "17.240.0.0/15 should be a child of 17.240.0.0/14" assert_includes children, child2, "17.242.0.0/15 should be a child of 17.240.0.0/14" assert_includes children, child3, "17.240.0.0/16 should be a child of 17.240.0.0/14" assert_includes children, child4, "17.241.0.0/16 should be a child of 17.240.0.0/14" assert_not_includes children, parent1, "Parent networks should not be in child_ranges" assert_not_includes children, parent2, "Parent networks should not be in child_ranges" assert_not_includes children, target_network, "Self should not be in child_ranges" # Test that parent can find child in its child_ranges parent1_children = parent1.child_ranges assert_includes parent1_children, target_network, "17.240.0.0/14 should be in child_ranges of 17.240.0.0/13" parent2_children = parent2.child_ranges assert_includes parent2_children, target_network, "17.240.0.0/14 should be in child_ranges of 17.128.0.0/9" # Test bidirectional consistency assert target_network.parent_ranges.include?(parent1), "Parent should list child" assert parent1.child_ranges.include?(target_network), "Child should list parent" assert target_network.parent_ranges.include?(parent2), "Parent should list child" assert parent2.child_ranges.include?(target_network), "Child should list parent" end # Intelligence and Inheritance test "has_intelligence? detects available intelligence data" do range = NetworkRange.new(network: "10.0.0.0/8") assert_not range.has_intelligence? range.asn = 12345 assert range.has_intelligence? range.asn = nil range.company = "Test Company" assert range.has_intelligence? range.company = nil range.country = "US" assert range.has_intelligence? range.country = nil range.is_datacenter = true assert range.has_intelligence? end test "own_intelligence returns correct data structure" do range = NetworkRange.create!( network: "10.0.0.0/8", asn: 12345, asn_org: "Test ASN", company: "Test Company", country: "US", is_datacenter: true, is_proxy: false, is_vpn: false, source: "manual" ) intelligence = range.own_intelligence assert_equal 12345, intelligence[:asn] assert_equal "Test ASN", intelligence[:asn_org] assert_equal "Test Company", intelligence[:company] assert_equal "US", intelligence[:country] assert_equal true, intelligence[:is_datacenter] assert_equal false, intelligence[:is_proxy] assert_equal false, intelligence[:is_vpn] assert_equal false, intelligence[:inherited] assert_equal "manual", intelligence[:source] end test "inherited_intelligence returns own data when available" do child = NetworkRange.create!( network: "192.168.1.0/24", country: "US", company: "Test Company" ) intelligence = child.inherited_intelligence assert_equal "US", intelligence[:country] assert_equal "Test Company", intelligence[:company] assert_equal false, intelligence[:inherited] end test "inherited_intelligence inherits from parent when needed" do parent = NetworkRange.create!( network: "192.168.0.0/16", country: "US", company: "Test Company" ) child = NetworkRange.create!(network: "192.168.1.0/24") intelligence = child.inherited_intelligence assert_equal "US", intelligence[:country] assert_equal "Test Company", intelligence[:company] assert_equal true, intelligence[:inherited] assert_equal parent.cidr, intelligence[:parent_cidr] end test "parent_with_intelligence finds nearest parent with data" do grandparent = NetworkRange.create!(network: "10.0.0.0/8", country: "US") parent = NetworkRange.create!(network: "10.1.0.0/16") child = NetworkRange.create!(network: "10.1.1.0/24") found_parent = child.parent_with_intelligence assert_equal grandparent, found_parent assert_not_equal parent, found_parent end # API Data Management test "is_fetching_api_data? tracks active fetches" do range = NetworkRange.create!(network: "10.0.0.0/8") assert_not range.is_fetching_api_data?(:ipapi) range.mark_as_fetching_api_data!(:ipapi) assert range.is_fetching_api_data?(:ipapi) range.clear_fetching_status!(:ipapi) assert_not range.is_fetching_api_data?(:ipapi) end test "should_fetch_api_data? prevents duplicate fetches" do range = NetworkRange.create!(network: "10.0.0.0/8") # Should fetch initially assert range.should_fetch_api_data?(:ipapi) # Should not fetch while fetching range.mark_as_fetching_api_data!(:ipapi) assert_not range.should_fetch_api_data?(:ipapi) # Should fetch again after clearing range.clear_fetching_status!(:ipapi) assert range.should_fetch_api_data?(:ipapi) end test "has_ipapi_data_available? checks inheritance" do parent = NetworkRange.create!(network: "10.0.0.0/8") parent.set_network_data(:ipapi, { country: "US" }) parent.save! child = NetworkRange.create!(network: "10.0.1.0/24") assert child.has_ipapi_data_available? end test "should_fetch_ipapi_data? respects parent fetching status" do parent = NetworkRange.create!(network: "10.0.0.0/8") child = NetworkRange.create!(network: "10.0.1.0/24") parent.mark_as_fetching_api_data!(:ipapi) assert_not child.should_fetch_ipapi_data? parent.clear_fetching_status!(:ipapi) assert child.should_fetch_ipapi_data? end # Network Data Management test "network_data_for and set_network_data work correctly" do range = NetworkRange.create!(network: "10.0.0.0/8") assert_equal({}, range.network_data_for(:ipapi)) data = { country: "US", city: "New York" } range.set_network_data(:ipapi, data) range.save! assert_equal data, range.network_data_for(:ipapi) end test "has_network_data_from? checks data presence" do range = NetworkRange.create!(network: "10.0.0.0/8") assert_not range.has_network_data_from?(:ipapi) range.set_network_data(:ipapi, { country: "US" }) range.save! assert range.has_network_data_from?(:ipapi) end # IPAPI Tracking Methods test "find_or_create_tracking_network_for_ip works correctly" do # IPv4 - should create /24 tracking_range = NetworkRange.find_or_create_tracking_network_for_ip("192.168.1.100") assert_equal "192.168.1.0/24", tracking_range.network.to_s assert_equal "auto_generated", tracking_range.source assert_equal "IPAPI tracking network", tracking_range.creation_reason # IPv6 - should create /64 ipv6_tracking = NetworkRange.find_or_create_tracking_network_for_ip("2001:db8::1") assert_equal "2001:db8::/64", ipv6_tracking.network.to_s end test "should_fetch_ipapi_for_ip? works correctly" do tracking_range = NetworkRange.create!(network: "192.168.1.0/8") # Should fetch initially assert NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100") # Mark as queried recently tracking_range.mark_ipapi_queried!("192.168.1.0/24") assert_not NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100") # Should fetch for old queries tracking_range.network_data['ipapi_queried_at'] = 2.years.ago.to_i tracking_range.save! assert NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100") end test "mark_ipapi_queried! stores query metadata" do range = NetworkRange.create!(network: "192.168.1.0/24") range.mark_ipapi_queried!("192.168.1.128/25") assert range.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i assert_equal "192.168.1.128/25", range.network_data['ipapi_returned_cidr'] end # JSON Helper Methods test "abuser_scores_hash handles JSON correctly" do range = NetworkRange.create!(network: "10.0.0.0/8") assert_equal({}, range.abuser_scores_hash) range.abuser_scores_hash = { ipquality: 85, abuseipdb: 92 } range.save! assert_equal({ "ipquality" => 85, "abuseipdb" => 92 }, JSON.parse(range.abuser_scores)) assert_equal({ ipquality: 85, abuseipdb: 92 }, range.abuser_scores_hash) end test "additional_data_hash handles JSON correctly" do range = NetworkRange.create!(network: "10.0.0.0/8") assert_equal({}, range.additional_data_hash) range.additional_data_hash = { tags: ["malicious", "botnet"], notes: "Test data" } range.save! parsed_data = JSON.parse(range.additional_data) assert_equal ["malicious", "botnet"], parsed_data["tags"] assert_equal "Test data", parsed_data["notes"] end # Scopes test "ipv4 and ipv6 scopes work correctly" do ipv4_range = NetworkRange.create!(network: "192.168.1.0/24") ipv6_range = NetworkRange.create!(network: "2001:db8::/32") assert_includes NetworkRange.ipv4, ipv4_range assert_not_includes NetworkRange.ipv4, ipv6_range assert_includes NetworkRange.ipv6, ipv6_range assert_not_includes NetworkRange.ipv6, ipv4_range end test "filtering scopes work correctly" do range1 = NetworkRange.create!(network: "192.168.1.0/24", country: "US", company: "Google", asn: 15169, is_datacenter: true) range2 = NetworkRange.create!(network: "10.0.0.0/8", country: "BR", company: "Amazon", asn: 16509, is_proxy: true) assert_includes NetworkRange.by_country("US"), range1 assert_not_includes NetworkRange.by_country("US"), range2 assert_includes NetworkRange.by_company("Google"), range1 assert_not_includes NetworkRange.by_company("Google"), range2 assert_includes NetworkRange.by_asn(15169), range1 assert_not_includes NetworkRange.by_asn(15169), range2 assert_includes NetworkRange.datacenter, range1 assert_not_includes NetworkRange.datacenter, range2 assert_includes NetworkRange.proxy, range2 assert_not_includes NetworkRange.proxy, range1 end # Class Methods test "contains_ip class method finds most specific network" do parent = NetworkRange.create!(network: "192.168.0.0/16") child = NetworkRange.create!(network: "192.168.1.0/24") found = NetworkRange.contains_ip("192.168.1.100") assert_equal child, found.first # More specific should come first end test "overlapping class method finds overlapping networks" do range1 = NetworkRange.create!(network: "192.168.0.0/16") range2 = NetworkRange.create!(network: "192.168.1.0/24") range3 = NetworkRange.create!(network: "10.0.0.0/8") overlapping = NetworkRange.overlapping("192.168.1.0/24") assert_includes overlapping, range1 assert_includes overlapping, range2 assert_not_includes overlapping, range3 end test "find_or_create_by_cidr works correctly" do # Creates new record range = NetworkRange.find_or_create_by_cidr("10.0.0.0/8", user: @user, source: "manual") assert range.persisted? assert_equal "10.0.0.0/8", range.network.to_s assert_equal @user, range.user assert_equal "manual", range.source # Returns existing record existing = NetworkRange.find_or_create_by_cidr("10.0.0.0/8") assert_equal range.id, existing.id end test "find_by_ip_or_network handles both IP and network inputs" do range = NetworkRange.create!(network: "192.168.1.0/24") # Find by IP within range found_by_ip = NetworkRange.find_by_ip_or_network("192.168.1.100") assert_includes found_by_ip, range # Find by exact network found_by_network = NetworkRange.find_by_ip_or_network("192.168.1.0/24") assert_includes found_by_network, range # Return none for invalid input assert_equal NetworkRange.none, NetworkRange.find_by_ip_or_network("") assert_equal NetworkRange.none, NetworkRange.find_by_ip_or_network("invalid") end # Analytics Methods test "has_events? correctly detects if network has events" do range = NetworkRange.create!(network: "192.168.1.0/24") assert_equal false, range.has_events? # Create a test event in this network Event.create!( request_id: "test-1", ip_address: "192.168.1.100", network_range: range, waf_action: 1, request_method: 0, response_status: 200 ) # Should now detect events exist assert_equal true, range.has_events? end test "events method finds events within range" do range = NetworkRange.create!(network: "192.168.1.0/24") # Create test events matching_event = Event.create!( request_id: "test-1", timestamp: Time.current, payload: {}, ip_address: "192.168.1.100" ) non_matching_event = Event.create!( request_id: "test-2", timestamp: Time.current, payload: {}, ip_address: "10.0.0.1" ) found_events = range.events assert_includes found_events, matching_event assert_not_includes found_events, non_matching_event end test "blocking_rules and active_rules work correctly" do range = NetworkRange.create!(network: "192.168.1.0/24") blocking_rule = Rule.create!( rule_type: "network", action: "deny", network_range: range, user: @user, enabled: true ) allow_rule = Rule.create!( rule_type: "network", action: "allow", network_range: range, user: @user, enabled: true ) disabled_rule = Rule.create!( rule_type: "network", action: "deny", network_range: range, user: @user, enabled: false ) assert_includes range.blocking_rules, blocking_rule assert_not_includes range.blocking_rules, allow_rule assert_not_includes range.blocking_rules, disabled_rule assert_includes range.active_rules, blocking_rule assert_includes range.active_rules, allow_rule assert_not_includes range.active_rules, disabled_rule end # Policy Evaluation test "needs_policy_evaluation? works correctly" do range = NetworkRange.create!(network: "192.168.1.0/24") # Should evaluate if never evaluated assert range.needs_policy_evaluation? # Should evaluate if policies updated since last evaluation range.update!(policies_evaluated_at: 1.hour.ago) WafPolicy.create!(name: "Test Policy", policy_type: "country", targets: ["US"], policy_action: "deny", user: @user) assert range.needs_policy_evaluation? # Should not evaluate if up to date range.update!(policies_evaluated_at: 5.minutes.ago) assert_not range.needs_policy_evaluation? end # String Representations test "to_s returns cidr" do @ipv4_range.save! assert_equal @ipv4_range.cidr, @ipv4_range.to_s end test "to_param parameterizes cidr" do @ipv4_range.save! assert_equal "192.168.1.0_24", @ipv4_range.to_param end # Geographic Lookup test "geo_lookup_country! updates country when available" do range = NetworkRange.create!(network: "8.8.8.0/24") # Google's network # Mock GeoIpService GeoIpService.expects(:lookup_country).with("8.8.8.0").returns("US") range.geo_lookup_country! assert_equal "US", range.reload.country end test "geo_lookup_country! handles errors gracefully" do range = NetworkRange.create!(network: "192.168.1.0/24") # Mock GeoIpService to raise error GeoIpService.expects(:lookup_country).with("192.168.1.0").raises(StandardError.new("Service error")) # Should not raise error but log it assert_nothing_raised do range.geo_lookup_country! end assert_nil range.reload.country end # Stats Methods test "import_stats_by_source returns statistics" do NetworkRange.create!(network: "10.0.0.0/8", source: "manual") NetworkRange.create!(network: "192.168.1.0/24", source: "api_imported") NetworkRange.create!(network: "172.16.0.0/12", source: "api_imported") stats = NetworkRange.import_stats_by_source assert_equal 2, stats.count api_stats = stats.find { |s| s.source == "api_imported" } assert_equal 2, api_stats.count end test "geolite_coverage_stats returns detailed coverage information" do NetworkRange.create!(network: "10.0.0.0/8", source: "geolite_asn", asn: 12345) NetworkRange.create!(network: "192.168.1.0/24", source: "geolite_country", country: "US") NetworkRange.create!(network: "172.16.0.0/12", source: "geolite_asn", country: "BR") stats = NetworkRange.geolite_coverage_stats assert_equal 3, stats[:total_networks] assert_equal 2, stats[:asn_networks] assert_equal 1, stats[:country_networks] assert_equal 2, stats[:with_asn_data] assert_equal 1, stats[:with_country_data] assert_equal 2, stats[:unique_countries] assert_equal 2, stats[:unique_asns] end end