defmodule SnmpKit.SnmpLib.PDU.V3EncoderTest do use ExUnit.Case, async: true alias SnmpKit.SnmpLib.PDU.V3Encoder describe "create_discovery_message/1" do test "creates discovery message with default msg_id" do msg = V3Encoder.create_discovery_message() assert msg.version == 3 assert is_integer(msg.msg_id) assert msg.msg_id > 0 assert msg.msg_flags.auth == false assert msg.msg_flags.priv == false assert msg.msg_flags.reportable == true assert msg.msg_security_parameters == <<>> assert msg.msg_data.context_engine_id == <<>> assert msg.msg_data.context_name == <<>> end test "creates discovery message with custom msg_id" do msg = V3Encoder.create_discovery_message(12_345) assert msg.msg_id == 12_345 assert msg.version == 3 end test "includes snmpEngineID varbind in discovery message" do msg = V3Encoder.create_discovery_message() pdu = msg.msg_data.pdu assert pdu.type == :get_request assert length(pdu.varbinds) == 1 # snmpEngineID OID [{oid, type, value}] = pdu.varbinds assert oid == [1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0] assert type == :null assert value == :null end end describe "encode_message/2" do test "returns error for non-v3 message" do msg = %{version: 2} assert {:error, :invalid_version} = V3Encoder.encode_message(msg, nil) end test "returns error for invalid message format" do assert {:error, :invalid_message_format} = V3Encoder.encode_message(%{}, nil) end test "encodes discovery message without user" do msg = V3Encoder.create_discovery_message(999) # Discovery messages should encode without user result = V3Encoder.encode_message(msg, nil) # Should succeed or fail with specific error (not crash) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "decode_message/2" do test "returns error for invalid binary data" do result = V3Encoder.decode_message(<<1, 2, 3>>, nil) assert match?({:error, _}, result) end test "returns error for empty data" do result = V3Encoder.decode_message(<<>>, nil) assert match?({:error, _}, result) end test "handles malformed SEQUENCE" do # Invalid SEQUENCE tag data = <<0xFF, 0x10, 1, 2, 3>> result = V3Encoder.decode_message(data, nil) assert match?({:error, _}, result) end end describe "roundtrip encoding/decoding" do test "discovery message roundtrip without security" do original_msg = V3Encoder.create_discovery_message(12_345) case V3Encoder.encode_message(original_msg, nil) do {:ok, encoded} -> case V3Encoder.decode_message(encoded, nil) do {:ok, decoded} -> # Version should match assert decoded.version == original_msg.version # Message ID should match assert decoded.msg_id == original_msg.msg_id # Flags should match assert decoded.msg_flags.auth == original_msg.msg_flags.auth assert decoded.msg_flags.priv == original_msg.msg_flags.priv {:error, _reason} -> # Decoding may fail due to complex ASN.1 processing, that's acceptable :ok end {:error, _reason} -> # Encoding may fail without full dependencies, that's acceptable :ok end end end describe "classify_error/1" do # Test error classification through the ErrorHandler if V3Encoder uses it test "v3 encoder handles encoding failures gracefully" do invalid_msg = %{ version: 3, msg_id: 1, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: false}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{type: :invalid_type} } } result = V3Encoder.encode_message(invalid_msg, nil) assert match?({:error, _}, result) end end describe "security parameter handling" do test "encodes message without security parameters" do msg = %{ version: 3, msg_id: 123, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_security_parameters: <<>>, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) # Should succeed or fail gracefully (not crash) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "message flags" do test "creates message with various flag combinations" do # No auth, no priv msg1 = %{V3Encoder.create_discovery_message() | msg_flags: %{auth: false, priv: false, reportable: true}} assert msg1.msg_flags.auth == false assert msg1.msg_flags.priv == false # Auth only msg2 = %{V3Encoder.create_discovery_message() | msg_flags: %{auth: true, priv: false, reportable: true}} assert msg2.msg_flags.auth == true assert msg2.msg_flags.priv == false # Auth and priv msg3 = %{V3Encoder.create_discovery_message() | msg_flags: %{auth: true, priv: true, reportable: false}} assert msg3.msg_flags.auth == true assert msg3.msg_flags.priv == true end end describe "edge cases" do test "handles empty varbinds" do msg = %{ version: 3, msg_id: 1, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 1, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "handles large msg_id" do large_id = 2_147_483_647 msg = V3Encoder.create_discovery_message(large_id) assert msg.msg_id == large_id end test "handles various context names" do msg = V3Encoder.create_discovery_message() msg1 = %{msg | msg_data: %{msg.msg_data | context_name: "test-context"}} assert msg1.msg_data.context_name == "test-context" msg2 = %{msg | msg_data: %{msg.msg_data | context_name: <<>>}} assert msg2.msg_data.context_name == <<>> end end describe "PDU types in SNMPv3 messages" do test "encodes get_next_request PDU in v3 message" do msg = %{ version: 3, msg_id: 456, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_next_request, request_id: 456, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1], :null, nil}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes get_response PDU in v3 message" do msg = %{ version: 3, msg_id: 789, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, 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"}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes set_request PDU in v3 message" do msg = %{ version: 3, msg_id: 111, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, 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"}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes get_bulk_request PDU in v3 message" do msg = %{ version: 3, msg_id: 999, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, 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}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "context engine ID and context name" do test "encodes message with non-empty context_engine_id" do msg = %{ version: 3, msg_id: 123, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<0x80, 0x00, 0x1F, 0x88, 0x80>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes message with non-empty context_name" do msg = %{ version: 3, msg_id: 456, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: "my-context", pdu: %{ type: :get_request, request_id: 456, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes message with both context_engine_id and context_name" do msg = %{ version: 3, msg_id: 789, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<0x80, 0x00, 0x1F, 0x88, 0x80>>, context_name: "remote-context", pdu: %{ type: :get_request, request_id: 789, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "message max size variations" do test "encodes message with minimum msg_max_size" do msg = %{ version: 3, msg_id: 1, msg_max_size: 484, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 1, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes message with custom msg_max_size" do msg = %{ version: 3, msg_id: 1, msg_max_size: 32_768, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 1, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "message with multiple varbinds" do test "encodes v3 message with multiple varbinds" do msg = %{ version: 3, msg_id: 12_345, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 12_345, 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} ] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 response with various value types" do msg = %{ version: 3, msg_id: 99_999, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 99_999, error_status: 0, error_index: 0, varbinds: [ {[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "System description"}, {[1, 3, 6, 1, 2, 1, 1, 3, 0], :timeticks, 123_456_789}, {[1, 3, 6, 1, 2, 1, 1, 7, 0], :integer, 72}, {[1, 3, 6, 1, 2, 1, 2, 1, 10, 1], :counter32, 987_654_321} ] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "error responses in v3" do test "encodes v3 message with non-zero error_status" do msg = %{ version: 3, msg_id: 777, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 777, error_status: 2, error_index: 1, varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with various error statuses" do for error_status <- 0..5 do msg = %{ version: 3, msg_id: 1000 + error_status, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 1000 + error_status, error_status: error_status, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end end describe "reportable flag variations" do test "encodes message with reportable=false" do msg = %{ version: 3, msg_id: 111, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: false}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 111, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes message with reportable=true" do msg = V3Encoder.create_discovery_message() assert msg.msg_flags.reportable == true result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "security model variations" do test "encodes message with USM security model (3)" do msg = %{ version: 3, msg_id: 1, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_request, request_id: 1, error_status: 0, error_index: 0, varbinds: [] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "varbind value type coverage in v3" do test "encodes v3 message with counter64 value" do msg = %{ version: 3, msg_id: 5555, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 5555, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :counter64, 9_876_543_210}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with gauge32 value" do msg = %{ version: 3, msg_id: 6666, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 6666, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 2, 1, 5, 1], :gauge32, 1_000_000}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with ip_address value" do msg = %{ version: 3, msg_id: 7777, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 7777, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 1], :ip_address, <<192, 168, 1, 1>>}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with opaque value" do msg = %{ version: 3, msg_id: 8888, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 8888, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 4, 1, 9, 2, 1, 57, 0], :opaque, <<0x9F, 0x78, 0x04>>}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with object_identifier value" do msg = %{ version: 3, msg_id: 9999, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 9999, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 1, 2, 0], :object_identifier, [1, 3, 6, 1, 4, 1]}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "exception values in v3" do test "encodes v3 message with no_such_object exception" do msg = %{ version: 3, msg_id: 1111, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 1111, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 99, 1], :no_such_object, nil}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with no_such_instance exception" do msg = %{ version: 3, msg_id: 2222, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 2222, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 99, 2], :no_such_instance, nil}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes v3 message with end_of_mib_view exception" do msg = %{ version: 3, msg_id: 3333, msg_max_size: 65_507, msg_flags: %{auth: false, priv: false, reportable: true}, msg_security_model: 3, msg_data: %{ context_engine_id: <<>>, context_name: <<>>, pdu: %{ type: :get_response, request_id: 3333, error_status: 0, error_index: 0, varbinds: [{[1, 3, 6, 1, 2, 1, 99, 3], :end_of_mib_view, nil}] } } } result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "message ID edge cases" do test "encodes message with zero msg_id" do msg = V3Encoder.create_discovery_message(0) assert msg.msg_id == 0 result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "encodes message with maximum msg_id" do max_id = 2_147_483_647 msg = V3Encoder.create_discovery_message(max_id) assert msg.msg_id == max_id result = V3Encoder.encode_message(msg, nil) assert match?({:ok, _}, result) or match?({:error, _}, result) end end describe "discovery message consistency" do test "creates discovery message with consistent structure" do msg1 = V3Encoder.create_discovery_message() msg2 = V3Encoder.create_discovery_message() # Same structure, different msg_id assert msg1.version == msg2.version assert msg1.msg_max_size == msg2.msg_max_size assert msg1.msg_flags == msg2.msg_flags assert msg1.msg_security_model == msg2.msg_security_model assert msg1.msg_data.context_engine_id == msg2.msg_data.context_engine_id assert msg1.msg_data.context_name == msg2.msg_data.context_name assert msg1.msg_data.pdu.type == msg2.msg_data.pdu.type end test "discovery message uses snmpEngineID OID" do msg = V3Encoder.create_discovery_message() [{oid, _type, _value}] = msg.msg_data.pdu.varbinds # snmpEngineID OID: 1.3.6.1.6.3.10.2.1.1.0 assert oid == [1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0] end end end