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