diff --git a/test/towerops/snmp/mib_parser_test.exs b/test/towerops/snmp/mib_parser_test.exs new file mode 100644 index 00000000..fbd29ed4 --- /dev/null +++ b/test/towerops/snmp/mib_parser_test.exs @@ -0,0 +1,343 @@ +defmodule Towerops.Snmp.MibParserTest do + use ExUnit.Case, async: true + + alias Towerops.Snmp.MibParser + + describe "parse_mib_content/1" do + test "parses simple OBJECT-TYPE definitions" do + content = """ + sysDescr OBJECT-TYPE + ::= { system 1 } + + sysName OBJECT-TYPE + ::= { system 5 } + """ + + oids = MibParser.parse_mib_content(content) + + assert is_map(oids) + # These won't resolve without parent "system" being defined as a root + # but they should be extracted as assignments + end + + test "parses OBJECT IDENTIFIER definitions" do + content = """ + enterprises OBJECT IDENTIFIER ::= { private 1 } + mikrotik OBJECT IDENTIFIER ::= { enterprises 14988 } + """ + + oids = MibParser.parse_mib_content(content) + + assert is_map(oids) + # enterprises should resolve to 1.3.6.1.4.1 + assert Map.get(oids, "enterprises") == "1.3.6.1.4.1" + end + + test "removes comments before parsing" do + content = """ + -- This is a comment + sysDescr OBJECT-TYPE -- inline comment + ::= { system 1 } + -- Another comment + """ + + oids = MibParser.parse_mib_content(content) + + # Should parse without errors + assert is_map(oids) + end + + test "resolves OIDs with known roots" do + content = """ + parent OBJECT IDENTIFIER ::= { enterprises 999 } + myObject OBJECT-TYPE ::= { parent 1 } + """ + + oids = MibParser.parse_mib_content(content) + + assert Map.get(oids, "parent") == "1.3.6.1.4.1.999" + assert Map.get(oids, "myObject") == "1.3.6.1.4.1.999.1" + end + + test "resolves nested OID assignments" do + content = """ + parent OBJECT IDENTIFIER ::= { enterprises 100 } + child OBJECT-TYPE ::= { parent 5 } + grandchild OBJECT-TYPE ::= { child 3 } + """ + + oids = MibParser.parse_mib_content(content) + + assert Map.get(oids, "parent") == "1.3.6.1.4.1.100" + assert Map.get(oids, "child") == "1.3.6.1.4.1.100.5" + assert Map.get(oids, "grandchild") == "1.3.6.1.4.1.100.5.3" + end + + test "includes well-known root OIDs" do + oids = MibParser.parse_mib_content("") + + assert Map.get(oids, "iso") == "1" + assert Map.get(oids, "internet") == "1.3.6.1" + assert Map.get(oids, "mib-2") == "1.3.6.1.2.1" + assert Map.get(oids, "enterprises") == "1.3.6.1.4.1" + end + + test "handles empty content" do + oids = MibParser.parse_mib_content("") + + assert is_map(oids) + # Should have root OIDs + assert map_size(oids) > 0 + end + + test "handles multiline definitions" do + content = """ + sysDescr OBJECT-TYPE + SYNTAX DisplayString (SIZE (0..255)) + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "A textual description of the entity." + ::= { system 1 } + """ + + oids = MibParser.parse_mib_content(content) + + assert is_map(oids) + end + + test "extracts multiple objects from real-world MIB structure" do + content = """ + mycompany OBJECT IDENTIFIER ::= { enterprises 12345 } + system OBJECT IDENTIFIER ::= { mycompany 1 } + + sysDescr OBJECT-TYPE + ::= { system 1 } + + sysObjectID OBJECT-TYPE + ::= { system 2 } + + sysUpTime OBJECT-TYPE + ::= { system 3 } + """ + + oids = MibParser.parse_mib_content(content) + + assert Map.get(oids, "mycompany") == "1.3.6.1.4.1.12345" + assert Map.get(oids, "system") == "1.3.6.1.4.1.12345.1" + assert Map.get(oids, "sysDescr") == "1.3.6.1.4.1.12345.1.1" + assert Map.get(oids, "sysObjectID") == "1.3.6.1.4.1.12345.1.2" + assert Map.get(oids, "sysUpTime") == "1.3.6.1.4.1.12345.1.3" + end + end + + describe "parse_mib_file/1" do + test "returns error for non-existent file" do + assert {:error, :enoent} = MibParser.parse_mib_file("/nonexistent/file") + end + + test "returns error with descriptive message for missing file" do + result = MibParser.parse_mib_file("/tmp/does_not_exist_#{:rand.uniform(1000)}") + + assert match?({:error, _}, result) + end + + @tag :tmp_dir + test "parses valid MIB file", %{tmp_dir: tmp_dir} do + mib_file = Path.join(tmp_dir, "TEST-MIB") + + File.write!(mib_file, """ + TEST-MIB DEFINITIONS ::= BEGIN + + testObject OBJECT-TYPE ::= { enterprises 9999 } + testChild OBJECT-TYPE ::= { testObject 1 } + + END + """) + + assert {:ok, oids} = MibParser.parse_mib_file(mib_file) + assert is_map(oids) + assert Map.get(oids, "testObject") == "1.3.6.1.4.1.9999" + assert Map.get(oids, "testChild") == "1.3.6.1.4.1.9999.1" + end + + @tag :tmp_dir + test "handles MIB file with complex structure", %{tmp_dir: tmp_dir} do + mib_file = Path.join(tmp_dir, "COMPLEX-MIB") + + File.write!(mib_file, """ + COMPLEX-MIB DEFINITIONS ::= BEGIN + + -- Root object + myCompany OBJECT IDENTIFIER ::= { enterprises 12345 } + + -- Product line + products OBJECT IDENTIFIER ::= { myCompany 1 } + services OBJECT IDENTIFIER ::= { myCompany 2 } + + -- Specific products + productA OBJECT IDENTIFIER ::= { products 1 } + productB OBJECT IDENTIFIER ::= { products 2 } + + -- Product A metrics + productAUptime OBJECT-TYPE ::= { productA 1 } + productAStatus OBJECT-TYPE ::= { productA 2 } + + END + """) + + assert {:ok, oids} = MibParser.parse_mib_file(mib_file) + + assert Map.get(oids, "myCompany") == "1.3.6.1.4.1.12345" + assert Map.get(oids, "products") == "1.3.6.1.4.1.12345.1" + assert Map.get(oids, "productA") == "1.3.6.1.4.1.12345.1.1" + assert Map.get(oids, "productAUptime") == "1.3.6.1.4.1.12345.1.1.1" + end + end + + describe "validate_oid/3" do + @tag :tmp_dir + test "returns :ok when OID matches", %{tmp_dir: tmp_dir} do + mib_file = Path.join(tmp_dir, "VALID-MIB") + + File.write!(mib_file, """ + testObj OBJECT-TYPE ::= { enterprises 1234 } + """) + + assert :ok = MibParser.validate_oid(mib_file, "testObj", "1.3.6.1.4.1.1234") + end + + @tag :tmp_dir + test "returns error when OID does not match", %{tmp_dir: tmp_dir} do + mib_file = Path.join(tmp_dir, "MISMATCH-MIB") + + File.write!(mib_file, """ + testObj OBJECT-TYPE ::= { enterprises 1234 } + """) + + assert {:error, msg} = MibParser.validate_oid(mib_file, "testObj", "1.3.6.1.4.1.9999") + assert msg =~ "OID mismatch" + assert msg =~ "expected 1.3.6.1.4.1.9999" + assert msg =~ "got 1.3.6.1.4.1.1234" + end + + @tag :tmp_dir + test "returns error when object not found in MIB", %{tmp_dir: tmp_dir} do + mib_file = Path.join(tmp_dir, "MISSING-MIB") + + File.write!(mib_file, """ + existingObj OBJECT-TYPE ::= { enterprises 1234 } + """) + + assert {:error, msg} = MibParser.validate_oid(mib_file, "missingObj", "1.3.6.1.4.1.9999") + assert msg =~ "not found in MIB" + end + + test "returns error when MIB file does not exist" do + assert {:error, msg} = MibParser.validate_oid("/nonexistent", "obj", "1.2.3") + assert msg =~ "Failed to parse MIB" + end + end + + describe "list_mib_files/0" do + test "returns a list" do + files = MibParser.list_mib_files() + assert is_list(files) + end + + test "returns absolute paths" do + files = MibParser.list_mib_files() + + if length(files) > 0 do + Enum.each(files, fn path -> + assert String.starts_with?(path, "/") + end) + end + end + + test "only returns files ending with MIB" do + files = MibParser.list_mib_files() + + Enum.each(files, fn path -> + assert String.ends_with?(Path.basename(path), "MIB") + end) + end + + test "searches in subdirectories" do + files = MibParser.list_mib_files() + + # Should find MIBs in different subdirectories + if length(files) > 0 do + # At least some files should be in subdirectories + subdirs = + files + |> Enum.map(&Path.dirname/1) + |> Enum.uniq() + + # Should have multiple subdirectories + assert length(subdirs) >= 1 + end + end + end + + describe "edge cases" do + test "handles whitespace variations in assignments" do + content = """ + obj1 OBJECT-TYPE::={parent 1} + obj2 OBJECT-TYPE ::= {parent 2} + obj3 OBJECT-TYPE ::= { parent 3 } + """ + + oids = MibParser.parse_mib_content(content) + assert is_map(oids) + end + + test "handles definitions with hyphens in names" do + content = """ + my-object OBJECT-TYPE ::= { mib-2 999 } + """ + + # Hyphens in identifiers are actually invalid in the regex + # This documents current behavior + oids = MibParser.parse_mib_content(content) + # Won't match due to \\w+ in regex + refute Map.has_key?(oids, "my-object") + end + + test "handles numeric parent references" do + content = """ + obj OBJECT-TYPE ::= { 1 3 6 1 2 1 999 } + """ + + # This format isn't supported by the parser + # Documents current limitation + oids = MibParser.parse_mib_content(content) + refute Map.has_key?(oids, "obj") + end + + test "preserves root OIDs when parsing empty MIB" do + oids = MibParser.parse_mib_content("") + + # All well-known roots should be present + expected_roots = [ + "iso", + "org", + "dod", + "internet", + "directory", + "mgmt", + "mib-2", + "experimental", + "private", + "enterprises", + "security", + "snmpV2", + "snmpModules" + ] + + Enum.each(expected_roots, fn root -> + assert Map.has_key?(oids, root), "Missing root OID: #{root}" + end) + end + end +end