more tests

This commit is contained in:
Graham McIntire 2026-02-03 10:28:39 -06:00
parent 1ce3763e5f
commit 13b3810010
No known key found for this signature in database
5 changed files with 1890 additions and 0 deletions

View file

@ -0,0 +1,517 @@
defmodule SnmpKit.SnmpMgr.BulkTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpMgr.Bulk
describe "get_bulk/3 version validation" do
test "rejects SNMPv1 with error" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1)
assert {:error, {:unsupported_operation, :get_bulk_requires_v2c}} = result
end
test "rejects SNMPv3 with error" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v3)
assert {:error, {:unsupported_operation, :get_bulk_requires_v2c}} = result
end
test "accepts SNMPv2c (explicit)" do
# Will timeout or error without proper SNMP infrastructure
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v2c, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "defaults to SNMPv2c when version not specified" do
# Should use v2c by default
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "get_bulk/3 OID handling" do
test "accepts single OID as list" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts single OID as string" do
result = Bulk.get_bulk("192.168.1.1", "1.3.6.1.2.1.1", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts multiple OIDs as list" do
result = Bulk.get_bulk("192.168.1.1", [[1, 3, 6, 1, 2, 1, 1], [1, 3, 6, 1, 2, 1, 2]], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "converts single OID to list internally" do
# Single OID should be wrapped in a list
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "returns error for invalid OID" do
result = Bulk.get_bulk("192.168.1.1", "invalid.oid", timeout: 100)
assert {:error, _} = result
end
end
describe "get_bulk/3 options handling" do
test "uses default max_repetitions when not specified" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
# Default is 30
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses specified max_repetitions" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], max_repetitions: 10, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses default non_repeaters when not specified" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
# Default is 0
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses specified non_repeaters" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], non_repeaters: 1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "forces version to v2c even if different version passed" do
# Should override any other version setting
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts community string option" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], community: "private", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts port option" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], port: 1161, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts timeout option" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 5000)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "get_table_bulk/3" do
test "retrieves table using GETBULK" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts string table OID" do
result = Bulk.get_table_bulk("192.168.1.1", "1.3.6.1.2.1.2.2", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses default max_entries when not specified" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100)
# Default is 1000
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "respects max_entries option" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], max_entries: 100, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "respects max_repetitions option" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], max_repetitions: 20, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "returns error for invalid table OID" do
result = Bulk.get_table_bulk("192.168.1.1", "invalid.table", timeout: 100)
assert {:error, _} = result
end
test "accepts community string" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], community: "public", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "walk_bulk/3" do
test "performs bulk walk of subtree" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts string root OID" do
result = Bulk.walk_bulk("192.168.1.1", "1.3.6.1.2.1.1", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses default max_entries when not specified" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
# Default is 1000
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "respects max_entries option" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], max_entries: 50, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "respects max_repetitions option" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], max_repetitions: 15, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "returns error for invalid root OID" do
result = Bulk.walk_bulk("192.168.1.1", "invalid", timeout: 100)
assert {:error, _} = result
end
test "accepts community string" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], community: "private", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts port option" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], port: 1161, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "get_bulk_multi/2" do
test "performs concurrent GETBULK operations" do
requests = [
{"192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0]},
{"192.168.1.2", [1, 3, 6, 1, 2, 1, 1, 2, 0]},
{"192.168.1.3", [1, 3, 6, 1, 2, 1, 1, 3, 0]}
]
results = Bulk.get_bulk_multi(requests, timeout: 100)
assert is_list(results)
assert length(results) == 3
# Each result is either {:ok, _} or {:error, _}
assert Enum.all?(results, fn result ->
match?({:ok, _}, result) or match?({:error, _}, result)
end)
end
test "handles empty request list" do
results = Bulk.get_bulk_multi([], timeout: 100)
assert results == []
end
test "handles single request" do
requests = [{"192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0]}]
results = Bulk.get_bulk_multi(requests, timeout: 100)
assert length(results) == 1
end
test "returns timeout error for tasks that don't complete" do
requests = [
{"192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0]},
{"192.168.1.2", [1, 3, 6, 1, 2, 1, 1, 2, 0]}
]
# Very short timeout to force timeout
results = Bulk.get_bulk_multi(requests, timeout: 1)
assert length(results) == 2
# At least some should timeout
assert Enum.any?(results, fn result ->
match?({:error, :timeout}, result)
end)
end
test "uses default timeout when not specified" do
requests = [{"192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0]}]
results = Bulk.get_bulk_multi(requests)
assert length(results) == 1
end
test "passes options to individual requests" do
requests = [
{"192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0]},
{"192.168.1.2", [1, 3, 6, 1, 2, 1, 1, 2, 0]}
]
results = Bulk.get_bulk_multi(requests, community: "public", max_repetitions: 10, timeout: 100)
assert length(results) == 2
end
test "handles task failures gracefully" do
requests = [
{"192.168.1.1", "invalid.oid"},
{"192.168.1.2", [1, 3, 6, 1, 2, 1, 1, 2, 0]}
]
results = Bulk.get_bulk_multi(requests, timeout: 100)
assert length(results) == 2
# First should error due to invalid OID
assert match?({:error, _}, Enum.at(results, 0))
end
test "accepts string OIDs in requests" do
requests = [
{"192.168.1.1", "1.3.6.1.2.1.1.1.0"},
{"192.168.1.2", "1.3.6.1.2.1.1.2.0"}
]
results = Bulk.get_bulk_multi(requests, timeout: 100)
assert length(results) == 2
end
test "accepts mixed OID formats in requests" do
requests = [
{"192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0]},
{"192.168.1.2", "1.3.6.1.2.1.1.2.0"}
]
results = Bulk.get_bulk_multi(requests, timeout: 100)
assert length(results) == 2
end
end
describe "target formats" do
test "accepts IP address as target" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts hostname as target" do
result = Bulk.get_bulk("device.local", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts target with port" do
result = Bulk.get_bulk("192.168.1.1:161", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "error handling" do
test "handles network timeout" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 1)
assert match?({:error, _}, result)
end
test "handles invalid target" do
result = Bulk.get_bulk("999.999.999.999", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result)
end
test "handles unreachable target" do
result = Bulk.get_bulk("192.168.255.254", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result)
end
test "returns error for malformed options" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], max_repetitions: -1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "edge cases" do
test "handles empty table results" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 999], timeout: 100)
# Empty table should return {:ok, []}
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles max_entries limit reached" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], max_entries: 1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles zero max_entries" do
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], max_entries: 0, timeout: 100)
# Zero max_entries should return empty results immediately
assert match?({:ok, _}, result) or match?({:error, _}, result)
end
test "handles very large max_repetitions" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], max_repetitions: 1000, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles zero max_repetitions" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], max_repetitions: 0, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles single OID result" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0], max_repetitions: 1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "get_bulk/3 with non_repeaters" do
test "uses non_repeaters for multiple OIDs" do
# When retrieving multiple OIDs, non_repeaters controls how many are scalar
result =
Bulk.get_bulk("192.168.1.1", [[1, 3, 6, 1, 2, 1, 1], [1, 3, 6, 1, 2, 1, 2]], non_repeaters: 1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "defaults non_repeaters to 0" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
# Should use non_repeaters: 0 by default
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts large non_repeaters value" do
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], non_repeaters: 100, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "walk_bulk/3 vs get_table_bulk/3" do
test "walk_bulk and get_table_bulk produce similar results for tables" do
# Both should work for walking tables
walk_result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100)
table_result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100)
# Both should either succeed or fail, but with same error type
assert (match?({:ok, _}, walk_result) and match?({:ok, _}, table_result)) or
(match?({:error, _}, walk_result) and match?({:error, _}, table_result))
end
test "walk_bulk works for non-table subtrees" do
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "multiple options combinations" do
test "accepts all options together" do
opts = [
version: :v2c,
community: "public",
port: 161,
timeout: 5000,
max_repetitions: 20,
non_repeaters: 0,
max_entries: 500
]
result = Bulk.walk_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], opts)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "get_bulk with all options" do
opts = [
community: "private",
port: 1161,
timeout: 3000,
max_repetitions: 15,
non_repeaters: 1
]
result = Bulk.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], opts)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "get_table_bulk with all options" do
opts = [
community: "public",
port: 161,
timeout: 2000,
max_repetitions: 25,
max_entries: 100
]
result = Bulk.get_table_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], opts)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "get_bulk_multi/2 with various request counts" do
test "handles large number of concurrent requests" do
requests =
for i <- 1..20 do
{"192.168.1.#{i}", [1, 3, 6, 1, 2, 1, 1, 1, 0]}
end
results = Bulk.get_bulk_multi(requests, timeout: 100)
assert length(results) == 20
end
test "all concurrent requests timeout consistently" do
requests =
for i <- 1..5 do
{"192.168.255.#{i}", [1, 3, 6, 1, 2, 1, 1, 1, 0]}
end
results = Bulk.get_bulk_multi(requests, timeout: 10)
assert length(results) == 5
# All should timeout or error
assert Enum.all?(results, fn result -> match?({:error, _}, result) end)
end
end
end

