more tests
This commit is contained in:
parent
1ce3763e5f
commit
13b3810010
5 changed files with 1890 additions and 0 deletions
517
test/snmpkit/snmp_mgr/bulk_test.exs
Normal file
517
test/snmpkit/snmp_mgr/bulk_test.exs
Normal 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
|
||||
447
test/snmpkit/snmp_mgr/errors_test.exs
Normal file
447
test/snmpkit/snmp_mgr/errors_test.exs
Normal 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
|
||||
436
test/snmpkit/snmp_mgr/types_test.exs
Normal file
436
test/snmpkit/snmp_mgr/types_test.exs
Normal 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
|
||||
339
test/snmpkit/snmp_mgr/walk_test.exs
Normal file
339
test/snmpkit/snmp_mgr/walk_test.exs
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue