towerops/test/snmpkit/oid_architecture_validation_test.exs
2026-01-23 12:52:17 -06:00

303 lines
11 KiB
Elixir

defmodule SnmpKit.OidArchitectureValidationTest do
@moduledoc """
Tests to validate that the OID handling architecture follows the established principles:
1. All internal OID handling uses lists of integers [1,3,6,1,2,1,1,1,0]
2. All external APIs accept both string "1.3.6.1.2.1.1.1.0" and list [1,3,6,1,2,1,1,1,0] formats
3. Conversion happens only at API boundaries
4. No unnecessary string/list conversions in internal operations
"""
use ExUnit.Case
alias SnmpKit.SnmpLib.OID
alias SnmpKit.SnmpMgr.Core
alias SnmpKit.SnmpMgr.Format
alias SnmpKit.SnmpMgr.MIB
doctest SnmpKit
describe "Core API Entry Points" do
test "parse_oid function always returns lists" do
# Test string input
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
Core.parse_oid("1.3.6.1.2.1.1.1.0")
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
Core.parse_oid(".1.3.6.1.2.1.1.1.0")
# Test list input - should validate and return same list
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
Core.parse_oid([1, 3, 6, 1, 2, 1, 1, 1, 0])
# Test empty cases get proper fallbacks
assert {:ok, [1, 3]} = Core.parse_oid("")
assert {:ok, [1, 3]} = Core.parse_oid([])
# Test invalid cases
assert {:error, :invalid_oid_input} = Core.parse_oid(123)
assert {:error, :invalid_oid_input} = Core.parse_oid(nil)
end
test "API functions accept both string and list OID formats" do
# These would normally make network calls, but we're testing the input validation
# The functions should not crash on valid OID formats
string_oid = "1.3.6.1.2.1.1.1.0"
list_oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
# Test that both formats are accepted (they will fail with network errors, but not format errors)
result1 = SnmpKit.SnmpMgr.get("invalid.host", string_oid, timeout: 100)
result2 = SnmpKit.SnmpMgr.get("invalid.host", list_oid, timeout: 100)
# Both should fail with network errors, not format errors
assert match?({:error, _}, result1)
assert match?({:error, _}, result2)
# Neither should fail with format-specific errors
refute match?({:error, :invalid_oid_format}, result1)
refute match?({:error, :invalid_oid_format}, result2)
refute match?({:error, :invalid_oid_input}, result1)
refute match?({:error, :invalid_oid_input}, result2)
end
end
describe "Internal Consistency" do
test "OID validation uses list format internally" do
# Test the OID validation functions work with lists
assert :ok = OID.valid_oid?([1, 3, 6, 1, 2, 1, 1, 1, 0])
assert {:error, :empty_oid} = OID.valid_oid?([])
assert {:error, :invalid_component} = OID.valid_oid?([1, -1, 3])
assert {:error, :invalid_input} = OID.valid_oid?("string")
end
test "OID normalization returns lists consistently" do
# Test the normalize function in OID module
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
OID.normalize("1.3.6.1.2.1.1.1.0")
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
OID.normalize([1, 3, 6, 1, 2, 1, 1, 1, 0])
# Empty cases return error for OID module (but get fallback in parse_oid)
assert {:error, :empty_oid} = OID.normalize("")
assert {:error, :empty_oid} = OID.normalize([])
end
test "bulk operations maintain list format internally" do
# Test that bulk resolve_oid function returns lists
test_cases = [
{"1.3.6.1.2.1.1.1.0", [1, 3, 6, 1, 2, 1, 1, 1, 0]},
{[1, 3, 6, 1, 2, 1, 1, 1, 0], [1, 3, 6, 1, 2, 1, 1, 1, 0]},
{"", [1, 3]},
{[], [1, 3]}
]
Enum.each(test_cases, fn {input, expected} ->
# Access the private resolve_oid function through a test helper pattern
# This validates internal consistency without exposing private APIs
case Core.parse_oid(input) do
{:ok, result} ->
assert result == expected,
"resolve_oid(#{inspect(input)}) should return #{inspect(expected)}, got #{inspect(result)}"
error ->
flunk("resolve_oid(#{inspect(input)}) returned error: #{inspect(error)}")
end
end)
end
end
describe "API Boundaries and Conversion" do
test "string-to-list conversion happens at entry points" do
# Test OID string parsing
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
OID.string_to_list("1.3.6.1.2.1.1.1.0")
assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]} =
OID.string_to_list(".1.3.6.1.2.1.1.1.0")
# Test edge cases
assert {:error, :empty_oid} = OID.string_to_list("")
assert {:error, :invalid_oid_string} = OID.string_to_list("1.3.6.x")
assert {:error, :invalid_input} = OID.string_to_list(123)
end
test "list-to-string conversion for output only" do
# Test OID list to string conversion
assert {:ok, "1.3.6.1.2.1.1.1.0"} =
OID.list_to_string([1, 3, 6, 1, 2, 1, 1, 1, 0])
# Test edge cases
assert {:error, :empty_oid} = OID.list_to_string([])
assert {:error, :invalid_component} = OID.list_to_string([1, -1, 3])
assert {:error, :invalid_input} = OID.list_to_string("string")
end
test "format functions handle mixed OID value types correctly" do
# Test that format functions handle object_identifier values correctly
# Note: pretty_print expects string OIDs as input (final output format)
test_cases = [
{{"1.3.6.1.2.1.1.1.0", :object_identifier, [1, 3, 6, 1, 4, 1, 9]}, "1.3.6.1.4.1.9"},
{{"1.3.6.1.2.1.1.2.0", :object_identifier, "1.3.6.1.4.1.9"}, "1.3.6.1.4.1.9"},
{{"1.3.6.1.2.1.1.3.0", :integer, 12_345}, "12345"}
]
Enum.each(test_cases, fn {input_tuple, expected_formatted_value} ->
{oid_string, type, formatted_value} = Format.pretty_print(input_tuple)
# OID should be string in output
assert is_binary(oid_string), "OID in output should be string format"
# Type should be preserved
{_, original_type, _} = input_tuple
assert type == original_type, "Type should be preserved"
# Value formatting should handle both list and string object identifiers
if original_type == :object_identifier do
assert formatted_value == expected_formatted_value,
"Object identifier should be formatted as string: #{inspect(formatted_value)}"
end
end)
end
end
describe "Error Handling and Validation" do
test "invalid OID inputs are properly rejected" do
invalid_inputs = [
"1.3.6.invalid",
"1.3.6.-1",
[1, 3, -1],
[1, 3, "invalid"],
123,
nil,
%{},
{:invalid}
]
Enum.each(invalid_inputs, fn invalid_input ->
result = Core.parse_oid(invalid_input)
assert match?({:error, _}, result),
"Invalid input #{inspect(invalid_input)} should be rejected"
end)
end
test "empty OID cases have consistent fallbacks" do
# Only test cases that actually get fallback treatment in parse_oid
empty_cases = ["", [], " "]
Enum.each(empty_cases, fn empty_input ->
case Core.parse_oid(empty_input) do
{:ok, [1, 3]} ->
# Expected fallback
:ok
{:ok, other} ->
flunk("Empty input #{inspect(empty_input)} should fallback to [1, 3], got #{inspect(other)}")
{:error, reason} ->
flunk("Empty input #{inspect(empty_input)} should not error, got #{inspect(reason)}")
end
end)
# "." is treated as an invalid OID string, not empty
assert {:error, _} = Core.parse_oid(".")
end
test "type information is preserved throughout operations" do
# This test validates that our architecture preserves SNMP type information
# Note: Format functions expect string OIDs (final output format)
mock_snmp_results = [
{"1.3.6.1.2.1.1.1.0", :octet_string, "System Description"},
{"1.3.6.1.2.1.1.2.0", :object_identifier, [1, 3, 6, 1, 4, 1, 9]},
{"1.3.6.1.2.1.1.3.0", :timeticks, 123_456}
]
# Test that formatting preserves type information
formatted_results =
Enum.map(mock_snmp_results, fn result ->
Format.pretty_print(result)
end)
# Every result should have 3-tuple format with preserved type
mock_snmp_results
|> Enum.zip(formatted_results)
|> Enum.each(fn {{_, original_type, _}, {_, formatted_type, _}} ->
assert formatted_type == original_type,
"Type information should be preserved: #{original_type} != #{formatted_type}"
end)
# OIDs should be converted to strings in final output
Enum.each(formatted_results, fn {oid_string, _type, _value} ->
assert is_binary(oid_string), "Final output OID should be string format"
assert String.match?(oid_string, ~r/^\d+(\.\d+)*$/),
"OID string should be valid dotted decimal format"
end)
end
end
describe "Performance and Efficiency" do
test "no unnecessary conversions in processing chain" do
# Test that we don't do redundant string<->list conversions
# This is more of a design validation than a direct test
list_oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
# If we start with a list, parse_oid should not do unnecessary conversions
assert {:ok, result} = Core.parse_oid(list_oid)
assert result == list_oid, "List input should return same list without conversion"
# The result should be the same object (no unnecessary copying)
# Note: This is more about design efficiency than strict object identity
end
end
describe "Integration Consistency" do
test "MIB operations maintain OID list format" do
# Test MIB reverse lookup expects lists
test_oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
# This will return an error since no MIBs are loaded, but should not fail on format
result = MIB.reverse_lookup(test_oid)
# Should not fail with format errors
refute match?({:error, :invalid_oid_format}, result)
# String version should convert to list internally
string_result = MIB.reverse_lookup("1.3.6.1.2.1.1.1.0")
refute match?({:error, :invalid_oid_format}, string_result)
end
test "walk operations use consistent OID formats" do
# Test that internal walk functions expect list OIDs
# We can't test actual walks without network, but we can test OID resolution
walk_test_cases = [
"1.3.6.1.2.1.1",
"system",
[1, 3, 6, 1, 2, 1, 1],
""
]
Enum.each(walk_test_cases, fn test_input ->
# All these should be resolved to lists internally
case Core.parse_oid(test_input) do
{:ok, result} ->
assert is_list(result),
"Walk OID resolution should return lists: #{inspect(test_input)} -> #{inspect(result)}"
assert Enum.all?(result, &is_integer/1),
"All OID components should be integers: #{inspect(result)}"
{:error, _} ->
# Some inputs may be invalid, that's expected
:ok
end
end)
end
end
end