343 lines
9.5 KiB
Elixir
343 lines
9.5 KiB
Elixir
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 files != [] 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 files != [] do
|
|
# At least some files should be in subdirectories
|
|
subdirs =
|
|
files
|
|
|> Enum.map(&Path.dirname/1)
|
|
|> Enum.uniq()
|
|
|
|
# Should have multiple subdirectories
|
|
assert subdirs != [], "Expected at least 1 subdirectory"
|
|
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
|