From 910298b6eb123f2b4d435035c189538f6472f2c5 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 9 May 2026 12:23:02 -0500 Subject: [PATCH] test: Profiles.Dynamic vendor post-processing + debug data branches --- .../snmp/profiles/dynamic_extra_test.exs | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 test/towerops/snmp/profiles/dynamic_extra_test.exs diff --git a/test/towerops/snmp/profiles/dynamic_extra_test.exs b/test/towerops/snmp/profiles/dynamic_extra_test.exs new file mode 100644 index 00000000..86d56d56 --- /dev/null +++ b/test/towerops/snmp/profiles/dynamic_extra_test.exs @@ -0,0 +1,248 @@ +defmodule Towerops.Snmp.Profiles.DynamicExtraTest do + @moduledoc """ + Targets uncovered branches in `Towerops.Snmp.Profiles.Dynamic`: + + * `collect_vendor_debug_data/2` — debug data with state sensors + (state_descr branch) and wireless OID error/value paths + * `walk_descr_oid/2` empty walk fallback (no descr map) + * `apply_vendor_post_processing` for `arista_eos`, `arista-mos`, + `dell-powervault` profile names (delegates to vendor modules) + * `get_vendor_wireless_oids/1` no-name fallback path via + `collect_vendor_debug_data` on profile without vendor module + * `has_mib_symbolic_name?/1` non-binary clause (e.g. `nil` description) + """ + + use Towerops.DataCase, async: true + + import Mox + + alias Towerops.Snmp.Profiles.Dynamic + alias Towerops.Snmp.SnmpMock + + setup :verify_on_exit! + + @client_opts [ + ip: "192.168.1.1", + community: "public", + version: "2c", + port: 161, + timeout: 5000 + ] + + defp base_profile(overrides \\ %{}) do + Map.merge( + %{ + name: "test_device", + vendor: "TestVendor", + type: "router", + device_oids: %{}, + sensor_oids: [], + table_sensor_oids: [], + processor_oids: [], + count_sensor_oids: [], + state_sensor_oids: [] + }, + overrides + ) + end + + describe "collect_vendor_debug_data/2" do + test "includes state sensors with state_descr in discovered_sensors" do + profile = + base_profile(%{ + state_sensor_oids: [ + %{ + base_oid: "1.3.6.1.4.1.99999.4.1", + oid_name: "gps_status", + sensor_type: "state", + sensor_descr: "GPS Status", + states: %{0 => "No Fix", 2 => "3D Fix"} + } + ] + }) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + + stub(SnmpMock, :walk, fn _, oid, _ -> + case oid do + "1.3.6.1.4.1.99999.4.1" -> + {:ok, [%{oid: "1.3.6.1.4.1.99999.4.1.0", value: {:integer, 2}}]} + + _ -> + {:ok, []} + end + end) + + result = Dynamic.collect_vendor_debug_data(profile, @client_opts) + + assert is_list(result.discovered_sensors) + assert is_map(result.wireless_sensors) + + # State sensor adds state_descr field to debug entry + state_entry = Enum.find(result.discovered_sensors, fn s -> Map.has_key?(s, :state_descr) end) + assert state_entry, "expected at least one entry with :state_descr key" + assert state_entry.state_descr == "3D Fix" + assert state_entry.type == "state" + end + + test "returns empty wireless_sensors map for profile with no vendor module" do + # `nonexistent_vendor` has no module in VendorRegistry, so + # get_vendor_wireless_oids returns []. wireless_sensors stays %{}. + profile = base_profile(%{name: "nonexistent_vendor_zzz"}) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end) + + result = Dynamic.collect_vendor_debug_data(profile, @client_opts) + + assert result.wireless_sensors == %{} + assert is_list(result.discovered_sensors) + end + + test "handles profile without :name key in get_vendor_wireless_oids" do + # When profile has no :name key, get_vendor_wireless_oids returns [] + # (the function-clause fallback). We need to provide all keys that + # discover_sensors reads via Map.get. + profile = %{ + vendor: "Anon", + type: "router", + device_oids: %{}, + sensor_oids: [], + table_sensor_oids: [], + processor_oids: [], + count_sensor_oids: [], + state_sensor_oids: [] + } + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end) + + result = Dynamic.collect_vendor_debug_data(profile, @client_opts) + + assert result.wireless_sensors == %{} + assert result.discovered_sensors == [] + end + end + + describe "discover_sensors/2 descr_oid empty walk fallback" do + test "uses default sensor description when descr_oid walk returns error" do + # descr_oid returns error -> walk_descr_oid returns %{} -> default desc used + profile = + base_profile(%{ + table_sensor_oids: [ + %{ + base_oid: "1.3.6.1.4.1.99999.3.1", + descr_oid: "1.3.6.1.4.1.99999.3.2", + sensor_type: "temperature", + sensor_descr: "Temp", + sensor_unit: "C", + sensor_divisor: 1 + } + ] + }) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + + stub(SnmpMock, :walk, fn _, oid, _ -> + case oid do + "1.3.6.1.4.1.99999.3.1" -> + {:ok, [%{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}]} + + "1.3.6.1.4.1.99999.3.2" -> + # Errored descr walk - hits walk_descr_oid catch-all returning %{} + {:error, :timeout} + + _ -> + {:ok, []} + end + end) + + assert {:ok, sensors} = Dynamic.discover_sensors(profile, @client_opts) + + sensor = Enum.find(sensors, &(&1.sensor_type == "temperature")) + assert sensor.sensor_descr == "Temp" + end + end + + describe "discover_sensors/2 vendor post-processing" do + test "arista_eos profile delegates to Arista.post_process_sensors" do + profile = + Map.merge(base_profile(), %{ + name: "arista_eos", + vendor: "Arista" + }) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end) + + assert {:ok, sensors} = Dynamic.discover_sensors(profile, @client_opts) + # No sensors discovered - post_process_sensors is called with empty list + # and returns it unchanged. + assert is_list(sensors) + end + + test "arista-mos profile delegates to Arista.post_process_sensors" do + profile = + Map.merge(base_profile(), %{ + name: "arista-mos", + vendor: "Arista" + }) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end) + + assert {:ok, sensors} = Dynamic.discover_sensors(profile, @client_opts) + assert is_list(sensors) + end + + test "dell-powervault profile delegates to Powervault.post_process_sensors" do + profile = + Map.merge(base_profile(), %{ + name: "dell-powervault", + vendor: "Dell" + }) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end) + + assert {:ok, sensors} = Dynamic.discover_sensors(profile, @client_opts) + assert is_list(sensors) + end + end + + describe "discover_sensors/2 deduplication with non-binary description" do + test "does not crash when sensor description is nil" do + # has_mib_symbolic_name? must handle non-binary (nil) values without + # crashing — covered by the catch-all clause `defp has_mib_symbolic_name?(_)`. + # Discover sensors so that one ends up with no sensor_descr. + profile = + base_profile(%{ + table_sensor_oids: [ + %{ + base_oid: "1.3.6.1.4.1.99999.3.1", + sensor_type: nil, + # No sensor_descr, no oid_name -> falls back to sensor_type which is nil + sensor_unit: "", + sensor_divisor: 1, + descr_oid: nil + } + ] + }) + + stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end) + + stub(SnmpMock, :walk, fn _, oid, _ -> + case oid do + "1.3.6.1.4.1.99999.3.1" -> + {:ok, [%{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}]} + + _ -> + {:ok, []} + end + end) + + # Just verifying no crash occurs + assert {:ok, _sensors} = Dynamic.discover_sensors(profile, @client_opts) + end + end +end