View file

@ -0,0 +1,447 @@
defmodule SnmpKit.SnmpMgr.ErrorsTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpMgr.Errors
describe "code_to_atom/1" do
test "converts standard SNMP error codes to atoms" do
assert Errors.code_to_atom(0) == :no_error
assert Errors.code_to_atom(1) == :too_big
assert Errors.code_to_atom(2) == :no_such_name
assert Errors.code_to_atom(3) == :bad_value
assert Errors.code_to_atom(4) == :read_only
assert Errors.code_to_atom(5) == :gen_err
end
test "converts SNMPv2c error codes to atoms" do
assert Errors.code_to_atom(6) == :no_access
assert Errors.code_to_atom(7) == :wrong_type
assert Errors.code_to_atom(8) == :wrong_length
assert Errors.code_to_atom(9) == :wrong_encoding
assert Errors.code_to_atom(10) == :wrong_value
assert Errors.code_to_atom(11) == :no_creation
assert Errors.code_to_atom(12) == :inconsistent_value
assert Errors.code_to_atom(13) == :resource_unavailable
assert Errors.code_to_atom(14) == :commit_failed
assert Errors.code_to_atom(15) == :undo_failed
assert Errors.code_to_atom(16) == :authorization_error
assert Errors.code_to_atom(17) == :not_writable
assert Errors.code_to_atom(18) == :inconsistent_name
end
test "returns :unknown_error for invalid codes" do
assert Errors.code_to_atom(999) == :unknown_error
assert Errors.code_to_atom(-1) == :unknown_error
end
end
describe "description/1" do
test "provides descriptions for standard errors" do
assert Errors.description(:no_error) == "No error occurred"
assert Errors.description(:too_big) == "Response too big to fit in message"
assert Errors.description(:no_such_name) == "Variable name not found"
assert Errors.description(:bad_value) == "Invalid value for variable"
assert Errors.description(:read_only) == "Variable is read-only"
assert Errors.description(:gen_err) == "General error"
end
test "provides descriptions for SNMPv2c errors" do
assert Errors.description(:no_access) == "Access denied"
assert Errors.description(:wrong_type) == "Wrong data type"
assert Errors.description(:authorization_error) == "Authorization failed"
assert Errors.description(:not_writable) == "Variable not writable"
end
test "returns 'Unknown error' for unknown atoms" do
assert Errors.description(:unknown_error) == "Unknown error"
assert Errors.description(:some_random_atom) == "Unknown error"
end
end
describe "code_to_description/1" do
test "converts code directly to description" do
assert Errors.code_to_description(2) == "Variable name not found"
assert Errors.code_to_description(18) == "Inconsistent name"
assert Errors.code_to_description(5) == "General error"
end
test "handles unknown codes" do
assert Errors.code_to_description(999) == "Unknown error"
end
end
describe "v2c_error?/1" do
test "returns true for SNMPv2c-specific errors" do
assert Errors.v2c_error?(:no_access) == true
assert Errors.v2c_error?(:wrong_type) == true
assert Errors.v2c_error?(:wrong_length) == true
assert Errors.v2c_error?(:authorization_error) == true
assert Errors.v2c_error?(:not_writable) == true
end
test "returns false for SNMPv1 errors" do
assert Errors.v2c_error?(:no_such_name) == false
assert Errors.v2c_error?(:too_big) == false
assert Errors.v2c_error?(:bad_value) == false
assert Errors.v2c_error?(:read_only) == false
assert Errors.v2c_error?(:gen_err) == false
end
test "returns false for unknown errors" do
assert Errors.v2c_error?(:unknown_error) == false
assert Errors.v2c_error?(:timeout) == false
end
end
describe "format_error/1" do
test "formats SNMP errors with integer codes" do
result = Errors.format_error({:snmp_error, 2})
assert result == "SNMP Error (2): Variable name not found"
end
test "formats SNMP errors with atom codes" do
result = Errors.format_error({:snmp_error, :no_such_name})
assert result == "SNMP Error: Variable name not found"
end
test "formats SNMPv2c errors without details" do
result = Errors.format_error({:v2c_error, :no_access})
assert result == "SNMPv2c Error: Access denied"
end
test "formats SNMPv2c errors with details" do
result = Errors.format_error({:v2c_error, :no_access, oid: "1.2.3.4"})
assert result == "SNMPv2c Error: Access denied (OID: 1.2.3.4)"
end
test "formats SNMPv2c errors with multiple details" do
result = Errors.format_error({:v2c_error, :wrong_type, [oid: "1.2.3.4", value: "test"]})
assert result =~ "SNMPv2c Error: Wrong data type"
assert result =~ "OID: 1.2.3.4"
assert result =~ "Value: \"test\""
end
test "formats network errors" do
result = Errors.format_error({:network_error, :timeout})
assert result == "Network Error: :timeout"
end
test "formats timeout errors" do
result = Errors.format_error({:timeout, nil})
assert result == "Timeout Error: Request timed out"
end
test "formats encoding errors" do
result = Errors.format_error({:encoding_error, "bad data"})
assert result == "Encoding Error: \"bad data\""
end
test "formats decoding errors" do
result = Errors.format_error({:decoding_error, "corrupt packet"})
assert result == "Decoding Error: \"corrupt packet\""
end
test "formats unknown errors" do
result = Errors.format_error(:some_random_error)
assert result == "Unknown Error: :some_random_error"
end
end
describe "recoverable?/1" do
test "returns false for unrecoverable network errors" do
assert Errors.recoverable?({:network_error, :host_unreachable}) == false
assert Errors.recoverable?({:network_error, :network_unreachable}) == false
end
test "returns false for permanent SNMP errors" do
assert Errors.recoverable?({:snmp_error, :no_such_name}) == false
assert Errors.recoverable?({:snmp_error, :bad_value}) == false
assert Errors.recoverable?({:snmp_error, :read_only}) == false
end
test "returns false for permanent SNMPv2c errors" do
assert Errors.recoverable?({:v2c_error, :no_access}) == false
assert Errors.recoverable?({:v2c_error, :not_writable}) == false
assert Errors.recoverable?({:v2c_error, :wrong_type}) == false
end
test "returns true for recoverable errors" do
assert Errors.recoverable?(:timeout) == true
assert Errors.recoverable?({:snmp_error, :too_big}) == true
assert Errors.recoverable?({:snmp_error, :gen_err}) == true
end
test "returns false for unknown errors" do
assert Errors.recoverable?(:unknown_error) == false
assert Errors.recoverable?({:custom_error, "test"}) == false
end
end
describe "analyze_error/1" do
test "analyzes SNMP protocol errors with integer codes" do
result = Errors.analyze_error({:snmp_error, 2})
assert result.type == :snmp_protocol
assert result.atom == :no_such_name
assert result.code == 2
assert result.severity == :error
assert result.retriable == false
assert result.category == :user_error
assert result.description == "Variable name not found"
end
test "analyzes SNMP protocol errors with atom codes" do
result = Errors.analyze_error({:snmp_error, :too_big})
assert result.type == :snmp_protocol
assert result.atom == :too_big
assert result.code == 1
assert result.retriable == true
assert result.category == :resource_error
end
test "analyzes network timeout errors" do
result = Errors.analyze_error(:timeout)
assert result.type == :network
assert result.atom == :timeout
assert result.retriable == true
assert result.category == :transient_error
assert result.description == "Operation timed out"
end
test "analyzes host unreachable errors" do
result = Errors.analyze_error(:host_unreachable)
assert result.type == :network
assert result.atom == :host_unreachable
assert result.retriable == false
assert result.category == :configuration_error
end
test "analyzes connection refused errors" do
result = Errors.analyze_error(:connection_refused)
assert result.type == :network
assert result.retriable == false
assert result.category == :service_error
end
test "analyzes unknown errors" do
result = Errors.analyze_error({:custom_error, "test"})
assert result.type == :other
assert result.retriable == false
# classify_error returns a tuple for unknown errors, but as subcategory
assert result.category == :unknown_error
assert result.subcategory == {:custom_error, "test"}
end
end
describe "classify_error/2" do
test "classifies user errors" do
assert Errors.classify_error({:snmp_error, :no_such_name}) == {:user_error, :invalid_oid}
assert Errors.classify_error({:snmp_error, :bad_value}) == {:user_error, :invalid_value}
assert Errors.classify_error({:snmp_error, :read_only}) == {:user_error, :write_to_readonly}
assert Errors.classify_error({:v2c_error, :wrong_type}) == {:user_error, :type_mismatch}
end
test "classifies security errors" do
assert Errors.classify_error({:v2c_error, :no_access}) == {:security_error, :access_denied}
assert Errors.classify_error({:v2c_error, :authorization_error}) ==
{:security_error, :authorization_failed}
end
test "classifies device errors" do
assert Errors.classify_error({:snmp_error, :gen_err}) == {:device_error, :general_failure}
assert Errors.classify_error({:v2c_error, :resource_unavailable}) ==
{:device_error, :resource_exhausted}
end
test "classifies recoverable errors" do
assert Errors.classify_error({:snmp_error, :too_big}) ==
{:recoverable_error, :response_too_large}
end
test "classifies network errors" do
assert Errors.classify_error({:network_error, :timeout}) ==
{:transient_error, :network_timeout}
assert Errors.classify_error({:network_error, :host_unreachable}) ==
{:configuration_error, :unreachable_host}
assert Errors.classify_error({:network_error, :network_unreachable}) ==
{:configuration_error, :network_unavailable}
end
test "classifies protocol errors" do
assert Errors.classify_error({:encoding_error, "bad data"}) ==
{:protocol_error, :message_encoding_failed}
assert Errors.classify_error({:decoding_error, "corrupt"}) ==
{:protocol_error, :message_decoding_failed}
end
test "classifies validation errors" do
assert Errors.classify_error(:invalid_oid_values) == :validation_error
assert Errors.classify_error({:snmp_encoding_error, "test"}) == :validation_error
assert Errors.classify_error(:encoding_failed) == :validation_error
end
test "classifies authentication errors" do
assert Errors.classify_error(:authentication_error) == :authentication_error
assert Errors.classify_error(:invalid_community) == :invalid_community
assert Errors.classify_error(:bad_community) == :invalid_community
end
test "classifies system errors" do
assert Errors.classify_error(:snmp_modules_not_available) == :system_error
end
test "classifies timeout errors" do
assert Errors.classify_error(:timeout) == {:transient_error, :operation_timeout}
end
test "accepts optional context parameter" do
# Context doesn't change behavior but is accepted
assert Errors.classify_error({:snmp_error, :no_such_name}, "Get request") ==
{:user_error, :invalid_oid}
end
test "classifies unknown errors" do
assert Errors.classify_error(:unknown_atom) == {:unknown_error, :unknown_atom}
assert Errors.classify_error({:custom, "test"}) == {:unknown_error, {:custom, "test"}}
end
end
describe "format_user_friendly_error/2" do
test "formats SNMP errors with context" do
assert Errors.format_user_friendly_error({:snmp_error, :no_such_name}, "Getting sysDescr") ==
"Getting sysDescr failed: The requested OID does not exist on the device"
assert Errors.format_user_friendly_error({:snmp_error, :bad_value}, "Setting value") ==
"Setting value failed: The provided value is invalid for this OID"
end
test "formats network errors with context" do
assert Errors.format_user_friendly_error({:network_error, :timeout}, "Polling device") ==
"Polling device failed: The device did not respond within the timeout period"
assert Errors.format_user_friendly_error(
{:network_error, :host_unreachable},
"Connecting"
) ==
"Connecting failed: Cannot reach the device. Check the IP address and network connectivity"
end
test "formats timeout errors" do
assert Errors.format_user_friendly_error(:timeout, "Discovery") ==
"Discovery timed out: The operation took too long to complete"
end
test "uses default context when not provided" do
assert Errors.format_user_friendly_error({:snmp_error, :no_such_name}) ==
"Operation failed: The requested OID does not exist on the device"
end
test "formats encoding/decoding errors" do
assert Errors.format_user_friendly_error({:encoding_error, "test"}, "Sending request") ==
"Sending request failed: Unable to encode the SNMP message"
assert Errors.format_user_friendly_error({:decoding_error, "test"}, "Reading response") ==
"Reading response failed: Unable to decode the SNMP response"
end
test "formats unknown errors" do
result = Errors.format_user_friendly_error({:custom_error, "test"}, "Testing")
assert result =~ "Testing failed:"
end
end
describe "get_recovery_suggestions/1" do
test "provides suggestions for no_such_name errors" do
suggestions = Errors.get_recovery_suggestions({:snmp_error, :no_such_name})
assert "Verify the OID is correct" in suggestions
assert "Check if the OID is supported by this device" in suggestions
assert "Try using MIB browser to explore available OIDs" in suggestions
assert "Ensure you're using the correct SNMP version" in suggestions
end
test "provides suggestions for bad_value errors" do
suggestions = Errors.get_recovery_suggestions({:snmp_error, :bad_value})
assert "Check the value format and type" in suggestions
assert "Verify the value is within acceptable range" in suggestions
end
test "provides suggestions for read_only errors" do
suggestions = Errors.get_recovery_suggestions({:snmp_error, :read_only})
assert "This OID cannot be modified" in suggestions
assert "Use GET operation instead of SET" in suggestions
end
test "provides suggestions for too_big errors" do
suggestions = Errors.get_recovery_suggestions({:snmp_error, :too_big})
assert "Reduce the number of OIDs in the request" in suggestions
assert "Use GETBULK operation with smaller max-repetitions" in suggestions
assert "Split the request into multiple smaller requests" in suggestions
end
test "provides suggestions for access errors" do
suggestions = Errors.get_recovery_suggestions({:v2c_error, :no_access})
assert "Verify the community string is correct" in suggestions
assert "Check device SNMP access configuration" in suggestions
end
test "provides suggestions for timeout errors" do
suggestions = Errors.get_recovery_suggestions({:network_error, :timeout})
assert "Increase timeout value" in suggestions
assert "Check network connectivity to the device" in suggestions
assert "Verify device is responding to ping" in suggestions
end
test "provides suggestions for host unreachable errors" do
suggestions = Errors.get_recovery_suggestions({:network_error, :host_unreachable})
assert "Verify the IP address is correct" in suggestions
assert "Check network routing" in suggestions
assert "Test basic connectivity with ping" in suggestions
end
test "provides suggestions for encoding errors" do
suggestions = Errors.get_recovery_suggestions({:encoding_error, "test"})
assert "Check OID format" in suggestions
assert "Verify value types are supported" in suggestions
end
test "provides suggestions for decoding errors" do
suggestions = Errors.get_recovery_suggestions({:decoding_error, "test"})
assert "Device may not support SNMP" in suggestions
assert "Check SNMP version compatibility" in suggestions
end
test "provides suggestions for timeout atom" do
suggestions = Errors.get_recovery_suggestions(:timeout)
assert "Increase operation timeout" in suggestions
assert "Check if operation is too complex" in suggestions
end
test "provides generic suggestions for unknown errors" do
suggestions = Errors.get_recovery_suggestions({:custom_error, "test"})
assert "Check device documentation" in suggestions
assert "Verify SNMP configuration" in suggestions
assert "Contact device vendor support" in suggestions
end
end
end

View file

@ -0,0 +1,436 @@
defmodule SnmpKit.SnmpMgr.TypesTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpMgr.Types
describe "encode_value/2 with automatic type inference" do
test "infers string type for printable binary" do
assert {:ok, {:string, "Hello World"}} = Types.encode_value("Hello World")
end
test "infers string type for empty string" do
assert {:ok, {:string, ""}} = Types.encode_value("")
end
test "infers octetString for non-printable binary" do
binary = <<0, 1, 2, 3>>
assert {:ok, {:octetString, ^binary}} = Types.encode_value(binary)
end
test "infers integer type for negative numbers" do
assert {:ok, {:integer, -42}} = Types.encode_value(-42)
end
test "infers unsigned32 for positive integers under 2^32" do
assert {:ok, {:unsigned32, 1000}} = Types.encode_value(1000)
assert {:ok, {:unsigned32, 4_294_967_295}} = Types.encode_value(4_294_967_295)
end
test "infers counter64 for large positive integers" do
assert {:ok, {:counter64, 4_294_967_296}} = Types.encode_value(4_294_967_296)
assert {:ok, {:counter64, 9_999_999_999}} = Types.encode_value(9_999_999_999)
end
test "infers objectIdentifier for integer lists" do
assert {:ok, {:objectIdentifier, [1, 3, 6, 1]}} = Types.encode_value([1, 3, 6, 1])
end
test "infers string for non-integer lists" do
assert {:ok, {:string, _}} = Types.encode_value([1, 2, "a"])
end
test "infers boolean type" do
assert {:ok, {:boolean, true}} = Types.encode_value(true)
assert {:ok, {:boolean, false}} = Types.encode_value(false)
end
test "infers ipAddress for valid IP tuple" do
assert {:ok, {:ipAddress, {192, 168, 1, 1}}} = Types.encode_value({192, 168, 1, 1})
end
test "infers opaque for invalid IP tuple" do
assert {:ok, {:opaque, {300, 400, 500, 600}}} = Types.encode_value({300, 400, 500, 600})
end
test "infers null for nil" do
assert {:ok, {:null, :null}} = Types.encode_value(nil)
end
test "infers null for nil only" do
assert {:ok, {:null, :null}} = Types.encode_value(nil)
end
test "special atoms require explicit type specification" do
# Special atoms are inferred as :null but encode_with_inferred_type only handles nil
# Use explicit type specification for these
assert {:ok, {:null, :null}} = Types.encode_value(:undefined, type: :null)
assert {:ok, {:null, :null}} = Types.encode_value(:noSuchObject, type: :null)
assert {:ok, {:null, :null}} = Types.encode_value(:noSuchInstance, type: :null)
assert {:ok, {:null, :null}} = Types.encode_value(:endOfMibView, type: :null)
end
test "infers opaque for unknown types" do
value = %{key: "value"}
assert {:ok, {:opaque, ^value}} = Types.encode_value(value)
end
end
describe "encode_value/2 with explicit type specification" do
test "encodes string type explicitly" do
assert {:ok, {:string, "test"}} = Types.encode_value("test", type: :string)
end
test "encodes integer type explicitly" do
assert {:ok, {:integer, 42}} = Types.encode_value(42, type: :integer)
end
test "encodes gauge32 type explicitly" do
assert {:ok, {:gauge32, 100}} = Types.encode_value(100, type: :gauge32)
end
test "encodes counter32 type explicitly" do
assert {:ok, {:counter32, 1000}} = Types.encode_value(1000, type: :counter32)
end
test "encodes counter64 type explicitly" do
assert {:ok, {:counter64, 9_999_999_999}} = Types.encode_value(9_999_999_999, type: :counter64)
end
test "encodes unsigned32 type explicitly" do
assert {:ok, {:unsigned32, 500}} = Types.encode_value(500, type: :unsigned32)
end
test "encodes timeticks type explicitly" do
assert {:ok, {:timeticks, 12_345}} = Types.encode_value(12_345, type: :timeticks)
end
test "encodes ipAddress from string" do
assert {:ok, {:ipAddress, {192, 168, 1, 1}}} =
Types.encode_value("192.168.1.1", type: :ipAddress)
end
test "encodes ipAddress from tuple" do
assert {:ok, {:ipAddress, {10, 0, 0, 1}}} =
Types.encode_value({10, 0, 0, 1}, type: :ipAddress)
end
test "returns error for invalid IP address string" do
assert {:error, {:invalid_ip_address, "999.999.999.999"}} =
Types.encode_value("999.999.999.999", type: :ipAddress)
end
test "encodes objectIdentifier from string" do
assert {:ok, {:objectIdentifier, [1, 3, 6, 1, 2, 1]}} =
Types.encode_value("1.3.6.1.2.1", type: :objectIdentifier)
end
test "encodes objectIdentifier from list" do
assert {:ok, {:objectIdentifier, [1, 3, 6, 1]}} =
Types.encode_value([1, 3, 6, 1], type: :objectIdentifier)
end
test "returns error for invalid OID string" do
assert {:error, _} = Types.encode_value("invalid.oid", type: :objectIdentifier)
end
test "returns error for non-integer OID list" do
assert {:error, _} = Types.encode_value([1, 2, "a"], type: :objectIdentifier)
end
test "encodes octetString type explicitly" do
binary = <<0, 1, 2>>
assert {:ok, {:octetString, ^binary}} = Types.encode_value(binary, type: :octetString)
end
test "encodes boolean type explicitly" do
assert {:ok, {:boolean, true}} = Types.encode_value(true, type: :boolean)
assert {:ok, {:boolean, false}} = Types.encode_value(false, type: :boolean)
end
test "encodes opaque type explicitly" do
value = %{test: "data"}
assert {:ok, {:opaque, ^value}} = Types.encode_value(value, type: :opaque)
end
test "encodes null type explicitly" do
assert {:ok, {:null, :null}} = Types.encode_value(nil, type: :null)
assert {:ok, {:null, :null}} = Types.encode_value("anything", type: :null)
end
test "returns error for unsupported type conversions" do
assert {:error, {:unsupported_type_conversion, "test", :gauge32}} =
Types.encode_value("test", type: :gauge32)
assert {:error, {:unsupported_type_conversion, -1, :counter32}} =
Types.encode_value(-1, type: :counter32)
assert {:error, {:unsupported_type_conversion, "test", :unknown_type}} =
Types.encode_value("test", type: :unknown_type)
end
end
describe "decode_value/1" do
test "decodes string values" do
assert Types.decode_value({:string, "hello"}) == "hello"
assert Types.decode_value({:string, ""}) == ""
end
test "decodes integer values" do
assert Types.decode_value({:integer, 42}) == 42
assert Types.decode_value({:integer, -100}) == -100
end
test "decodes gauge32 values" do
assert Types.decode_value({:gauge32, 1000}) == 1000
end
test "decodes counter32 values" do
assert Types.decode_value({:counter32, 5000}) == 5000
end
test "decodes counter64 values" do
assert Types.decode_value({:counter64, 9_999_999_999}) == 9_999_999_999
end
test "decodes unsigned32 values" do
assert Types.decode_value({:unsigned32, 300}) == 300
end
test "decodes timeticks values" do
assert Types.decode_value({:timeticks, 12_345}) == 12_345
end
test "decodes valid ipAddress tuples" do
assert Types.decode_value({:ipAddress, {192, 168, 1, 1}}) == "192.168.1.1"
assert Types.decode_value({:ipAddress, {10, 0, 0, 1}}) == "10.0.0.1"
assert Types.decode_value({:ipAddress, {0, 0, 0, 0}}) == "0.0.0.0"
assert Types.decode_value({:ipAddress, {255, 255, 255, 255}}) == "255.255.255.255"
end
test "returns error for invalid ipAddress tuples" do
assert {:error, {:invalid_ip_address, {300, 400, 500, 600}}} =
Types.decode_value({:ipAddress, {300, 400, 500, 600}})
assert {:error, {:invalid_ip_address, {-1, 0, 0, 0}}} =
Types.decode_value({:ipAddress, {-1, 0, 0, 0}})
end
test "decodes objectId values" do
assert Types.decode_value({:objectId, [1, 3, 6, 1]}) == [1, 3, 6, 1]
end
test "decodes objectIdentifier values" do
assert Types.decode_value({:objectIdentifier, [1, 3, 6, 1, 2, 1]}) == [1, 3, 6, 1, 2, 1]
end
test "decodes octetString values" do
binary = <<0, 1, 2, 3>>
assert Types.decode_value({:octetString, binary}) == binary
end
test "decodes boolean values" do
assert Types.decode_value({:boolean, true}) == true
assert Types.decode_value({:boolean, false}) == false
end
test "decodes opaque values" do
value = %{test: "data"}
assert Types.decode_value({:opaque, value}) == value
end
test "decodes null values" do
assert Types.decode_value({:null, nil}) == nil
assert Types.decode_value({:null, :null}) == nil
end
test "decodes SNMPv2c exception values" do
assert Types.decode_value(:noSuchObject) == :no_such_object
assert Types.decode_value(:noSuchInstance) == :no_such_instance
assert Types.decode_value(:endOfMibView) == :end_of_mib_view
end
test "returns error for unknown SNMP types" do
assert {:error, {:unknown_snmp_type, :unknown_type}} =
Types.decode_value({:unknown_type, "value"})
end
test "passes through non-tuple values unchanged" do
assert Types.decode_value("plain string") == "plain string"
assert Types.decode_value(42) == 42
assert Types.decode_value([1, 2, 3]) == [1, 2, 3]
end
end
describe "infer_type/1" do
test "infers string for printable binaries" do
assert Types.infer_type("Hello World") == :string
assert Types.infer_type("Test 123") == :string
assert Types.infer_type("") == :string
end
test "infers octetString for non-printable binaries" do
assert Types.infer_type(<<0, 1, 2, 3>>) == :octetString
assert Types.infer_type(<<255, 254, 253>>) == :octetString
end
test "infers unsigned32 for small positive integers" do
assert Types.infer_type(0) == :unsigned32
assert Types.infer_type(1000) == :unsigned32
assert Types.infer_type(4_294_967_295) == :unsigned32
end
test "infers counter64 for large positive integers" do
assert Types.infer_type(4_294_967_296) == :counter64
assert Types.infer_type(9_999_999_999) == :counter64
end
test "infers integer for negative integers" do
assert Types.infer_type(-1) == :integer
assert Types.infer_type(-1000) == :integer
end
test "infers objectIdentifier for integer lists" do
assert Types.infer_type([1, 3, 6, 1]) == :objectIdentifier
assert Types.infer_type([1]) == :objectIdentifier
end
test "infers string for non-integer lists" do
assert Types.infer_type([1, 2, "a"]) == :string
assert Types.infer_type(["a", "b"]) == :string
end
test "infers boolean for boolean values" do
assert Types.infer_type(true) == :boolean
assert Types.infer_type(false) == :boolean
end
test "infers ipAddress for valid IP tuples" do
assert Types.infer_type({192, 168, 1, 1}) == :ipAddress
assert Types.infer_type({0, 0, 0, 0}) == :ipAddress
assert Types.infer_type({255, 255, 255, 255}) == :ipAddress
end
test "infers opaque for invalid IP tuples" do
assert Types.infer_type({300, 400, 500, 600}) == :opaque
assert Types.infer_type({-1, 0, 0, 0}) == :opaque
end
test "infers null for nil and special atoms" do
assert Types.infer_type(nil) == :null
assert Types.infer_type(:null) == :null
assert Types.infer_type(:undefined) == :null
assert Types.infer_type(:noSuchObject) == :null
assert Types.infer_type(:noSuchInstance) == :null
assert Types.infer_type(:endOfMibView) == :null
end
test "infers opaque for unknown types" do
assert Types.infer_type(%{key: "value"}) == :opaque
assert Types.infer_type({1, 2, 3, 4, 5}) == :opaque
assert Types.infer_type(:some_atom) == :opaque
end
end
describe "encode_value/2 edge cases" do
test "handles boundary values for unsigned32" do
assert {:ok, {:unsigned32, 0}} = Types.encode_value(0, type: :unsigned32)
assert {:error, {:unsupported_type_conversion, -1, :unsigned32}} =
Types.encode_value(-1, type: :unsigned32)
end
test "handles boundary values for counter32" do
assert {:ok, {:counter32, 0}} = Types.encode_value(0, type: :counter32)
assert {:error, {:unsupported_type_conversion, -1, :counter32}} =
Types.encode_value(-1, type: :counter32)
end
test "handles boundary values for counter64" do
assert {:ok, {:counter64, 0}} = Types.encode_value(0, type: :counter64)
assert {:error, {:unsupported_type_conversion, -1, :counter64}} =
Types.encode_value(-1, type: :counter64)
end
test "handles boundary values for gauge32" do
assert {:ok, {:gauge32, 0}} = Types.encode_value(0, type: :gauge32)
assert {:error, {:unsupported_type_conversion, -1, :gauge32}} =
Types.encode_value(-1, type: :gauge32)
end
test "handles boundary values for timeticks" do
assert {:ok, {:timeticks, 0}} = Types.encode_value(0, type: :timeticks)
assert {:error, {:unsupported_type_conversion, -1, :timeticks}} =
Types.encode_value(-1, type: :timeticks)
end
test "handles empty OID list" do
assert {:ok, {:objectIdentifier, []}} = Types.encode_value([], type: :objectIdentifier)
end
test "handles single element OID list" do
assert {:ok, {:objectIdentifier, [1]}} = Types.encode_value([1], type: :objectIdentifier)
end
test "handles IPv6-style addresses gracefully" do
# IPv6 not supported, should error
assert {:error, _} = Types.encode_value("::1", type: :ipAddress)
assert {:error, _} = Types.encode_value("2001:db8::1", type: :ipAddress)
end
test "handles malformed IP addresses" do
assert {:error, {:invalid_ip_address, "256.1.1.1"}} =
Types.encode_value("256.1.1.1", type: :ipAddress)
assert {:error, {:invalid_ip_address, "192.168.1"}} =
Types.encode_value("192.168.1", type: :ipAddress)
assert {:error, {:invalid_ip_address, "invalid"}} =
Types.encode_value("invalid", type: :ipAddress)
end
test "handles UTF-8 strings correctly" do
assert {:ok, {:string, "Hello 世界"}} = Types.encode_value("Hello 世界")
assert {:ok, {:string, "Ñoño"}} = Types.encode_value("Ñoño")
end
end
describe "roundtrip encoding and decoding" do
test "roundtrips string values" do
{:ok, encoded} = Types.encode_value("test")
assert Types.decode_value(encoded) == "test"
end
test "roundtrips integer values" do
{:ok, encoded} = Types.encode_value(42, type: :integer)
assert Types.decode_value(encoded) == 42
end
test "roundtrips IP addresses" do
{:ok, encoded} = Types.encode_value("192.168.1.1", type: :ipAddress)
assert Types.decode_value(encoded) == "192.168.1.1"
end
test "roundtrips OIDs" do
{:ok, encoded} = Types.encode_value([1, 3, 6, 1, 2, 1])
decoded = Types.decode_value(encoded)
assert decoded == [1, 3, 6, 1, 2, 1]
end
test "roundtrips boolean values" do
{:ok, encoded_true} = Types.encode_value(true)
assert Types.decode_value(encoded_true) == true
{:ok, encoded_false} = Types.encode_value(false)
assert Types.decode_value(encoded_false) == false
end
test "roundtrips null values" do
{:ok, encoded} = Types.encode_value(nil)
assert Types.decode_value(encoded) == nil
end
end
end

View file

@ -0,0 +1,339 @@
defmodule SnmpKit.SnmpMgr.WalkTest do
use ExUnit.Case, async: true
alias SnmpKit.SnmpMgr.Walk
# Test helpers to mock Core module behavior
defmodule CoreMock do
@moduledoc false
def send_get_next_request(_target, oid, _opts) do
# Simulate walking through a small tree
case oid do
[1, 3, 6, 1, 2, 1, 1] ->
{:ok, {"1.3.6.1.2.1.1.1.0", :octet_string, "Test Device"}}
[1, 3, 6, 1, 2, 1, 1, 1, 0] ->
{:ok, {"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, 2, 0] ->
{:ok, {"1.3.6.1.2.1.1.3.0", :timeticks, 12_345}}
[1, 3, 6, 1, 2, 1, 1, 3, 0] ->
# Out of scope - different subtree
{:ok, {"1.3.6.1.2.1.2.1.0", :integer, 100}}
# Simulate end of MIB view
[1, 3, 6, 1, 2, 1, 9] ->
{:error, :end_of_mib_view}
[1, 3, 6, 1, 2, 1, 10] ->
{:error, {:snmp_error, :noSuchName}}
# Timeout simulation
[1, 3, 6, 1, 2, 1, 99] ->
{:error, :timeout}
_ ->
{:error, :no_such_name}
end
end
def parse_oid(oid) when is_list(oid), do: {:ok, oid}
def parse_oid(oid) when is_binary(oid) do
case oid do
"1.3.6.1.2.1.1" -> {:ok, [1, 3, 6, 1, 2, 1, 1]}
"1.3.6.1.2.1.2" -> {:ok, [1, 3, 6, 1, 2, 1, 2]}
"invalid" -> {:error, :invalid_oid}
_ -> {:ok, oid |> String.split(".") |> Enum.map(&String.to_integer/1)}
end
end
end
setup do
# Mock the Core module for v1 walks
original_core = Application.get_env(:snmpkit, :core_module)
on_exit(fn ->
if original_core do
Application.put_env(:snmpkit, :core_module, original_core)
else
Application.delete_env(:snmpkit, :core_module)
end
end)
:ok
end
describe "walk/3 with SNMPv2c" do
test "delegates to Bulk.walk_bulk for v2c" do
# This test verifies that v2c walks use the bulk implementation
# We'll test with a simple OID and verify the delegation happens
# The actual bulk implementation is tested in bulk_test.exs
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v2c, timeout: 100)
# Expect timeout or error since we're not mocking Bulk
# This test just verifies the code path is hit
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts string OID for v2c" do
result = Walk.walk("192.168.1.1", "1.3.6.1.2.1.1", version: :v2c, timeout: 100)
# Should attempt to delegate to Bulk
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts max_repetitions option for v2c" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v2c, max_repetitions: 10, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "walk/3 with SNMPv1" do
test "performs iterative GETNEXT walk for v1" do
# This would require mocking Core.send_get_next_request
# For now, test that it attempts the operation
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, timeout: 100)
# Will likely timeout or error without proper mock
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses max_iterations option for v1" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, max_iterations: 10, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "removes max_repetitions from v1 options" do
# max_repetitions is not valid for v1, should be removed
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, max_repetitions: 20, timeout: 100)
# Should not crash, max_repetitions should be ignored
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "returns error for invalid OID" do
result = Walk.walk("192.168.1.1", "invalid", version: :v1, timeout: 100)
assert {:error, _} = result
end
end
describe "walk_table/3 with SNMPv2c" do
test "delegates to Bulk.get_table_bulk for v2c" do
result = Walk.walk_table("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], version: :v2c, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts string table OID for v2c" do
result = Walk.walk_table("192.168.1.1", "1.3.6.1.2.1.2.2", version: :v2c, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "walk_table/3 with SNMPv1" do
test "performs iterative GETNEXT walk for v1 tables" do
result = Walk.walk_table("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses max_iterations for v1 table walks" do
result = Walk.walk_table("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], version: :v1, max_iterations: 5, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "returns error for invalid table OID" do
result = Walk.walk_table("192.168.1.1", "invalid", version: :v1, timeout: 100)
assert {:error, _} = result
end
end
describe "walk_column/3" do
test "walks a specific table column" do
result = Walk.walk_column("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2, 1, 2], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts string column OID" do
result = Walk.walk_column("192.168.1.1", "1.3.6.1.2.1.2.2.1.2", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "respects max_iterations option" do
result = Walk.walk_column("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2, 1, 2], max_iterations: 3, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "returns error for invalid column OID" do
result = Walk.walk_column("192.168.1.1", "invalid", timeout: 100)
assert {:error, _} = result
end
end
describe "walk/3 default behavior" do
test "defaults to v2c when no version specified" do
# Default should delegate to Bulk (v2c)
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses default max_iterations when not specified for v1" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, timeout: 100)
# Should use default max_iterations (100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "uses default timeout when not specified" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1)
# Should use default timeout (5000ms)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "OID resolution" do
test "accepts list format OID" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts string format OID" do
result = Walk.walk("192.168.1.1", "1.3.6.1.2.1.1", version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles OID resolution errors gracefully" do
result = Walk.walk("192.168.1.1", "invalid.oid.format", version: :v1, timeout: 100)
assert {:error, _} = result
end
end
describe "error handling" do
test "handles timeout errors gracefully" do
# Test that timeout errors don't crash the walk
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 99], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles end of MIB view errors" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 9], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles no such name errors" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 10], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "options handling" do
test "accepts community string option" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, community: "private", timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts port option" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, port: 161, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts multiple options" do
opts = [
version: :v1,
community: "public",
port: 161,
timeout: 2000,
max_iterations: 50
]
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], opts)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "target formats" do
test "accepts IP address as target" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts hostname as target" do
result = Walk.walk("device.local", [1, 3, 6, 1, 2, 1, 1], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "accepts target with port" do
result = Walk.walk("192.168.1.1:161", [1, 3, 6, 1, 2, 1, 1], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "walk_table/3 default behavior" do
test "defaults to v2c for table walks" do
result = Walk.walk_table("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "walk_column/3 default behavior" do
test "uses v1-style walk for columns" do
# walk_column always uses iterative GETNEXT
result = Walk.walk_column("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2, 1, 2], timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
describe "edge cases" do
test "handles empty walk results gracefully" do
# Test walking a non-existent subtree
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 999], version: :v1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles max_iterations limit" do
# Test that walk stops at max_iterations
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, max_iterations: 1, timeout: 100)
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
test "handles zero max_iterations" do
result = Walk.walk("192.168.1.1", [1, 3, 6, 1, 2, 1, 1], version: :v1, max_iterations: 0, timeout: 100)
# Should return empty results or error
assert match?({:error, _}, result) or match?({:ok, _}, result)
end
end
end

View file

@ -647,5 +647,156 @@ defmodule Towerops.OrganizationsTest do
# Member can still create their own free org
assert {:ok, _org} = Organizations.create_organization(%{name: "Member's Org"}, member.id)
end
test "create_organization/3 with explicit free subscription plan" do
user = user_fixture()
# Explicitly specify "free" subscription plan
assert {:ok, org} =
Organizations.create_organization(
%{name: "Free Org", subscription_plan: "free"},
user.id
)
assert org.subscription_plan == "free"
end
end
describe "update_organization/2 MikroTik propagation" do
test "propagates MikroTik settings when changed" do
user = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, device} =
Towerops.Devices.create_device(%{
name: "MikroTik Device",
ip_address: "192.168.1.1",
site_id: site.id,
manufacturer: "MikroTik"
})
# Update organization MikroTik settings
{:ok, updated_org} =
Organizations.update_organization(organization, %{
mikrotik_username: "admin",
mikrotik_password: "password123",
mikrotik_port: 8729,
mikrotik_use_ssl: true,
mikrotik_enabled: true
})
assert updated_org.mikrotik_username == "admin"
assert updated_org.mikrotik_port == 8729
assert updated_org.mikrotik_use_ssl == true
assert updated_org.mikrotik_enabled == true
# Verify device was updated with inherited settings
updated_device = Repo.get!(Device, device.id)
assert updated_device.mikrotik_credential_source == "organization"
end
test "propagates when individual MikroTik fields change" do
user = user_fixture()
{:ok, organization} =
Organizations.create_organization(
%{
name: "Test Org",
mikrotik_username: "original",
mikrotik_password: "oldpass"
},
user.id
)
{:ok, site} =
Towerops.Sites.create_site(%{
name: "Test Site",
organization_id: organization.id
})
{:ok, _device} =
Towerops.Devices.create_device(%{
name: "MikroTik Device",
ip_address: "192.168.1.1",
site_id: site.id,
manufacturer: "MikroTik"
})
# Change only username - should still propagate
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_username: "newuser"
})
# Change only password - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_password: "newpass"
})
# Change only port - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_port: 8728
})
# Change only use_ssl - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_use_ssl: false
})
# Change only enabled - should propagate
organization = Organizations.get_organization!(organization.id)
{:ok, _updated} =
Organizations.update_organization(organization, %{
mikrotik_enabled: false
})
end
end
describe "accept_invitation/2 error handling" do
test "returns error when membership creation fails" do
owner = user_fixture()
{:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, owner.id)
token = "test-token-#{System.unique_integer()}"
{:ok, invitation} =
Organizations.create_invitation(%{
organization_id: organization.id,
email: "test@example.com",
role: :member,
invited_by_id: owner.id,
token: token,
expires_at: DateTime.truncate(DateTime.add(DateTime.utc_now(), 7, :day), :second)
})
# Create a membership first so the accept will fail with unique constraint
new_user = user_fixture()
{:ok, _existing_membership} =
Organizations.create_membership(%{
organization_id: organization.id,
user_id: new_user.id,
role: :admin
})
# Accepting invitation should fail because membership already exists
assert {:error, _changeset} = Organizations.accept_invitation(invitation, new_user.id)
end
end
end