towerops/test/snmpkit/snmp_lib/transport_test.exs
2026-06-15 11:15:46 -05:00

359 lines
12 KiB
Elixir

defmodule SnmpKit.SnmpLib.TransportTest do
# UDP sockets need sequential access
use ExUnit.Case, async: false
alias SnmpKit.SnmpLib.Transport
doctest Transport
@moduletag :unit
@moduletag :protocol
@moduletag :phase_2
# Use short timeouts as per testing rules
@test_timeout 200
describe "Socket creation and management" do
test "creates client socket successfully" do
{:ok, socket} = Transport.create_client_socket()
assert is_port(socket)
:ok = Transport.close_socket(socket)
end
test "creates server socket on specific port" do
# Use ephemeral port to avoid conflicts
{:ok, socket} = Transport.create_server_socket(0)
assert is_port(socket)
{:ok, {_address, port}} = Transport.get_socket_address(socket)
assert port > 0
:ok = Transport.close_socket(socket)
end
test "creates socket with custom options" do
options = [{:recbuf, 32_768}, {:active, true}]
{:ok, socket} = Transport.create_client_socket(options)
assert is_port(socket)
:ok = Transport.close_socket(socket)
end
test "rejects invalid port numbers" do
assert {:error, :invalid_port} = Transport.create_socket("0.0.0.0", 0)
assert {:error, :invalid_port} = Transport.create_socket("0.0.0.0", 65_536)
assert {:error, :invalid_port} = Transport.create_socket("0.0.0.0", -1)
end
test "rejects invalid bind addresses" do
assert {:error, _reason} = Transport.create_socket("999.999.999.999", 12_345)
assert {:error, _reason} = Transport.create_socket("invalid.address", 12_345)
end
test "closes socket without errors" do
{:ok, socket} = Transport.create_client_socket()
assert :ok = Transport.close_socket(socket)
# Closing again should still return :ok
assert :ok = Transport.close_socket(socket)
end
end
describe "Address resolution and validation" do
test "resolves valid IP addresses" do
{:ok, {127, 0, 0, 1}} = Transport.resolve_address("127.0.0.1")
{:ok, {192, 168, 1, 1}} = Transport.resolve_address("192.168.1.1")
{:ok, {0, 0, 0, 0}} = Transport.resolve_address("0.0.0.0")
end
test "resolves IP tuples" do
{:ok, {192, 168, 1, 100}} = Transport.resolve_address({192, 168, 1, 100})
{:ok, {127, 0, 0, 1}} = Transport.resolve_address({127, 0, 0, 1})
end
test "resolves localhost hostname" do
{:ok, resolved} = Transport.resolve_address("localhost")
# Should resolve to 127.0.0.1 or ::1, but we'll accept any valid result
assert tuple_size(resolved) == 4
end
test "rejects invalid IP addresses" do
assert {:error, :hostname_resolution_failed} = Transport.resolve_address("256.256.256.256")
assert {:error, :invalid_ip_tuple} = Transport.resolve_address({256, 1, 1, 1})
assert {:error, :invalid_ip_tuple} = Transport.resolve_address({1, 2, 3})
assert {:error, :invalid_address_format} = Transport.resolve_address(12_345)
end
test "validates port numbers correctly" do
assert Transport.validate_port(1) == true
assert Transport.validate_port(161) == true
assert Transport.validate_port(65_535) == true
assert Transport.validate_port(0) == false
assert Transport.validate_port(-1) == false
assert Transport.validate_port(65_536) == false
assert Transport.validate_port("161") == false
end
test "formats endpoints correctly" do
assert Transport.format_endpoint({192, 168, 1, 100}, 161) == "192.168.1.100:161"
assert Transport.format_endpoint("localhost", 162) == "localhost:162"
end
test "gets socket address information" do
{:ok, socket} = Transport.create_server_socket(0)
{:ok, {address, port}} = Transport.get_socket_address(socket)
assert tuple_size(address) == 4
assert is_integer(port) and port > 0
:ok = Transport.close_socket(socket)
end
end
describe "Packet transmission" do
test "sends and receives packets between sockets" do
# Create server socket
{:ok, server_socket} = Transport.create_server_socket(0)
{:ok, {_server_addr, server_port}} = Transport.get_socket_address(server_socket)
# Create client socket
{:ok, client_socket} = Transport.create_client_socket()
# Send packet from client to server
test_data = "Hello SNMP"
:ok = Transport.send_packet(client_socket, "127.0.0.1", server_port, test_data)
# Receive packet on server
{:ok, {received_data, from_addr, from_port}} =
Transport.receive_packet(server_socket, @test_timeout)
assert received_data == test_data
assert tuple_size(from_addr) == 4
assert is_integer(from_port) and from_port > 0
# Cleanup
:ok = Transport.close_socket(server_socket)
:ok = Transport.close_socket(client_socket)
end
test "handles receive timeout" do
{:ok, socket} = Transport.create_client_socket()
# Should timeout since no data is coming
assert {:error, :timeout} = Transport.receive_packet(socket, 50)
:ok = Transport.close_socket(socket)
end
test "validates packet data before sending" do
{:ok, socket} = Transport.create_client_socket()
assert {:error, :invalid_data} =
Transport.send_packet(socket, "127.0.0.1", 12_345, :not_binary)
assert {:error, :invalid_data} = Transport.send_packet(socket, "127.0.0.1", 12_345, 12_345)
:ok = Transport.close_socket(socket)
end
test "rejects invalid destination addresses for sending" do
{:ok, socket} = Transport.create_client_socket()
assert {:error, _reason} = Transport.send_packet(socket, "invalid.address", 12_345, "data")
assert {:error, :invalid_port} = Transport.send_packet(socket, "127.0.0.1", 0, "data")
:ok = Transport.close_socket(socket)
end
end
describe "Filtered packet reception" do
test "receives packet matching filter" do
{:ok, server_socket} = Transport.create_server_socket(0)
{:ok, {_server_addr, server_port}} = Transport.get_socket_address(server_socket)
{:ok, client_socket} = Transport.create_client_socket()
{:ok, {_client_addr, _client_port}} = Transport.get_socket_address(client_socket)
# Send packet
test_data = "filtered packet"
:ok = Transport.send_packet(client_socket, "127.0.0.1", server_port, test_data)
# Filter for packets from client
filter_fn = fn {_data, from_addr, _from_port} ->
from_addr == {127, 0, 0, 1}
end
{:ok, {received_data, _from_addr, _from_port}} =
Transport.receive_packet_filtered(server_socket, filter_fn, @test_timeout)
assert received_data == test_data
:ok = Transport.close_socket(server_socket)
:ok = Transport.close_socket(client_socket)
end
test "times out when no packets match filter" do
{:ok, socket} = Transport.create_client_socket()
# Filter that never matches
filter_fn = fn {_data, _from_addr, _from_port} -> false end
assert {:error, :timeout} =
Transport.receive_packet_filtered(socket, filter_fn, 50)
:ok = Transport.close_socket(socket)
end
end
describe "Connectivity testing" do
test "detects connectivity to localhost" do
# Start a simple server
{:ok, server_socket} = Transport.create_server_socket(0)
{:ok, {_server_addr, server_port}} = Transport.get_socket_address(server_socket)
# Spawn a task to respond to any packet
server_task =
Task.async(fn ->
case Transport.receive_packet(server_socket, @test_timeout) do
{:ok, {_data, from_addr, from_port}} ->
Transport.send_packet(server_socket, from_addr, from_port, "response")
_ ->
:ok
end
end)
# Test connectivity
result = Transport.test_connectivity("127.0.0.1", server_port, @test_timeout)
assert result == :ok
# Cleanup
Task.await(server_task, @test_timeout)
:ok = Transport.close_socket(server_socket)
end
test "detects lack of connectivity to non-existent service" do
# Use a port that's unlikely to be in use
unused_port = 45_678
assert {:error, :timeout} = Transport.test_connectivity("127.0.0.1", unused_port, 50)
end
end
describe "Socket statistics and information" do
test "retrieves socket statistics" do
{:ok, socket} = Transport.create_client_socket()
{:ok, stats} = Transport.get_socket_stats(socket)
assert Map.has_key?(stats, :socket_info)
assert Map.has_key?(stats, :port_info)
assert Map.has_key?(stats, :statistics)
:ok = Transport.close_socket(socket)
end
end
describe "Utility functions" do
test "returns standard SNMP ports" do
assert Transport.snmp_agent_port() == 161
assert Transport.snmp_trap_port() == 162
end
test "identifies SNMP ports" do
assert Transport.snmp_port?(161) == true
assert Transport.snmp_port?(162) == true
assert Transport.snmp_port?(80) == false
assert Transport.snmp_port?(443) == false
end
test "returns maximum SNMP payload size" do
max_size = Transport.max_snmp_payload_size()
assert is_integer(max_size)
# Should be reasonable size
assert max_size > 1000
# Ethernet MTU - IP - UDP headers
assert max_size == 1472
end
test "validates packet sizes" do
assert Transport.valid_packet_size?(100) == true
assert Transport.valid_packet_size?(1472) == true
assert Transport.valid_packet_size?(2000) == false
assert Transport.valid_packet_size?(0) == false
assert Transport.valid_packet_size?(-1) == false
end
end
describe "Error handling and edge cases" do
test "handles concurrent socket operations" do
# Create multiple sockets concurrently
tasks =
for i <- 1..10 do
Task.async(fn ->
{:ok, socket} = Transport.create_client_socket()
{:ok, {_addr, port}} = Transport.get_socket_address(socket)
:ok = Transport.close_socket(socket)
{i, port}
end)
end
results = Task.await_many(tasks, 1000)
# All should succeed
assert length(results) == 10
for {i, port} <- results do
assert is_integer(i)
assert is_integer(port) and port > 0
end
end
test "handles large packet transmission" do
{:ok, server_socket} = Transport.create_server_socket(0)
{:ok, {_server_addr, server_port}} = Transport.get_socket_address(server_socket)
{:ok, client_socket} = Transport.create_client_socket()
# Send large packet (but within limits)
large_data = String.duplicate("A", 1400)
:ok = Transport.send_packet(client_socket, "127.0.0.1", server_port, large_data)
{:ok, {received_data, _from_addr, _from_port}} =
Transport.receive_packet(server_socket, @test_timeout)
assert received_data == large_data
assert byte_size(received_data) == 1400
:ok = Transport.close_socket(server_socket)
:ok = Transport.close_socket(client_socket)
end
test "handles empty packet transmission" do
{:ok, server_socket} = Transport.create_server_socket(0)
{:ok, {_server_addr, server_port}} = Transport.get_socket_address(server_socket)
{:ok, client_socket} = Transport.create_client_socket()
# Send empty packet
:ok = Transport.send_packet(client_socket, "127.0.0.1", server_port, "")
{:ok, {received_data, _from_addr, _from_port}} =
Transport.receive_packet(server_socket, @test_timeout)
assert received_data == ""
:ok = Transport.close_socket(server_socket)
:ok = Transport.close_socket(client_socket)
end
test "handles socket operations on closed socket gracefully" do
{:ok, socket} = Transport.create_client_socket()
:ok = Transport.close_socket(socket)
# Operations on closed socket should fail gracefully
result = Transport.send_packet(socket, "127.0.0.1", 12_345, "test")
assert {:error, _reason} = result
end
end
end