towerops/test/snmpkit/snmp_lib/oid_test.exs
Graham McIntire 348975dcc9 Rewrite polling_offset, changelog_parser, and user_agent_parser in Gleam (#103)
Migrate three pure-function modules from Elixir to Gleam:

- polling_offset: deterministic hash-based polling offset (pure Gleam, no FFI)
- changelog_parser: text parsing logic in Gleam, thin Elixir wrapper for File I/O
- user_agent_parser: regex-based UA parsing in Gleam with Erlang FFI for
  atom-keyed map conversion

Add gleam_regexp dependency for regex support in Gleam modules.
Update all call sites and tests to use Gleam module atoms.

Reviewed-on: graham/towerops-web#103
2026-03-21 10:11:06 -05:00

433 lines
13 KiB
Elixir

defmodule SnmpKit.SnmpLib.OIDTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpLib.OID
@moduletag :unit
@moduletag :protocol
@moduletag :phase_2
describe "String/List conversions" do
test "converts valid OID string to list" do
{:ok, oid_list} = OID.string_to_list("1.3.6.1.2.1.1.1.0")
assert oid_list == [1, 3, 6, 1, 2, 1, 1, 1, 0]
end
test "converts valid OID string with leading dot to list" do
{:ok, oid_list} = OID.string_to_list(".1.3.6.1.2.1.1.1.0")
assert oid_list == [1, 3, 6, 1, 2, 1, 1, 1, 0]
end
test "converts valid OID list to string" do
{:ok, oid_string} = OID.list_to_string([1, 3, 6, 1, 2, 1, 1, 1, 0])
assert oid_string == "1.3.6.1.2.1.1.1.0"
end
test "handles single component OID" do
{:ok, oid_list} = OID.string_to_list("42")
assert oid_list == [42]
{:ok, oid_string} = OID.list_to_string([42])
assert oid_string == "42"
end
test "handles single component OID with leading dot" do
{:ok, oid_list} = OID.string_to_list(".42")
assert oid_list == [42]
end
test "handles short OID with leading dot" do
{:ok, oid_list} = OID.string_to_list(".1.3.6")
assert oid_list == [1, 3, 6]
end
test "rejects empty OID string" do
assert {:error, :empty_oid} = OID.string_to_list("")
assert {:error, :empty_oid} = OID.string_to_list(" ")
assert {:error, :empty_oid} = OID.string_to_list(".")
assert {:error, :empty_oid} = OID.string_to_list(" . ")
end
test "rejects empty OID list" do
assert {:error, :empty_oid} = OID.list_to_string([])
end
test "rejects invalid OID string formats" do
assert {:error, :invalid_oid_string} = OID.string_to_list("1.3.6.1.a.2")
assert {:error, :invalid_oid_string} = OID.string_to_list("1..3.6.1")
assert {:error, :invalid_oid_string} = OID.string_to_list("1.3.6.1.")
assert {:error, :invalid_oid_string} = OID.string_to_list(".1.3.6.1.a.2")
assert {:error, :invalid_oid_string} = OID.string_to_list(".1..3.6.1")
assert {:error, :invalid_oid_string} = OID.string_to_list(".1.3.6.1.")
end
test "rejects negative components" do
assert {:error, :invalid_component} = OID.list_to_string([1, 3, -1, 4])
assert {:error, :invalid_component} = OID.list_to_string([-1])
end
test "rejects non-integer components" do
assert {:error, :invalid_component} = OID.list_to_string([1, 3, "6", 1])
assert {:error, :invalid_component} = OID.list_to_string([1.5, 3, 6])
end
test "handles large OID components" do
large_oid = [1, 3, 6, 1, 4, 1, 999_999, 1, 2, 3]
{:ok, oid_string} = OID.list_to_string(large_oid)
{:ok, parsed_oid} = OID.string_to_list(oid_string)
assert parsed_oid == large_oid
end
end
describe "Tree operations" do
test "correctly identifies child relationships" do
parent = [1, 3, 6, 1, 2, 1]
child = [1, 3, 6, 1, 2, 1, 1, 1, 0]
assert OID.child_of?(child, parent) == true
assert OID.child_of?(parent, child) == false
end
test "correctly identifies parent relationships" do
parent = [1, 3, 6, 1, 2, 1]
child = [1, 3, 6, 1, 2, 1, 1, 1, 0]
assert OID.parent_of?(parent, child) == true
assert OID.parent_of?(child, parent) == false
end
test "rejects equal OIDs as child/parent" do
oid = [1, 3, 6, 1, 2, 1]
assert OID.child_of?(oid, oid) == false
assert OID.parent_of?(oid, oid) == false
end
test "rejects sibling OIDs as child/parent" do
oid1 = [1, 3, 6, 1, 2, 1]
oid2 = [1, 3, 6, 1, 2, 2]
assert OID.child_of?(oid1, oid2) == false
assert OID.child_of?(oid2, oid1) == false
end
test "gets parent OID correctly" do
oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
{:ok, parent} = OID.get_parent(oid)
assert parent == [1, 3, 6, 1, 2, 1, 1, 1]
end
test "handles root OIDs in get_parent" do
assert {:error, :root_oid} = OID.get_parent([])
assert {:error, :root_oid} = OID.get_parent([1])
end
test "gets immediate children from OID set" do
parent = [1, 3, 6, 1]
oid_set = [
[1, 3, 6, 1, 2],
[1, 3, 6, 1, 4],
[1, 3, 6, 1, 2, 1],
[1, 3, 6, 1, 4, 1, 9],
[1, 3, 6, 2]
]
children = OID.get_children(parent, oid_set)
assert length(children) == 2
assert [1, 3, 6, 1, 2] in children
assert [1, 3, 6, 1, 4] in children
end
test "finds next OID in sorted set" do
current = [1, 3, 6, 1, 2, 1, 1, 1, 0]
oid_set = [
[1, 3, 6, 1, 2, 1, 1, 1, 0],
[1, 3, 6, 1, 2, 1, 1, 2, 0],
[1, 3, 6, 1, 2, 1, 1, 3, 0]
]
{:ok, next_oid} = OID.get_next_oid(current, oid_set)
assert next_oid == [1, 3, 6, 1, 2, 1, 1, 2, 0]
end
test "handles end of MIB in get_next_oid" do
current = [1, 3, 6, 1, 2, 1, 1, 3, 0]
oid_set = [
[1, 3, 6, 1, 2, 1, 1, 1, 0],
[1, 3, 6, 1, 2, 1, 1, 2, 0],
[1, 3, 6, 1, 2, 1, 1, 3, 0]
]
assert {:error, :end_of_mib} = OID.get_next_oid(current, oid_set)
end
end
describe "Comparison operations" do
test "compares OIDs lexicographically" do
oid1 = [1, 3, 6, 1]
oid2 = [1, 3, 6, 2]
oid3 = [1, 3, 6, 1]
oid4 = [1, 3, 6, 1, 2]
assert OID.compare(oid1, oid2) == :lt
assert OID.compare(oid2, oid1) == :gt
assert OID.compare(oid1, oid3) == :eq
assert OID.compare(oid1, oid4) == :lt
assert OID.compare(oid4, oid1) == :gt
end
test "sorts OID list correctly" do
oids = [
[1, 3, 6, 2],
[1, 3, 6, 1, 2],
[1, 3, 6, 1],
[1, 3, 6, 1, 4, 1],
[1, 3, 6, 1, 1]
]
sorted = OID.sort(oids)
expected = [
[1, 3, 6, 1],
[1, 3, 6, 1, 1],
[1, 3, 6, 1, 2],
[1, 3, 6, 1, 4, 1],
[1, 3, 6, 2]
]
assert sorted == expected
end
test "handles empty list in sort" do
assert OID.sort([]) == []
assert OID.sort(:not_a_list) == []
end
end
describe "Table operations" do
test "extracts table index correctly" do
table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
instance_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1]
{:ok, index} = OID.extract_table_index(table_oid, instance_oid)
assert index == [1]
end
test "extracts complex table index" do
table_oid = [1, 3, 6, 1, 2, 1, 4, 20, 1, 1]
instance_oid = [1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 192, 168, 1, 1]
{:ok, index} = OID.extract_table_index(table_oid, instance_oid)
assert index == [192, 168, 1, 1]
end
test "rejects invalid table instances" do
table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
# Wrong table column
invalid_instance = [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1]
assert {:error, :invalid_table_instance} =
OID.extract_table_index(table_oid, invalid_instance)
end
test "builds table instance from OID and index" do
table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
index = [1]
{:ok, instance_oid} = OID.build_table_instance(table_oid, index)
assert instance_oid == [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1]
end
test "builds complex table instance" do
table_oid = [1, 3, 6, 1, 2, 1, 4, 20, 1, 1]
index = [192, 168, 1, 1]
{:ok, instance_oid} = OID.build_table_instance(table_oid, index)
assert instance_oid == [1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 192, 168, 1, 1]
end
test "parses integer table index" do
{:ok, value} = OID.parse_table_index([42], :integer)
assert value == 42
end
test "parses fixed-length string table index" do
# "test" in ASCII
index = [116, 101, 115, 116]
{:ok, value} = OID.parse_table_index(index, {:string, 4})
assert value == "test"
end
test "parses variable-length string table index" do
# Length-prefixed "test"
index = [4, 116, 101, 115, 116]
{:ok, value} = OID.parse_table_index(index, {:variable_string})
assert value == "test"
end
test "builds integer table index" do
{:ok, index} = OID.build_table_index(42, :integer)
assert index == [42]
end
test "builds fixed-length string table index" do
{:ok, index} = OID.build_table_index("test", {:string, 4})
assert index == [116, 101, 115, 116]
end
test "builds variable-length string table index" do
{:ok, index} = OID.build_table_index("test", {:variable_string})
assert index == [4, 116, 101, 115, 116]
end
test "rejects invalid string length for fixed-length index" do
assert {:error, :invalid_string_length} = OID.build_table_index("test", {:string, 5})
end
end
describe "Validation" do
test "validates correct OIDs" do
assert :ok = OID.valid_oid?([1, 3, 6, 1, 2, 1, 1, 1, 0])
assert :ok = OID.valid_oid?([0])
assert :ok = OID.valid_oid?([1, 2, 3, 4, 5])
end
test "rejects invalid OIDs" do
assert {:error, :empty_oid} = OID.valid_oid?([])
assert {:error, :invalid_component} = OID.valid_oid?([1, 3, -1, 4])
assert {:error, :invalid_component} = OID.valid_oid?([1, 3, "6", 1])
assert {:error, :invalid_input} = OID.valid_oid?(:not_a_list)
end
test "normalizes different OID formats" do
{:ok, normalized1} = OID.normalize([1, 3, 6, 1])
assert normalized1 == [1, 3, 6, 1]
{:ok, normalized2} = OID.normalize("1.3.6.1")
assert normalized2 == [1, 3, 6, 1]
assert {:error, :invalid_input} = OID.normalize(:invalid)
end
end
describe "Standard OID utilities" do
test "returns standard OID prefixes" do
assert OID.mib_2() == [1, 3, 6, 1, 2, 1]
assert OID.enterprises() == [1, 3, 6, 1, 4, 1]
assert OID.experimental() == [1, 3, 6, 1, 3]
assert OID.private() == [1, 3, 6, 1, 4]
end
test "identifies MIB-2 OIDs" do
assert OID.mib_2?([1, 3, 6, 1, 2, 1]) == true
assert OID.mib_2?([1, 3, 6, 1, 2, 1, 1, 1, 0]) == true
assert OID.mib_2?([1, 3, 6, 1, 4, 1, 9]) == false
end
test "identifies enterprise OIDs" do
assert OID.enterprise?([1, 3, 6, 1, 4, 1, 9, 1, 1]) == true
assert OID.enterprise?([1, 3, 6, 1, 2, 1, 1, 1, 0]) == false
end
test "identifies experimental OIDs" do
assert OID.experimental?([1, 3, 6, 1, 3, 1]) == true
assert OID.experimental?([1, 3, 6, 1, 2, 1]) == false
end
test "identifies private OIDs" do
assert OID.private?([1, 3, 6, 1, 4, 2]) == true
assert OID.private?([1, 3, 6, 1, 2, 1]) == false
end
test "extracts enterprise numbers" do
{:ok, enterprise_num} = OID.get_enterprise_number([1, 3, 6, 1, 4, 1, 9, 1, 1])
assert enterprise_num == 9
{:ok, enterprise_num2} = OID.get_enterprise_number([1, 3, 6, 1, 4, 1, 12_345])
assert enterprise_num2 == 12_345
end
test "rejects non-enterprise OIDs for enterprise number extraction" do
assert {:error, :not_enterprise_oid} = OID.get_enterprise_number([1, 3, 6, 1, 2, 1])
# Too short
assert {:error, :not_enterprise_oid} = OID.get_enterprise_number([1, 3, 6, 1, 4, 1])
end
end
describe "Error handling and edge cases" do
test "handles invalid inputs gracefully" do
assert {:error, :invalid_input} = OID.string_to_list(:not_binary)
assert {:error, :invalid_input} = OID.list_to_string(:not_list)
assert OID.child_of?(:not_list, [1, 2, 3]) == false
assert OID.child_of?([1, 2, 3], :not_list) == false
assert {:error, :invalid_input} = OID.get_parent(:not_list)
end
test "handles whitespace in OID strings" do
{:ok, oid} = OID.string_to_list(" 1.3.6.1.2.1 ")
assert oid == [1, 3, 6, 1, 2, 1]
{:ok, oid_with_dot} = OID.string_to_list(" .1.3.6.1.2.1 ")
assert oid_with_dot == [1, 3, 6, 1, 2, 1]
end
test "handles very long OIDs" do
long_oid = [1, 3, 6, 1] ++ Enum.to_list(1..100)
{:ok, oid_string} = OID.list_to_string(long_oid)
{:ok, parsed_oid} = OID.string_to_list(oid_string)
assert parsed_oid == long_oid
end
test "handles zero components in OID" do
oid_with_zeros = [1, 3, 0, 1, 0, 0, 1]
{:ok, oid_string} = OID.list_to_string(oid_with_zeros)
{:ok, parsed_oid} = OID.string_to_list(oid_string)
assert parsed_oid == oid_with_zeros
end
end
describe "Performance and stress testing" do
test "handles many OID operations efficiently" do
# Generate 1000 OIDs
oids =
for i <- 1..1000 do
[1, 3, 6, 1, 4, 1, 9999, i]
end
# Test sorting performance
start_time = System.monotonic_time(:microsecond)
sorted_oids = OID.sort(oids)
end_time = System.monotonic_time(:microsecond)
# Should complete in reasonable time (< 500ms)
assert end_time - start_time < 500_000
assert length(sorted_oids) == 1000
end
test "handles concurrent OID operations" do
# Test thread safety
tasks =
for i <- 1..50 do
Task.async(fn ->
oid = [1, 3, 6, 1, 4, 1, 9999, i]
{:ok, oid_string} = OID.list_to_string(oid)
{:ok, parsed_oid} = OID.string_to_list(oid_string)
{i, oid, parsed_oid}
end)
end
results = Task.await_many(tasks, 1000)
# Verify all operations completed successfully
assert length(results) == 50
for {i, original_oid, parsed_oid} <- results do
assert original_oid == parsed_oid
assert List.last(original_oid) == i
end
end
end
end