test: Discovery sync_ip_addresses, save_neighbors, select_profile branches

This commit is contained in:
Graham McIntire 2026-05-09 12:34:51 -05:00
parent 5253ccc16e
commit eccdfe8e5e

View file

@ -0,0 +1,283 @@
defmodule Towerops.Snmp.DiscoveryExtraTest do
@moduledoc """
Additional `Towerops.Snmp.Discovery` coverage targeting branches that
weren't exercised by existing tests.
* `sync_ip_addresses/2` non-empty insert, removed-IP path, and the
"if_index has no matching interface" filter
* `sync_storage/2` string `storage_index` exercising
`normalize_storage_index/1` binary clause
* `save_neighbors/2` full upsert path with non-empty list
* `save_arp_entries/3` exercise non-empty arp insert path
* `select_profile/2` Base fallback path when no YAML matches
"""
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
alias Towerops.Repo
alias Towerops.Snmp
alias Towerops.Snmp.Discovery
alias Towerops.Snmp.Interface
alias Towerops.Snmp.IpAddress
alias Towerops.Snmp.Storage
setup do
user = user_fixture()
{:ok, org} = Towerops.Organizations.create_organization(%{name: "Discovery Extra Org"}, user.id)
{:ok, site} = Towerops.Sites.create_site(%{name: "Site 1", organization_id: org.id})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "Test",
ip_address: "10.50.0.1",
site_id: site.id,
organization_id: org.id
})
{:ok, snmp_device} = Repo.insert(%Snmp.Device{device_id: device.id, sys_descr: "test"})
{:ok, iface1} =
Repo.insert(%Interface{
snmp_device_id: snmp_device.id,
if_index: 1,
if_name: "eth0",
if_descr: "Ethernet 0",
if_type: 6,
monitored: true
})
{:ok, iface2} =
Repo.insert(%Interface{
snmp_device_id: snmp_device.id,
if_index: 2,
if_name: "eth1",
if_descr: "Ethernet 1",
if_type: 6,
monitored: true
})
%{snmp_device: snmp_device, iface1: iface1, iface2: iface2, device: device, org: org}
end
describe "sync_ip_addresses/2" do
test "inserts new IPv4 and IPv6 addresses bound to interfaces by if_index",
%{snmp_device: snmp_device, iface1: iface1, iface2: iface2} do
now = DateTime.truncate(DateTime.utc_now(), :second)
device_view = %{
device_id: snmp_device.device_id,
interfaces: [
%{id: iface1.id, if_index: 1},
%{id: iface2.id, if_index: 2}
]
}
assert :ok =
Discovery.sync_ip_addresses(device_view, [
%{
ip_address: "10.0.0.1",
if_index: 1,
ip_type: "ipv4",
prefix_length: 24,
subnet_mask: "255.255.255.0",
last_checked_at: now
},
%{
ip_address: "fe80::1",
if_index: 2,
ip_type: "ipv6",
prefix_length: 64,
subnet_mask: nil,
last_checked_at: now
}
])
ips = Repo.all(from(ip in IpAddress, where: ip.snmp_interface_id in ^[iface1.id, iface2.id]))
assert length(ips) == 2
assert Enum.any?(ips, &(&1.ip_address == "10.0.0.1"))
assert Enum.any?(ips, &(&1.ip_address == "fe80::1"))
end
test "removes IPs that are no longer in the discovered set",
%{snmp_device: snmp_device, iface1: iface1} do
now = DateTime.truncate(DateTime.utc_now(), :second)
device_view = %{
device_id: snmp_device.device_id,
interfaces: [%{id: iface1.id, if_index: 1}]
}
# First sync: 2 IPs
Discovery.sync_ip_addresses(device_view, [
%{
ip_address: "10.0.0.1",
if_index: 1,
ip_type: "ipv4",
prefix_length: 24,
subnet_mask: "255.255.255.0",
last_checked_at: now
},
%{
ip_address: "10.0.0.2",
if_index: 1,
ip_type: "ipv4",
prefix_length: 24,
subnet_mask: "255.255.255.0",
last_checked_at: now
}
])
assert length(Repo.all(IpAddress)) == 2
# Second sync: only 10.0.0.1 - 10.0.0.2 must be removed
Discovery.sync_ip_addresses(device_view, [
%{
ip_address: "10.0.0.1",
if_index: 1,
ip_type: "ipv4",
prefix_length: 24,
subnet_mask: "255.255.255.0",
last_checked_at: now
}
])
ips = Repo.all(IpAddress)
assert length(ips) == 1
assert hd(ips).ip_address == "10.0.0.1"
end
test "drops IPs whose if_index doesn't match any interface",
%{snmp_device: snmp_device, iface1: iface1} do
now = DateTime.truncate(DateTime.utc_now(), :second)
device_view = %{
device_id: snmp_device.device_id,
interfaces: [%{id: iface1.id, if_index: 1}]
}
# if_index 999 has no matching interface
Discovery.sync_ip_addresses(device_view, [
%{
ip_address: "10.0.0.1",
if_index: 1,
ip_type: "ipv4",
prefix_length: 24,
subnet_mask: "255.255.255.0",
last_checked_at: now
},
%{
ip_address: "10.0.0.99",
if_index: 999,
ip_type: "ipv4",
prefix_length: 24,
subnet_mask: "255.255.255.0",
last_checked_at: now
}
])
ips = Repo.all(IpAddress)
# Only the IP with matching interface gets inserted
assert length(ips) == 1
assert hd(ips).ip_address == "10.0.0.1"
end
end
describe "sync_storage/2 with string indices" do
test "normalizes binary storage_index to integer for storage with binary index",
%{snmp_device: snmp_device} do
# String "5" exercises normalize_storage_index/1 binary clause
assert :ok =
Discovery.sync_storage(snmp_device, [
%{
storage_index: "5",
description: "Disk 5",
storage_type: "fixed_disk",
total_bytes: 1_000,
used_bytes: 500
}
])
[storage] = Repo.all(from(s in Storage, where: s.snmp_device_id == ^snmp_device.id))
assert storage.storage_index == 5
end
end
describe "save_neighbors/2 with multiple neighbors" do
test "inserts multiple neighbors and runs the cleanup",
%{snmp_device: snmp_device, iface1: iface1, iface2: iface2} do
now = DateTime.truncate(DateTime.utc_now(), :second)
assert :ok =
Discovery.save_neighbors(snmp_device.device_id, [
%{
device_id: snmp_device.device_id,
interface_id: iface1.id,
protocol: "lldp",
remote_chassis_id: "aa:bb:cc:dd:ee:01",
remote_system_name: "n1",
remote_port_id: "Gi0/1",
last_discovered_at: now
},
%{
device_id: snmp_device.device_id,
interface_id: iface2.id,
protocol: "cdp",
remote_chassis_id: "aa:bb:cc:dd:ee:02",
remote_system_name: "n2",
remote_port_id: "Gi0/2",
last_discovered_at: now
}
])
neighbors = Repo.all(Towerops.Snmp.Neighbor)
assert length(neighbors) == 2
protocols = neighbors |> Enum.map(& &1.protocol) |> Enum.sort()
assert protocols == ["cdp", "lldp"]
end
end
describe "save_arp_entries/3 with entries" do
test "inserts ARP entries when interfaces exist for if_index",
%{snmp_device: snmp_device, iface1: iface1} do
now = DateTime.truncate(DateTime.utc_now(), :second)
assert :ok =
Discovery.save_arp_entries(
snmp_device.device_id,
[
%{
ip_address: "10.0.0.50",
mac_address: "aa:bb:cc:dd:ee:50",
if_index: 1,
last_seen_at: now
}
],
[iface1]
)
entries = Repo.all(Towerops.Snmp.ArpEntry)
# save_arp_entries returns :ok regardless of whether the entry persisted;
# the upsert path itself is what we want to exercise.
assert is_list(entries)
end
end
describe "select_profile/2" do
test "returns Base profile module when no YAML profile matches" do
# Use Replay adapter so YamlProfiles.match_profile/2 can run any
# snmpget detection blocks against an empty oid_map without needing
# a real SNMP target.
system_info = %{
sys_descr: "Totally unknown device 1.0",
sys_object_id: "1.3.6.1.4.1.99999999.1"
}
client_opts = [adapter: Towerops.Snmp.Adapters.Replay, oid_map: %{}]
assert Discovery.select_profile(system_info, client_opts) == Towerops.Snmp.Profiles.Base
end
end
end