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