From 19d9ef9e5222f99fc3bdc3ccb88ce9a11d5aec81 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 9 May 2026 12:29:23 -0500 Subject: [PATCH] test: Profiles.Base storage/printer/processor/Cambium branches --- .../snmp/profiles/base_extra_test.exs | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 test/towerops/snmp/profiles/base_extra_test.exs diff --git a/test/towerops/snmp/profiles/base_extra_test.exs b/test/towerops/snmp/profiles/base_extra_test.exs new file mode 100644 index 00000000..03f5c30e --- /dev/null +++ b/test/towerops/snmp/profiles/base_extra_test.exs @@ -0,0 +1,277 @@ +defmodule Towerops.Snmp.Profiles.BaseExtraTest do + @moduledoc """ + Additional `Towerops.Snmp.Profiles.Base` coverage targeting branches that + remained uncovered after the existing test files. + + Covered here: + * `discover_ipv6_addresses/1` via the modern RFC 4293 ipAddressTable + (the prior tests only hit the IPV6-MIB fallback) + * `discover_transceivers/1` empty-result and walk-failure branches + * `discover_printer_supplies/1` empty / supplies parsing including + `parse_supply_capacity` for binary values + * `discover_processors/1` UCD-SNMP-MIB three-value branch + * `discover_storage/1` happy path covering disk types + * `discover_physical_entities/1` for entity_class string mapping + * `identify_device/1` Cambium ePMP and other Ubiquiti antenna branches + """ + + use Towerops.DataCase, async: true + + alias Towerops.Snmp.Adapters.Replay + alias Towerops.Snmp.Profiles.Base + + describe "discover_ipv6_addresses/1 via ipAddressTable (RFC 4293)" do + test "ipAddressTable with only IPv4 entries returns empty list (then falls through to IPV6-MIB)" do + # Modern table has IPv4 addresses (type 1), but discover_ipv6_addresses + # should return [] from the modern path, then fall through to IPV6-MIB + # which is also empty -> empty list. + ipv4_suffix = "1.4.10.0.0.1" + + client_opts = [ + adapter: Replay, + oid_map: %{ + ("1.3.6.1.2.1.4.34.1.3." <> ipv4_suffix) => "1" + } + ] + + assert {:ok, []} = Base.discover_ipv6_addresses(client_opts) + end + end + + describe "discover_transceivers/1 empty paths" do + test "returns empty list when no entity descriptions exist" do + assert {:ok, []} = Base.discover_transceivers(adapter: Replay, oid_map: %{}) + end + + test "filters out entities that lack transceiver keywords in description" do + client_opts = [ + adapter: Replay, + oid_map: %{ + # entPhysicalDescr that doesn't match transceiver keywords + "1.3.6.1.2.1.47.1.1.1.1.2.1" => "Power Module", + "1.3.6.1.2.1.47.1.1.1.1.5.1" => "9" + } + ] + + assert {:ok, []} = Base.discover_transceivers(client_opts) + end + end + + describe "discover_printer_supplies/1" do + test "returns empty list when no supplies present" do + assert {:ok, []} = Base.discover_printer_supplies(adapter: Replay, oid_map: %{}) + end + + test "discovers printer supplies with all attributes including capacity parsing" do + client_opts = [ + adapter: Replay, + oid_map: %{ + # prtMarkerSuppliesType: 3 = toner, 10 = drum + "1.3.6.1.2.1.43.11.1.1.3.1.1" => "3", + "1.3.6.1.2.1.43.11.1.1.3.1.2" => "10", + # prtMarkerSuppliesDescription + "1.3.6.1.2.1.43.11.1.1.4.1.1" => "Black Toner Cartridge", + "1.3.6.1.2.1.43.11.1.1.4.1.2" => "Imaging Drum", + # prtMarkerSuppliesUnit: 19=percent, 18=items + "1.3.6.1.2.1.43.11.1.1.7.1.1" => "19", + "1.3.6.1.2.1.43.11.1.1.7.1.2" => "18", + # prtMarkerSuppliesMaxCapacity (string -> tests parse_supply_capacity binary branch) + "1.3.6.1.2.1.43.11.1.1.8.1.1" => "100", + "1.3.6.1.2.1.43.11.1.1.8.1.2" => "30000", + # prtMarkerSuppliesLevel + "1.3.6.1.2.1.43.11.1.1.9.1.1" => "75", + "1.3.6.1.2.1.43.11.1.1.9.1.2" => "12500", + # prtMarkerColorantValue + "1.3.6.1.2.1.43.11.1.1.12.1.1" => "black", + "1.3.6.1.2.1.43.11.1.1.12.1.2" => "n/a" + } + ] + + assert {:ok, supplies} = Base.discover_printer_supplies(client_opts) + assert length(supplies) == 2 + + toner = Enum.find(supplies, &(&1.supply_index == "1")) + assert toner.supply_type == "toner" + assert toner.supply_description == "Black Toner Cartridge" + assert toner.supply_unit == "percent" + assert toner.max_capacity == 100 + assert toner.current_level == 75 + assert toner.color_name == "black" + + drum = Enum.find(supplies, &(&1.supply_index == "2")) + assert drum.supply_type == "drum" + assert drum.max_capacity == 30_000 + assert drum.current_level == 12_500 + end + + test "handles unknown supply type code by mapping to 'unknown'" do + client_opts = [ + adapter: Replay, + oid_map: %{ + # Unknown supply type code 999 + "1.3.6.1.2.1.43.11.1.1.3.1.1" => "999" + } + ] + + assert {:ok, [supply]} = Base.discover_printer_supplies(client_opts) + assert supply.supply_type == "unknown" + end + end + + describe "discover_processors/1 UCD-SNMP path" do + test "synthesizes a UCD CPU when other MIBs are silent" do + # This path exercises discover_ucd_cpu when both HR and Cisco return []. + # The Replay adapter with empty oid_map returns nil for UCD OIDs which + # parse as 0; we still get a synthetic processor. + assert {:ok, processors} = Base.discover_processors(adapter: Replay, oid_map: %{}) + assert length(processors) == 1 + [cpu] = processors + assert cpu.processor_type == "ucd_cpu" + assert cpu.processor_index == "ucd_0" + assert is_map(cpu.metadata) + end + end + + describe "discover_storage/1 (Replay)" do + test "discovers fixed disks while ignoring RAM/swap entries" do + client_opts = [ + adapter: Replay, + oid_map: %{ + # hrStorageType: RAM (1.3.6.1.2.1.25.2.1.2) - filtered out + "1.3.6.1.2.1.25.2.3.1.2.1" => "1.3.6.1.2.1.25.2.1.2", + # fixed disk (1.3.6.1.2.1.25.2.1.4) + "1.3.6.1.2.1.25.2.3.1.2.2" => "1.3.6.1.2.1.25.2.1.4", + # flash memory (1.3.6.1.2.1.25.2.1.9) + "1.3.6.1.2.1.25.2.3.1.2.3" => "1.3.6.1.2.1.25.2.1.9", + # Descriptions + "1.3.6.1.2.1.25.2.3.1.3.2" => "/var", + "1.3.6.1.2.1.25.2.3.1.3.3" => "/boot", + # Allocation units + "1.3.6.1.2.1.25.2.3.1.4.2" => "4096", + "1.3.6.1.2.1.25.2.3.1.4.3" => "1024", + # Sizes (in units) + "1.3.6.1.2.1.25.2.3.1.5.2" => "1000000", + "1.3.6.1.2.1.25.2.3.1.5.3" => "100000", + # Used (in units) + "1.3.6.1.2.1.25.2.3.1.6.2" => "500000", + "1.3.6.1.2.1.25.2.3.1.6.3" => "75000" + } + ] + + assert {:ok, storage} = Base.discover_storage(client_opts) + assert length(storage) == 2 + + fixed = Enum.find(storage, &(&1.storage_type == "fixed_disk")) + assert fixed.description == "/var" + assert fixed.total_bytes == 1_000_000 * 4096 + + flash = Enum.find(storage, &(&1.storage_type == "flash_memory")) + assert flash.description == "/boot" + assert flash.used_bytes == 75_000 * 1024 + end + + test "returns empty list when storage walk has no entries" do + assert {:ok, []} = Base.discover_storage(adapter: Replay, oid_map: %{}) + end + end + + describe "discover_physical_entities/1 entity class mapping" do + test "discovers entities with class strings mapped from class codes" do + client_opts = [ + adapter: Replay, + oid_map: %{ + # entPhysicalDescr + "1.3.6.1.2.1.47.1.1.1.1.2.1" => "Chassis", + "1.3.6.1.2.1.47.1.1.1.1.2.2" => "PSU", + "1.3.6.1.2.1.47.1.1.1.1.2.3" => "Fan Module", + "1.3.6.1.2.1.47.1.1.1.1.2.4" => "Sensor X", + "1.3.6.1.2.1.47.1.1.1.1.2.5" => "Linecard", + "1.3.6.1.2.1.47.1.1.1.1.2.6" => "Port 1", + # entPhysicalClass + # chassis + "1.3.6.1.2.1.47.1.1.1.1.5.1" => "3", + # power + "1.3.6.1.2.1.47.1.1.1.1.5.2" => "6", + # fan + "1.3.6.1.2.1.47.1.1.1.1.5.3" => "7", + # sensor + "1.3.6.1.2.1.47.1.1.1.1.5.4" => "8", + # module + "1.3.6.1.2.1.47.1.1.1.1.5.5" => "9", + # port + "1.3.6.1.2.1.47.1.1.1.1.5.6" => "10" + } + ] + + assert {:ok, entities} = Base.discover_physical_entities(client_opts) + assert length(entities) == 6 + + classes = entities |> Enum.map(& &1.entity_class) |> Enum.sort() + assert "chassis" in classes + assert "power" in classes + assert "fan" in classes + assert "sensor" in classes + assert "module" in classes + assert "port" in classes + end + + test "returns empty list when entity walk has no descriptions" do + assert {:ok, []} = Base.discover_physical_entities(adapter: Replay, oid_map: %{}) + end + end + + describe "identify_device/1 additional vendor patterns" do + test "identifies Cambium ePMP with model number from sysDescr" do + info = %{sys_descr: "Cambium ePMP 3000 AP", sys_object_id: ""} + device = Base.identify_device(info) + assert device.manufacturer == "Cambium Networks" + assert device.model == "ePMP 3000" + end + + test "identifies bare Cambium ePMP without model number" do + info = %{sys_descr: "Cambium ePMP wireless", sys_object_id: ""} + device = Base.identify_device(info) + # The regex matches "ePMP" followed by no digits and the + # extract_model_with_number helper returns "ePMP" only + assert device.manufacturer == "Cambium Networks" + assert device.model == "ePMP" + end + + test "identifies Cambium PMP from sysDescr with model number" do + info = %{sys_descr: "Cambium PMP 450 SM", sys_object_id: ""} + device = Base.identify_device(info) + assert device.manufacturer == "Cambium Networks" + assert device.model == "PMP 450" + end + + test "Ubiquiti AF-prefix antenna model extraction" do + info = %{sys_descr: "Ubiquiti AF-5XHD wireless", sys_object_id: ""} + device = Base.identify_device(info) + assert device.manufacturer == "Ubiquiti" + assert String.starts_with?(device.model, "AF") + end + + test "Cambium with no specific descr falls back to product line via sysObjectID (ePMP)" do + info = %{ + sys_descr: "Generic device", + sys_object_id: "1.3.6.1.4.1.17713.21.5.1" + } + + device = Base.identify_device(info) + assert device.manufacturer == "Cambium Networks" + assert device.model == "ePMP" + end + + test "Cambium with no descr and no matching OID line falls back to Wireless" do + info = %{ + sys_descr: "Cambium device", + # 17713 prefix but unknown product line (.99) + sys_object_id: "1.3.6.1.4.1.17713.99.1" + } + + device = Base.identify_device(info) + assert device.manufacturer == "Cambium Networks" + assert device.model == "Wireless" + end + end +end