Add comprehensive tests for Towerops.Snmp.MibParser

Created test/towerops/snmp/mib_parser_test.exs with 25 tests covering:
- parse_mib_content/1: OID extraction and resolution logic
- parse_mib_file/1: File reading and error handling
- validate_oid/3: OID validation against MIB definitions
- list_mib_files/0: MIB file discovery
- Edge cases: whitespace, comments, nested assignments

Coverage improvement:
- Before: 60.00%
- After: 97.50%
- Improvement: +37.5%

All 1066 tests passing.
This commit is contained in:
Graham McIntire 2026-01-20 13:39:48 -06:00
parent ace3f1f661
commit 1dbcbb92f1
No known key found for this signature in database

View file

@ -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