diff --git a/test/snmpkit/snmp_lib/pdu/encoder_test.exs b/test/snmpkit/snmp_lib/pdu/encoder_test.exs new file mode 100644 index 00000000..945e9beb --- /dev/null +++ b/test/snmpkit/snmp_lib/pdu/encoder_test.exs @@ -0,0 +1,1563 @@ +defmodule SnmpKit.SnmpLib.PDU.EncoderTest do + use ExUnit.Case, async: true + + alias SnmpKit.SnmpLib.PDU.Encoder + + describe "encode_message/1" do + test "encodes SNMPv1 message" do + message = %{ + version: 0, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + # Should start with SEQUENCE tag + assert <<0x30, _::binary>> = encoded + end + + test "encodes SNMPv2c message" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 100, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 5, 0], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "delegates SNMPv3 messages to V3Encoder" do + message = %{ + version: 3, + msgID: 1, + msgMaxSize: 65507, + msgFlags: <<0x04>>, + msgSecurityModel: 3, + msgSecurityParameters: "", + contextEngineID: "", + contextName: "", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + # Should delegate to V3Encoder (may fail if V3Encoder requires specific fields) + result = Encoder.encode_message(message) + assert is_tuple(result) and elem(result, 0) in [:ok, :error] + end + + test "returns error for invalid message format" do + assert {:error, :invalid_message_format} = Encoder.encode_message(%{invalid: :format}) + assert {:error, :invalid_message_format} = Encoder.encode_message(%{version: 1}) + assert {:error, :invalid_message_format} = Encoder.encode_message(%{}) + end + + test "encodes message with multiple varbinds" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 1, 3, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 1, 5, 0], :null, nil} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes message with empty community string" do + message = %{ + version: 0, + community: "", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "encode_message/2" do + test "delegates SNMPv3 messages with user to V3Encoder" do + message = %{ + version: 3, + msgID: 1, + msgMaxSize: 65507, + msgFlags: <<0x04>>, + msgSecurityModel: 3, + msgSecurityParameters: "", + contextEngineID: "", + contextName: "", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + user = %{ + auth_protocol: :md5, + auth_key: "authentication_key", + priv_protocol: :des, + priv_key: "privacy_key" + } + + # Should delegate to V3Encoder (may fail if V3Encoder requires specific fields) + result = Encoder.encode_message(message, user) + assert is_tuple(result) and elem(result, 0) in [:ok, :error] + end + + test "falls back to regular encoding for v1/v2c with user parameter" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + user = %{auth_protocol: :md5} + + assert {:ok, encoded} = Encoder.encode_message(message, user) + assert is_binary(encoded) + end + + test "ignores user parameter for non-v3 messages" do + message = %{ + version: 0, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded1} = Encoder.encode_message(message, nil) + assert {:ok, encoded2} = Encoder.encode_message(message, %{some: "user"}) + assert encoded1 == encoded2 + end + end + + describe "encode_pdu/1" do + test "encodes get_request PDU" do + pdu = %{ + type: :get_request, + request_id: 123, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :null, nil}] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + # Should start with get_request tag (0xA0) + assert <<0xA0, _::binary>> = encoded + end + + test "encodes get_next_request PDU" do + pdu = %{ + type: :get_next_request, + request_id: 456, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6], :null, nil}] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + # Should start with getnext_request tag (0xA1) + assert <<0xA1, _::binary>> = encoded + end + + test "encodes get_response PDU" do + pdu = %{ + type: :get_response, + request_id: 789, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "System description"}] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + # Should start with get_response tag (0xA2) + assert <<0xA2, _::binary>> = encoded + end + + test "encodes set_request PDU" do + pdu = %{ + type: :set_request, + request_id: 111, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 5, 0], :octet_string, "NewName"}] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + # Should start with set_request tag (0xA3) + assert <<0xA3, _::binary>> = encoded + end + + test "encodes get_bulk_request PDU" do + pdu = %{ + type: :get_bulk_request, + request_id: 999, + non_repeaters: 0, + max_repetitions: 10, + varbinds: [{[1, 3, 6, 1, 2, 1, 2, 2], :null, nil}] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + # Should start with getbulk_request tag (0xA5) + assert <<0xA5, _::binary>> = encoded + end + + test "returns error for unsupported PDU type" do + pdu = %{ + type: :unsupported_type, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + + assert {:error, :unsupported_pdu_type} = Encoder.encode_pdu(pdu) + end + + test "encodes PDU with empty varbinds" do + pdu = %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + end + + test "encodes PDU with multiple varbinds" do + pdu = %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 1, 3, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 1, 5, 0], :null, nil} + ] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + end + + test "encodes PDU with large request_id" do + pdu = %{ + type: :get_request, + request_id: 2_147_483_647, + error_status: 0, + error_index: 0, + varbinds: [] + } + + assert {:ok, encoded} = Encoder.encode_pdu(pdu) + assert is_binary(encoded) + end + end + + describe "encode/1 and encode_snmp_packet/1" do + test "encode/1 is an alias for encode_message/1" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded1} = Encoder.encode(message) + assert {:ok, encoded2} = Encoder.encode_message(message) + assert encoded1 == encoded2 + end + + test "encode_snmp_packet/1 is an alias for encode/1" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded1} = Encoder.encode_snmp_packet(message) + assert {:ok, encoded2} = Encoder.encode(message) + assert encoded1 == encoded2 + end + end + + describe "varbind encoding" do + test "encodes varbind with explicit null type" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with integer value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 7, 0], :integer, 72}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with octet_string value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "System description"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with counter32 value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 2, 1, 10, 1], :counter32, 1_234_567}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with gauge32 value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 2, 1, 5, 1], :gauge32, 1_000_000}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with timeticks value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 3, 0], :timeticks, 12345678}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with counter64 value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :counter64, 9_876_543_210}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with ip_address value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 1], :ip_address, <<192, 168, 1, 1>>}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with opaque value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 4, 1, 9, 2, 1, 57, 0], :opaque, <<0x9F, 0x78, 0x04, 0x41, 0x70, 0x00, 0x00>>}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with object_identifier value (list)" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 2, 0], :object_identifier, [1, 3, 6, 1, 4, 1]}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with object_identifier value (string)" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 2, 0], :object_identifier, "1.3.6.1.4.1"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "auto type varbind encoding" do + test "encodes varbind with auto type and integer value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 7, 0], :auto, 72}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and string value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 5, 0], :auto, "router1"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and OID list" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 2, 0], :auto, [1, 3, 6, 1, 4, 1, 9]}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and OID string" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 2, 0], :auto, "1.3.6.1.4.1.9"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and null value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :auto, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and :null atom" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :auto, :null}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and typed tuple" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 2, 1, 10, 1], :auto, {:counter32, 1_234_567}}, + {[1, 3, 6, 1, 2, 1, 2, 1, 5, 1], :auto, {:gauge32, 1_000_000}}, + {[1, 3, 6, 1, 2, 1, 1, 3, 0], :auto, {:timeticks, 12345}}, + {[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :auto, {:counter64, 9_876_543_210}} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind with auto type and exception values" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 99, 1], :auto, {:no_such_object, nil}}, + {[1, 3, 6, 1, 2, 1, 99, 2], :auto, {:no_such_instance, nil}}, + {[1, 3, 6, 1, 2, 1, 99, 3], :auto, {:end_of_mib_view, nil}} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "exception value encoding" do + test "encodes no_such_object exception" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 99, 1], :no_such_object, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes no_such_instance exception" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 99, 2], :no_such_instance, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes end_of_mib_view exception" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 99, 3], :end_of_mib_view, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "integer encoding edge cases" do + test "encodes zero integer" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 0, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :integer, 0}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes small positive integer (<128)" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :integer, 42}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes large positive integer" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :integer, 2_147_483_647}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes negative integer" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :integer, -100}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes large negative integer" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :integer, -2_147_483_648}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "counter32/gauge32/timeticks encoding" do + test "encodes counter32 min value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :counter32, 0}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes counter32 max value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :counter32, 4_294_967_295}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes gauge32 min value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :gauge32, 0}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes gauge32 max value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :gauge32, 4_294_967_295}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes timeticks min value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 3, 0], :timeticks, 0}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes timeticks max value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 3, 0], :timeticks, 4_294_967_295}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "counter64 encoding" do + test "encodes counter64 min value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :counter64, 0}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes counter64 large value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :counter64, 9_999_999_999_999}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes counter64 max value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :counter64, 18_446_744_073_709_551_615}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "string encoding" do + test "encodes empty string" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, ""}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes short string" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "test"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes long string" do + long_string = String.duplicate("A", 256) + + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, long_string}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes string with special characters" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "Test\nString\twith\rspecial"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes binary string" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, <<0x00, 0x01, 0x02, 0xFF>>}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "OID encoding" do + test "encodes simple OID" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes OID with large subidentifiers" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1000], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes OID with very large subidentifiers" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 4, 1, 9, 9, 1, 1, 1, 1, 1_000_000], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes long OID" do + long_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1] ++ Enum.to_list(1..50) + + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{long_oid, :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes minimum OID (two components)" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[0, 0], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes OID starting with 2" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[2, 5, 29, 17], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "bulk request encoding" do + test "encodes bulk request with non_repeaters" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_bulk_request, + request_id: 1, + non_repeaters: 1, + max_repetitions: 10, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 2, 2, 1, 10], :null, nil} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes bulk request with zero non_repeaters" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_bulk_request, + request_id: 1, + non_repeaters: 0, + max_repetitions: 25, + varbinds: [{[1, 3, 6, 1, 2, 1, 2, 2, 1, 10], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes bulk request with large max_repetitions" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_bulk_request, + request_id: 1, + non_repeaters: 0, + max_repetitions: 1000, + varbinds: [{[1, 3, 6, 1, 2, 1, 2, 2], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "error handling" do + test "handles encoding errors gracefully" do + # Invalid varbind format should be caught + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: ["invalid_varbind"] + } + } + + assert {:error, _reason} = Encoder.encode_message(message) + end + + test "handles missing PDU fields" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request + # Missing required fields + } + } + + assert {:error, _reason} = Encoder.encode_message(message) + end + end + + describe "PDU with error status" do + test "encodes PDU with non-zero error_status" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 2, + error_index: 1, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes PDU with various error statuses" do + for error_status <- 0..5 do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: error_status, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + end + + describe "community string variations" do + test "encodes message with short community" do + message = %{ + version: 1, + community: "p", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes message with long community" do + long_community = String.duplicate("community", 20) + + message = %{ + version: 1, + community: long_community, + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes message with special characters in community" do + message = %{ + version: 1, + community: "test@community#123", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "varbind format variations" do + test "encodes varbind as two-tuple (implicit auto type)" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], nil}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes varbind as two-tuple with value" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], "Test"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "length encoding edge cases" do + test "encodes value with length < 128 (short form)" do + # Short string (< 128 bytes) + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :octet_string, "Short"}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes value with length 128-255 (long form 1 byte)" do + # String of 150 bytes + string = String.duplicate("A", 150) + + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :octet_string, string}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes value with length 256-65535 (long form 2 bytes)" do + # String of 300 bytes + string = String.duplicate("B", 300) + + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 1, + error_status: 0, + error_index: 0, + varbinds: [{[1, 3, 6, 1], :octet_string, string}] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end + + describe "comprehensive integration tests" do + test "encodes complete SNMP GET request" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_request, + request_id: 12345, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 1, 3, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 1, 5, 0], :null, nil} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + assert byte_size(encoded) > 0 + end + + test "encodes complete SNMP GET response with multiple types" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_response, + request_id: 12345, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "Linux router 4.19.0"}, + {[1, 3, 6, 1, 2, 1, 1, 3, 0], :timeticks, 123456789}, + {[1, 3, 6, 1, 2, 1, 1, 5, 0], :octet_string, "router1.example.com"}, + {[1, 3, 6, 1, 2, 1, 1, 7, 0], :integer, 72}, + {[1, 3, 6, 1, 2, 1, 2, 1, 10, 1], :counter32, 987654321} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + assert byte_size(encoded) > 0 + end + + test "encodes SNMP SET request" do + message = %{ + version: 1, + community: "private", + pdu: %{ + type: :set_request, + request_id: 9999, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 5, 0], :octet_string, "NewHostName"}, + {[1, 3, 6, 1, 2, 1, 1, 6, 0], :octet_string, "Data Center A"} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes SNMP GETNEXT request" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_next_request, + request_id: 5555, + error_status: 0, + error_index: 0, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 2, 2, 1, 1], :null, nil} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + + test "encodes SNMP GETBULK request" do + message = %{ + version: 1, + community: "public", + pdu: %{ + type: :get_bulk_request, + request_id: 7777, + non_repeaters: 1, + max_repetitions: 10, + varbinds: [ + {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}, + {[1, 3, 6, 1, 2, 1, 2, 2, 1, 2], :null, nil} + ] + } + } + + assert {:ok, encoded} = Encoder.encode_message(message) + assert is_binary(encoded) + end + end +end