defmodule SnmpKit.SnmpMgr.MIBTest do use ExUnit.Case, async: false alias SnmpKit.SnmpLib.MIB.Parser alias SnmpKit.SnmpMgr.MIB doctest MIB setup do # Start the MIB GenServer if not already running case GenServer.whereis(MIB) do nil -> {:ok, pid} = MIB.start_link() on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end) {:ok, mib_pid: pid} pid -> {:ok, mib_pid: pid} end end # Test helper to parse a MIB and extract name->OID map defp parse_and_extract(mib_path) do {:ok, content} = File.read(mib_path) {:ok, parsed} = Parser.parse(content) definitions = Map.get(parsed, :definitions, []) # Extract OID definitions (simulating build_oid_definition_map) oid_defs = definitions |> Enum.filter(fn def -> type = Map.get(def, :__type__) type in [:object_type, :object_identifier, :module_identity] end) |> Enum.reduce(%{}, fn def, acc -> name = Map.get(def, :name) oid = Map.get(def, :oid) parent = Map.get(def, :parent) sub_index = Map.get(def, :sub_index) cond do name && oid -> Map.put(acc, name, oid) name && parent && sub_index -> Map.put(acc, name, {String.to_atom(parent), sub_index}) name && parent -> Map.put(acc, name, {String.to_atom(parent), nil}) true -> acc end end) # Apply workaround for parser bug with index 10 fix_missing_indices(oid_defs, content) end # Workaround for parser bug - extract indices from raw MIB content defp fix_missing_indices(oid_defs, mib_content) do missing = Enum.filter(oid_defs, fn {_, oid_def} -> case oid_def do {_, nil} -> true _ -> false end end) Enum.reduce(missing, oid_defs, fn {name, _}, acc -> regex = ~r/#{name}\s+OBJECT\s+IDENTIFIER\s+::=\s+\{\s+(\w+)\s+(\d+)\s+\}/ case Regex.run(regex, mib_content) do [_, parent, index_str] -> index = String.to_integer(index_str) parent_atom = String.to_atom(parent) Map.put(acc, name, {parent_atom, <>}) _ -> acc end end) end # Test helper to resolve OID tuples defp resolve_oid_def({parent_name, index_binary}, resolved_map) when is_atom(parent_name) do parent_str = Atom.to_string(parent_name) case Map.get(resolved_map, parent_str) do parent_oid when is_list(parent_oid) -> # Decode index - MIB parser uses UTF-8 character for numeric values index = case String.to_charlist(index_binary) do [code_point | _] -> code_point [] -> :binary.decode_unsigned(index_binary) end {:ok, parent_oid ++ [index]} _ -> {:error, :parent_not_resolved} end end defp resolve_oid_def(oid_list, _resolved_map) when is_list(oid_list) do if Enum.all?(oid_list, &is_integer/1) do {:ok, oid_list} else {:error, :not_resolved} end end defp resolve_oid_def(_, _), do: {:error, :invalid_format} describe "MIB parsing and OID resolution" do @tag :yecc_required test "workaround fixes missing sub_index for ubntAFLTU" do mib_path = Path.join(:code.priv_dir(:towerops), "mibs/ubnt/UBNT-MIB") oid_defs = parse_and_extract(mib_path) # ubntAFLTU should have index 10 after workaround assert {:ubntMIB, <<10>>} = oid_defs["ubntAFLTU"], "ubntAFLTU should have been fixed to {:ubntMIB, <<10>>} by workaround" end @tag :yecc_required test "parses UBNT-MIB and extracts OID definitions" do mib_path = Path.join(:code.priv_dir(:towerops), "mibs/ubnt/UBNT-MIB") oid_defs = parse_and_extract(mib_path) # Should have extracted OID definitions assert map_size(oid_defs) > 0 # Check specific definitions assert Map.has_key?(oid_defs, "ubnt") assert Map.has_key?(oid_defs, "ubntMIB") assert Map.has_key?(oid_defs, "ubntAFLTU") # ubnt should reference enterprises with index 41112 assert {:enterprises, _} = oid_defs["ubnt"] # ubntMIB should reference ubnt with an index assert {:ubnt, _} = oid_defs["ubntMIB"] # ubntAFLTU should reference ubntMIB with index 10 (fixed by workaround) assert {:ubntMIB, <<10>>} = oid_defs["ubntAFLTU"] end test "resolves OID with parent reference" do # Simulate having enterprises already resolved resolved_map = %{"enterprises" => [1, 3, 6, 1, 4, 1]} # ubnt = enterprises.41112 (where 41112 is encoded as UTF-8 char "ꂘ") ubnt_def = {:enterprises, "ꂘ"} assert {:ok, [1, 3, 6, 1, 4, 1, 41_112]} = resolve_oid_def(ubnt_def, resolved_map) end test "resolves OID chain iteratively" do # Start with standard OIDs resolved = %{ "enterprises" => [1, 3, 6, 1, 4, 1], "mib-2" => [1, 3, 6, 1, 2, 1] } # First iteration: resolve ubnt ubnt_def = {:enterprises, "ꂘ"} {:ok, ubnt_oid} = resolve_oid_def(ubnt_def, resolved) resolved = Map.put(resolved, "ubnt", ubnt_oid) assert resolved["ubnt"] == [1, 3, 6, 1, 4, 1, 41_112] # Second iteration: resolve ubntMIB (ubnt.1) ubnt_mib_def = {:ubnt, <<1>>} {:ok, ubnt_mib_oid} = resolve_oid_def(ubnt_mib_def, resolved) resolved = Map.put(resolved, "ubntMIB", ubnt_mib_oid) assert resolved["ubntMIB"] == [1, 3, 6, 1, 4, 1, 41_112, 1] # Third iteration: resolve ubntAFLTU (ubntMIB.10) ubnt_afltu_def = {:ubntMIB, <<10>>} {:ok, ubnt_afltu_oid} = resolve_oid_def(ubnt_afltu_def, resolved) resolved = Map.put(resolved, "ubntAFLTU", ubnt_afltu_oid) assert resolved["ubntAFLTU"] == [1, 3, 6, 1, 4, 1, 41_112, 1, 10] end end describe "MIB GenServer basic operations" do test "GenServer starts successfully" do assert Process.alive?(Process.whereis(MIB)) end test "load_standard_mibs returns :ok" do assert :ok = MIB.load_standard_mibs() end end describe "resolve/1" do test "resolves standard MIB names" do assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1]} = MIB.resolve("sysDescr") assert {:ok, [1, 3, 6, 1, 2, 1, 1, 5]} = MIB.resolve("sysName") assert {:ok, [1, 3, 6, 1, 2, 1, 2, 2, 1, 2]} = MIB.resolve("ifDescr") end test "resolves names with instance suffix" do assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} = MIB.resolve("sysDescr.0") assert {:ok, [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1]} = MIB.resolve("ifDescr.1") assert {:ok, [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1, 2]} = MIB.resolve("ifDescr.1.2") end test "returns error for unknown names" do assert {:error, :not_found} = MIB.resolve("unknownMibName") end test "returns error for invalid input" do assert {:error, :invalid_name} = MIB.resolve(nil) assert {:error, :invalid_name} = MIB.resolve(123) end test "returns error for names with invalid instance parts" do assert {:error, :invalid_instance} = MIB.resolve("sysDescr.abc") end end describe "reverse_lookup/1" do test "reverses standard OIDs to names" do assert {:ok, "sysDescr"} = MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1]) assert {:ok, "sysName"} = MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 5]) end test "reverses OIDs with instance suffix" do assert {:ok, "sysDescr.0"} = MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1, 0]) assert {:ok, "ifDescr.1"} = MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1]) end test "accepts OID as string" do assert {:ok, "sysDescr"} = MIB.reverse_lookup("1.3.6.1.2.1.1.1") assert {:ok, "sysDescr.0"} = MIB.reverse_lookup("1.3.6.1.2.1.1.1.0") end test "returns error for unknown OIDs" do assert {:error, :not_found} = MIB.reverse_lookup([9, 9, 9, 9, 9]) end test "returns error for empty OID" do assert {:error, :empty_oid} = MIB.reverse_lookup([]) end test "returns error for invalid OID format" do assert {:error, :invalid_oid_string} = MIB.reverse_lookup("not.a.valid.oid") end test "returns error for binary OID (edge case)" do # This tests the guard in find_partial_reverse_match assert {:error, :invalid_oid_string} = MIB.reverse_lookup("binary_string") end end describe "parent/1" do test "returns parent OID" do assert {:ok, [1, 3, 6, 1, 2, 1, 1]} = MIB.parent([1, 3, 6, 1, 2, 1, 1, 1]) assert {:ok, [1, 3, 6]} = MIB.parent([1, 3, 6, 1]) end test "returns empty list for single element" do assert {:ok, []} = MIB.parent([1]) end test "returns error for empty list" do assert {:error, :no_parent} = MIB.parent([]) end test "accepts OID as string" do assert {:ok, [1, 3, 6, 1, 2, 1, 1]} = MIB.parent("1.3.6.1.2.1.1.1") end test "returns error for invalid string OID" do assert {:error, :invalid_oid_string} = MIB.parent("invalid") end end describe "children/1" do test "finds direct children of an OID" do # system (1.3.6.1.2.1.1) should have children like sysDescr, sysObjectID, etc. {:ok, children} = MIB.children([1, 3, 6, 1, 2, 1, 1]) assert is_list(children) and children != [] assert "sysDescr" in children assert "sysName" in children end test "accepts OID as string" do {:ok, children} = MIB.children("1.3.6.1.2.1.1") assert is_list(children) and children != [] assert "sysDescr" in children end test "accepts nil as root (returns all top-level entries)" do {:ok, children} = MIB.children(nil) assert is_list(children) and children != [] end test "returns empty list for leaf nodes" do # sysDescr is a leaf node {:ok, children} = MIB.children([1, 3, 6, 1, 2, 1, 1, 1]) assert children == [] end test "returns error for invalid OID format" do assert {:error, :invalid_parent_oid} = MIB.children("invalid") assert {:error, :invalid_parent_oid} = MIB.children(123) end end describe "walk_tree/2" do test "walks tree from root OID" do {:ok, descendants} = MIB.walk_tree([1, 3, 6, 1, 2, 1, 1]) assert is_list(descendants) and descendants != [] refute Enum.empty?(descendants) # Should return tuples of {name, oid} assert Enum.all?(descendants, fn {name, oid} -> is_binary(name) and is_list(oid) end) end test "accepts OID as string" do {:ok, descendants} = MIB.walk_tree("1.3.6.1.2.1.1") assert is_list(descendants) and descendants != [] refute Enum.empty?(descendants) end test "accepts nil as root" do {:ok, descendants} = MIB.walk_tree(nil) assert is_list(descendants) and descendants != [] # Should return all known OIDs refute Enum.empty?(descendants) end test "returns empty list for unknown OID" do {:ok, descendants} = MIB.walk_tree([9, 9, 9, 9]) assert descendants == [] end end describe "object_info/1" do test "returns enriched metadata for standard objects" do {:ok, info} = MIB.object_info("sysDescr") assert info.name == "sysDescr" assert info.oid == [1, 3, 6, 1, 2, 1, 1, 1] assert info.module == "SNMPv2-MIB" assert is_map(info.syntax) and map_size(info.syntax) > 0 assert info.syntax.base == :octet_string assert info.syntax.textual_convention == "DisplayString" end test "includes instance information when present" do {:ok, info} = MIB.object_info("sysDescr.0") assert info.name == "sysDescr" assert info.instance_index == 0 assert info.instance_oid == [1, 3, 6, 1, 2, 1, 1, 1, 0] end test "handles multi-part instance indices" do {:ok, info} = MIB.object_info("ifDescr.1.2") assert info.name == "ifDescr" assert info.instance_index == [1, 2] assert info.instance_oid == [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1, 2] end test "accepts OID as list" do {:ok, info} = MIB.object_info([1, 3, 6, 1, 2, 1, 1, 1]) assert info.name == "sysDescr" end test "accepts OID as dotted string" do {:ok, info} = MIB.object_info("1.3.6.1.2.1.1.1") assert info.name == "sysDescr" end test "returns error for invalid input" do assert {:error, _} = MIB.object_info(123) assert {:error, _} = MIB.object_info(%{}) end end describe "reverse_lookup_enriched/1" do test "is an alias for object_info/1" do {:ok, info1} = MIB.object_info("sysDescr") {:ok, info2} = MIB.reverse_lookup_enriched("sysDescr") assert info1 == info2 end end describe "object_info_many/1" do test "returns list of info for multiple names" do {:ok, infos} = MIB.object_info_many(["sysDescr", "sysName", "sysLocation"]) assert length(infos) == 3 assert Enum.all?(infos, &is_map/1) assert Enum.map(infos, & &1.name) == ["sysDescr", "sysName", "sysLocation"] end test "returns error if any lookup fails" do assert {:error, _} = MIB.object_info_many(["sysDescr", "invalidName"]) end end describe "resolve_enhanced/2" do test "resolves names to object info" do {:ok, info} = MIB.resolve_enhanced("sysDescr") assert is_map(info) and map_size(info) > 0 assert info.name == "sysDescr" assert info.oid == [1, 3, 6, 1, 2, 1, 1, 1] end end describe "compile/2" do test "returns error for non-existent file" do assert {:error, _} = MIB.compile("nonexistent.mib") end test "handles snmp_lib_not_available gracefully" do # The function should return an error, not crash result = MIB.compile("test.mib") assert match?({:error, _}, result) end end describe "compile_dir/2" do test "returns error for non-existent directory" do assert {:error, {:directory_error, :enoent}} = MIB.compile_dir("/nonexistent/path") end test "processes existing directory" do # Create a temporary directory tmp_dir = Path.join(System.tmp_dir!(), "mib_test_#{:rand.uniform(10_000)}") File.mkdir_p!(tmp_dir) try do # Should return ok with empty results since no .mib files result = MIB.compile_dir(tmp_dir) assert elem(result, 0) in [:ok, :error] after File.rm_rf!(tmp_dir) end end end describe "parse_mib_file/2" do test "returns error for non-existent file" do assert {:error, {:file_read_error, :enoent}} = MIB.parse_mib_file("nonexistent.mib") end end describe "parse_mib_content/2" do test "parses basic MIB content" do content = """ TestMIB DEFINITIONS ::= BEGIN testObject OBJECT-TYPE SYNTAX Integer32 MAX-ACCESS read-only STATUS current DESCRIPTION "Test object" ::= { testGroup 1 } END """ {:ok, result} = MIB.parse_mib_content(content) assert is_map(result) and map_size(result) > 0 assert Map.has_key?(result, :tokens) assert Map.has_key?(result, :parsed_objects) end test "returns error for invalid MIB content" do # Completely invalid content that can't be tokenized result = MIB.parse_mib_content("not valid mib at all!!!") assert match?({:error, {:tokenization_failed, _}}, result) end end describe "load/1" do test "handles non-existent compiled MIB path" do # Should return error, not crash result = MIB.load("/nonexistent/compiled.mib") valid_result = match?(:ok, result) or match?({:error, _}, result) assert valid_result == true end end describe "load_and_integrate_mib/2" do test "returns error when compilation fails" do result = MIB.load_and_integrate_mib("nonexistent.mib") assert match?({:error, _}, result) end end describe "syntax parsing" do test "handles various syntax types in object_info" do # Test different syntax types through object_info {:ok, sys_descr_info} = MIB.object_info("sysDescr") assert sys_descr_info.syntax.base == :octet_string assert sys_descr_info.syntax.textual_convention == "DisplayString" {:ok, if_speed_info} = MIB.object_info("ifSpeed") assert if_speed_info.syntax.base == :gauge32 end test "handles objects without curated syntax metadata" do # Enterprise OIDs don't have curated syntax metadata {:ok, enterprises_info} = MIB.object_info("enterprises") # Should still return syntax map even if empty assert %{} = enterprises_info.syntax end end describe "MIB integration with parsed content" do test "integrates MIB data with complex OID definitions" do # Simulate parsed MIB data with parent references mib_content = """ TEST-MIB DEFINITIONS ::= BEGIN testRoot OBJECT IDENTIFIER ::= { enterprises 99999 } testObject OBJECT-TYPE SYNTAX Integer32 MAX-ACCESS read-only STATUS current ::= { testRoot 1 } END """ # This will exercise the parse_mib_content path {:ok, result} = MIB.parse_mib_content(mib_content) assert is_map(result) and map_size(result) > 0 assert Map.has_key?(result, :tokens) end end describe "error handling edge cases" do test "compile handles SnmpLib.MIB compilation errors gracefully" do # Should return error tuple, not crash result = MIB.compile("invalid_mib_file.mib") assert match?({:error, _}, result) end test "compile_dir handles fallback when SnmpLib is not available" do # Create a temp directory with a .mib file to test fallback tmp_dir = Path.join(System.tmp_dir!(), "mib_fallback_test_#{:rand.uniform(10_000)}") File.mkdir_p!(tmp_dir) try do # Create a dummy .mib file test_mib = Path.join(tmp_dir, "test.mib") File.write!(test_mib, "TEST-MIB DEFINITIONS ::= BEGIN END") # This should trigger compile_dir_fallback result = MIB.compile_dir(tmp_dir) assert elem(result, 0) in [:ok, :error] after File.rm_rf!(tmp_dir) end end end describe "private helper coverage" do test "object_info handles dotted OID strings" do # This exercises numeric_oid_string? and OID parsing through object_info {:ok, info} = MIB.object_info("1.3.6.1.2.1.1.1") assert info.name == "sysDescr" assert info.oid == [1, 3, 6, 1, 2, 1, 1, 1] end test "object_info handles leading dot in OID string" do {:ok, info} = MIB.object_info(".1.3.6.1.2.1.1.1") assert info.name == "sysDescr" assert info.oid == [1, 3, 6, 1, 2, 1, 1, 1] end test "object_info handles complex instance indices" do # Multi-part instance index {:ok, info} = MIB.object_info([1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1, 0, 0]) assert info.name == "ifDescr" assert info.instance_index == [1, 0, 0] end test "children returns sorted results" do {:ok, children} = MIB.children([1, 3, 6, 1, 2, 1, 1]) # Should be sorted alphabetically assert children == Enum.sort(children) end test "walk_tree returns sorted results by OID" do {:ok, descendants} = MIB.walk_tree([1, 3, 6, 1, 2, 1, 1]) # Extract OIDs and verify they're sorted oids = Enum.map(descendants, fn {_name, oid} -> oid end) assert oids == Enum.sort(oids) end end describe "module_for helper" do test "identifies correct modules for known prefixes" do {:ok, info} = MIB.object_info("ifHCInOctets") assert info.module == "IF-MIB" {:ok, info2} = MIB.object_info("sysDescr") assert info2.module == "SNMPv2-MIB" {:ok, info3} = MIB.object_info("ifDescr") assert info3.module == "IF-MIB" end test "handles objects without module mapping" do {:ok, info} = MIB.object_info("enterprises") # May not have a module mapping, should handle gracefully has_valid_module = is_nil(info.module) or is_binary(info.module) assert has_valid_module == true end end end