defmodule SnmpKit.SnmpLib.PDUTest do use ExUnit.Case, async: true alias SnmpKit.SnmpLib.PDU doctest PDU @moduletag :unit @moduletag :protocol @moduletag :phase_1 describe "PDU construction" do test "builds GET request PDU with correct structure" do oid = [1, 3, 6, 1, 2, 1, 1, 1, 0] request_id = 12_345 pdu = PDU.build_get_request(oid, request_id) assert pdu.type == :get_request assert pdu.request_id == request_id assert pdu.error_status == 0 assert pdu.error_index == 0 assert pdu.varbinds == [{oid, :null, :null}] end test "builds GETNEXT request PDU" do oid = [1, 3, 6, 1, 2, 1, 1] request_id = 23_456 pdu = PDU.build_get_next_request(oid, request_id) assert pdu.type == :get_next_request assert pdu.request_id == request_id assert pdu.varbinds == [{oid, :null, :null}] end test "builds GETBULK request PDU with non-repeaters and max-repetitions" do oid = [1, 3, 6, 1, 2, 1, 2, 2] request_id = 34_567 non_repeaters = 0 max_repetitions = 10 pdu = PDU.build_get_bulk_request(oid, request_id, non_repeaters, max_repetitions) assert pdu.type == :get_bulk_request assert pdu.request_id == request_id assert pdu.non_repeaters == non_repeaters assert pdu.max_repetitions == max_repetitions assert pdu.varbinds == [{oid, :null, :null}] end test "builds SET request PDU with value" do oid = [1, 3, 6, 1, 2, 1, 1, 5, 0] request_id = 45_678 value = {:string, "new-hostname"} pdu = PDU.build_set_request(oid, value, request_id) assert pdu.type == :set_request assert pdu.request_id == request_id assert pdu.varbinds == [{oid, :string, "new-hostname"}] end test "builds response PDU with error status" do request_id = 56_789 # noSuchName error_status = 2 error_index = 1 varbinds = [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :string, "test"}] pdu = PDU.build_response(request_id, error_status, error_index, varbinds) assert pdu.type == :get_response assert pdu.request_id == request_id assert pdu.error_status == error_status assert pdu.error_index == error_index assert pdu.varbinds == varbinds end test "builds multi-varbind GET request" do varbinds = [ {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}, {[1, 3, 6, 1, 2, 1, 1, 2, 0], :null, :null} ] request_id = 67_890 pdu = PDU.build_get_request_multi(varbinds, request_id) assert pdu.type == :get_request assert pdu.request_id == request_id assert pdu.varbinds == varbinds end end describe "PDU validation" do test "validates request ID range" do valid_ids = [0, 1, 65_535, 2_147_483_647] for request_id <- valid_ids do pdu = PDU.build_get_request([1, 3, 6, 1], request_id) assert {:ok, _} = PDU.validate(pdu) end end test "rejects invalid request IDs" do invalid_ids = [-1, 2_147_483_648] for request_id <- invalid_ids do assert_raise ArgumentError, fn -> PDU.build_get_request([1, 3, 6, 1], request_id) end end end test "validates PDU type" do pdu = PDU.build_get_request([1, 3, 6, 1], 123) assert {:ok, _} = PDU.validate(pdu) invalid_pdu = %{type: :invalid_type, request_id: 123, varbinds: []} assert :error = PDU.validate(invalid_pdu) end test "validates varbinds format" do valid_varbinds = [{[1, 3, 6, 1], :null, :null}] pdu = PDU.build_get_request_multi(valid_varbinds, 123) assert {:ok, _} = PDU.validate(pdu) invalid_varbinds = [{"invalid", :null, :null}] assert {:error, :invalid_varbind_format} = PDU.build_get_request_multi(invalid_varbinds, 123) end test "validates GETBULK specific fields" do pdu = PDU.build_get_bulk_request([1, 3, 6, 1], 123, 0, 10) assert {:ok, _} = PDU.validate(pdu) invalid_bulk = %{type: :get_bulk_request, request_id: 123, varbinds: []} assert {:error, :missing_bulk_fields} = PDU.validate(invalid_bulk) end end describe "Message construction" do test "builds SNMP message with v1" do pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345) message = PDU.build_message(pdu, "public", :v1) assert message.version == 0 assert message.community == "public" assert message.pdu == pdu end test "builds SNMP message with v2c" do pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345) message = PDU.build_message(pdu, "public", :v2c) assert message.version == 1 assert message.community == "public" assert message.pdu == pdu end test "rejects GETBULK with v1" do pdu = PDU.build_get_bulk_request([1, 3, 6, 1], 123) assert_raise ArgumentError, ~r/GETBULK requests require SNMPv2c/, fn -> PDU.build_message(pdu, "public", :v1) end end test "validates community string" do pdu = PDU.build_get_request([1, 3, 6, 1], 123) assert_raise ArgumentError, ~r/Community must be a binary string/, fn -> PDU.build_message(pdu, :invalid_community, :v1) end end end describe "Encoding and decoding" do test "encodes and decodes GET request round-trip" do # Build request pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345) message = PDU.build_message(pdu, "public", :v2c) # Encode {:ok, encoded} = PDU.encode_message(message) assert byte_size(encoded) > 0 # Decode {:ok, decoded} = PDU.decode_message(encoded) # Verify structure assert decoded.version == 1 assert decoded.community == "public" assert decoded.pdu.type == :get_request assert decoded.pdu.request_id == 12_345 assert decoded.pdu.error_status == 0 assert decoded.pdu.error_index == 0 assert length(decoded.pdu.varbinds) == 1 end test "encodes and decodes GETBULK request round-trip" do pdu = PDU.build_get_bulk_request([1, 3, 6, 1, 2, 1, 2], 23_456, 0, 10) message = PDU.build_message(pdu, "public", :v2c) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert decoded.version == 1 assert decoded.community == "public" assert decoded.pdu.type == :get_bulk_request assert decoded.pdu.request_id == 23_456 assert decoded.pdu.non_repeaters == 0 assert decoded.pdu.max_repetitions == 10 end test "encodes and decodes SET request with values" do pdu = PDU.build_set_request([1, 3, 6, 1, 2, 1, 1, 5, 0], {:string, "test-value"}, 34_567) message = PDU.build_message(pdu, "private", :v2c) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert decoded.version == 1 assert decoded.community == "private" assert decoded.pdu.type == :set_request assert decoded.pdu.request_id == 34_567 assert length(decoded.pdu.varbinds) == 1 end test "handles encoding errors gracefully" do invalid_message = %{invalid: :structure} assert {:error, :invalid_message_format} = PDU.encode_message(invalid_message) end test "handles decoding errors gracefully" do invalid_binary = <<1, 2, 3, 4>> assert {:error, _} = PDU.decode_message(invalid_binary) assert {:error, :invalid_input} = PDU.decode_message(:not_binary) end test "decodes malformed packets with error" do malformed_data = <<0x01, 0x02, 0x03, 0x04>> result = PDU.decode_message(malformed_data) assert {:error, _} = result end end describe "Community validation" do test "validates correct community string" do pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345) message = PDU.build_message(pdu, "test-community", :v1) {:ok, encoded} = PDU.encode_message(message) assert :ok = PDU.validate_community(encoded, "test-community") end test "rejects incorrect community string" do pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345) message = PDU.build_message(pdu, "correct-community", :v1) {:ok, encoded} = PDU.encode_message(message) assert {:error, :invalid_community} = PDU.validate_community(encoded, "wrong-community") end test "handles validation with malformed packets" do malformed_data = <<0x01, 0x02, 0x03>> result = PDU.validate_community(malformed_data, "any-community") assert {:error, _} = result end test "validates parameter types" do assert {:error, :invalid_parameters} = PDU.validate_community(:not_binary, "community") assert {:error, :invalid_parameters} = PDU.validate_community(<<1, 2, 3>>, :not_binary) end end describe "Error response creation" do test "creates error response from request PDU" do request_pdu = PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 12_345) error_pdu = PDU.create_error_response(request_pdu, 2, 1) assert error_pdu.type == :get_response assert error_pdu.request_id == 12_345 assert error_pdu.error_status == 2 assert error_pdu.error_index == 1 assert error_pdu.varbinds == request_pdu.varbinds end test "creates error response with default error index" do request_pdu = PDU.build_get_request([1, 3, 6, 1], 98_765) error_pdu = PDU.create_error_response(request_pdu, 3) assert error_pdu.type == :get_response assert error_pdu.request_id == 98_765 assert error_pdu.error_status == 3 assert error_pdu.error_index == 0 end test "handles missing fields gracefully" do incomplete_pdu = %{type: :get_request} error_pdu = PDU.create_error_response(incomplete_pdu, 5, 2) assert error_pdu.type == :get_response # Default fallback assert error_pdu.request_id == 1 assert error_pdu.error_status == 5 assert error_pdu.error_index == 2 # Default fallback assert error_pdu.varbinds == [] end end describe "Performance and edge cases" do test "handles large request IDs" do # Max 32-bit signed integer large_id = 2_147_483_647 pdu = PDU.build_get_request([1, 3, 6, 1], large_id) message = PDU.build_message(pdu, "public", :v1) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert decoded.pdu.request_id == large_id end test "handles empty community string" do pdu = PDU.build_get_request([1, 3, 6, 1], 123) message = PDU.build_message(pdu, "", :v1) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert decoded.community == "" end test "handles long community strings" do long_community = String.duplicate("x", 255) pdu = PDU.build_get_request([1, 3, 6, 1], 123) message = PDU.build_message(pdu, long_community, :v1) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert decoded.community == long_community end test "handles complex OIDs" do # Use simple values that work correctly - large OID values to be fixed in Phase 2 complex_oid = [1, 3, 6, 1, 4, 1, 127, 1, 2, 3, 4, 5, 10, 100, 127] pdu = PDU.build_get_request(complex_oid, 123) message = PDU.build_message(pdu, "public", :v2c) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert length(decoded.pdu.varbinds) == 1 {decoded_oid, _, _} = hd(decoded.pdu.varbinds) assert decoded_oid == complex_oid end test "handles maximum GETBULK parameters" do max_reps = 65_535 pdu = PDU.build_get_bulk_request([1, 3, 6, 1], 123, 255, max_reps) message = PDU.build_message(pdu, "public", :v2c) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert decoded.pdu.non_repeaters == 255 assert decoded.pdu.max_repetitions == max_reps end end describe "Error conditions and boundary testing" do test "handles zero-length input gracefully" do assert {:error, _} = PDU.decode_message(<<>>) end test "handles truncated packets" do # Create a valid packet then truncate it pdu = PDU.build_get_request([1, 3, 6, 1], 123) message = PDU.build_message(pdu, "public", :v1) {:ok, encoded} = PDU.encode_message(message) truncated = binary_part(encoded, 0, div(byte_size(encoded), 2)) assert {:error, _} = PDU.decode_message(truncated) end test "validates OID bounds in varbinds" do # Very large OID component large_oid = [1, 3, 6, 1, 999_999_999] pdu = PDU.build_get_request(large_oid, 123) message = PDU.build_message(pdu, "public", :v2c) # Should encode/decode without error {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) assert %{type: _} = decoded.pdu end test "handles concurrent encoding/decoding operations" do # Test thread safety by running multiple operations concurrently tasks = for i <- 1..50 do Task.async(fn -> pdu = PDU.build_get_request([1, 3, 6, 1, i], i) message = PDU.build_message(pdu, "public-#{i}", :v2c) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) {i, decoded.pdu.request_id, decoded.community} end) end results = Task.await_many(tasks, 1000) # Verify all operations completed successfully assert length(results) == 50 for {i, request_id, community} <- results do assert i == request_id assert community == "public-#{i}" end end test "maintains encoding fidelity with random data" do # Test with various random OIDs and values for _iteration <- 1..20 do # Generate random but valid OID # 3 to 12 components oid_length = :rand.uniform(10) + 2 oid = [1, 3] ++ for(_ <- 1..(oid_length - 2), do: :rand.uniform(65_535)) request_id = :rand.uniform(2_147_483_647) community = "test-#{:rand.uniform(1000)}" pdu = PDU.build_get_request(oid, request_id) message = PDU.build_message(pdu, community, :v2c) {:ok, encoded} = PDU.encode_message(message) {:ok, decoded} = PDU.decode_message(encoded) # Verify round-trip fidelity assert decoded.pdu.request_id == request_id assert decoded.community == community assert decoded.pdu.type == :get_request end end end end