From 35ee87952f4f8a62e81a701af6046144edf7b08f Mon Sep 17 00:00:00 2001 From: mayor Date: Thu, 5 Feb 2026 17:24:10 -0600 Subject: [PATCH] test: expand Types module test coverage Added comprehensive test coverage for previously untested functionality: - IP address parsing (valid IPs, invalid inputs, IPv6 rejection) - Zero-filling behavior for incomplete IPs (Erlang :inet standard) - Type inference for all SNMP types (integers, strings, lists, booleans, nulls) - Value decoding for all SNMP types (strings, integers, IPs, OIDs, exceptions) - Value encoding with type inference and validation controls - Unsigned32 validation (valid ranges, overflow, underflow) - Coercion edge cases (IP addresses, unsigned32, octet_string, OIDs, booleans) Fixes: - Corrected type inference expectation for large integers (returns :unsigned32) - Fixed boolean assertion to use equality check instead of pattern match - Documented :inet.parse_address zero-fill behavior for incomplete IPs Total: 97 tests, all passing --- test/snmpkit/snmp_lib/types_test.exs | 205 +++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/test/snmpkit/snmp_lib/types_test.exs b/test/snmpkit/snmp_lib/types_test.exs index 1ac8cdb7..59c5fbee 100644 --- a/test/snmpkit/snmp_lib/types_test.exs +++ b/test/snmpkit/snmp_lib/types_test.exs @@ -479,4 +479,209 @@ defmodule SnmpKit.SnmpLib.TypesTest do assert :ok = Types.validate_integer(42) end end + + describe "IP address parsing" do + test "parses valid IP address strings" do + assert {:ok, {192, 168, 1, 1}} = Types.parse_ip_address("192.168.1.1") + assert {:ok, {127, 0, 0, 1}} = Types.parse_ip_address("127.0.0.1") + assert {:ok, {255, 255, 255, 255}} = Types.parse_ip_address("255.255.255.255") + assert {:ok, {0, 0, 0, 0}} = Types.parse_ip_address("0.0.0.0") + end + + test "rejects invalid IP address strings" do + assert {:error, :invalid_format} = Types.parse_ip_address("invalid") + assert {:error, :invalid_format} = Types.parse_ip_address("256.0.0.1") + end + + test "zero-fills incomplete IP addresses" do + # :inet.parse_address accepts incomplete IPs and zero-fills them + # Missing octets are filled at the right with the last octet going to the end + assert {:ok, {192, 168, 0, 1}} = Types.parse_ip_address("192.168.1") + assert {:ok, {0, 0, 0, 10}} = Types.parse_ip_address("10") + end + + test "rejects IPv6 addresses" do + assert {:error, :not_ipv4} = Types.parse_ip_address("::1") + assert {:error, :not_ipv4} = Types.parse_ip_address("2001:db8::1") + end + + test "rejects non-binary input" do + assert {:error, :invalid_input} = Types.parse_ip_address(nil) + assert {:error, :invalid_input} = Types.parse_ip_address(123) + end + end + + describe "Type inference" do + test "infers integer types" do + assert Types.infer_type(100) == :unsigned32 + assert Types.infer_type(-100) == :integer + assert Types.infer_type(2_147_483_647) == :unsigned32 + end + + test "infers string types" do + assert Types.infer_type("hello") == :string + assert Types.infer_type("192.168.1.1") == :ip_address + end + + test "infers list types" do + assert Types.infer_type([1, 3, 6, 1]) == :object_identifier + assert Types.infer_type(~c"hello") == :string + end + + test "infers boolean types" do + assert Types.infer_type(true) == :boolean + assert Types.infer_type(false) == :boolean + end + + test "infers null types" do + assert Types.infer_type(:null) == :null + assert Types.infer_type(nil) == :null + end + + test "infers ip_address from tuple" do + assert Types.infer_type({192, 168, 1, 1}) == :ip_address + end + + test "returns unknown for unrecognized types" do + assert Types.infer_type(%{}) == :unknown + assert Types.infer_type({256, 0, 0, 0}) == :unknown + end + end + + describe "Value decoding" do + test "decodes string values" do + assert "hello" = Types.decode_value({:string, "hello"}) + assert "world" = Types.decode_value({:string, ~c"world"}) + end + + test "decodes octet string values" do + assert "test" = Types.decode_value({:octet_string, "test"}) + assert "data" = Types.decode_value({:octet_string, ~c"data"}) + end + + test "decodes integer values" do + assert 42 = Types.decode_value({:integer, 42}) + assert 100 = Types.decode_value({:unsigned32, 100}) + assert 200 = Types.decode_value({:counter32, 200}) + assert 300 = Types.decode_value({:gauge32, 300}) + assert 400 = Types.decode_value({:timeticks, 400}) + assert 500 = Types.decode_value({:counter64, 500}) + end + + test "decodes boolean values" do + assert true == Types.decode_value({:boolean, true}) + assert false == Types.decode_value({:boolean, false}) + end + + test "decodes null" do + assert nil == Types.decode_value({:null, :null}) + end + + test "decodes ip_address from tuple" do + assert "192.168.1.1" = Types.decode_value({:ip_address, {192, 168, 1, 1}}) + end + + test "decodes ip_address from binary" do + assert "192.168.1.1" = Types.decode_value({:ip_address, <<192, 168, 1, 1>>}) + end + + test "decodes oid" do + assert [1, 3, 6, 1] = Types.decode_value({:object_identifier, [1, 3, 6, 1]}) + assert [1, 3, 6, 1] = Types.decode_value({:oid, [1, 3, 6, 1]}) + end + + test "decodes opaque" do + assert <<1, 2, 3>> = Types.decode_value({:opaque, <<1, 2, 3>>}) + end + + test "decodes exception types" do + assert :no_such_object = Types.decode_value({:no_such_object, nil}) + assert :no_such_instance = Types.decode_value({:no_such_instance, nil}) + assert :end_of_mib_view = Types.decode_value({:end_of_mib_view, nil}) + end + + test "passes through untyped values" do + assert 42 = Types.decode_value(42) + assert "test" = Types.decode_value("test") + end + end + + describe "Value encoding" do + test "encodes with inferred type" do + assert {:ok, {:string, "hello"}} = Types.encode_value("hello") + assert {:ok, {:unsigned32, 42}} = Types.encode_value(42) + end + + test "encodes with explicit type" do + assert {:ok, {:counter32, 42}} = Types.encode_value(42, type: :counter32) + assert {:ok, {:string, "test"}} = Types.encode_value("test", type: :string) + end + + test "validates encoded values by default" do + assert {:error, :out_of_range} = Types.encode_value(-1, type: :counter32) + assert {:error, :out_of_range} = Types.encode_value(5_000_000_000, type: :counter32) + end + + test "skips validation when requested" do + assert {:ok, {:counter32, -1}} = Types.encode_value(-1, type: :counter32, validate: false) + end + + test "returns error for uninferable types" do + assert {:error, :cannot_infer_type} = Types.encode_value(%{}) + end + + test "encodes IP addresses" do + assert {:ok, {:ip_address, {192, 168, 1, 1}}} = + Types.encode_value("192.168.1.1", type: :ip_address) + end + + test "encodes OIDs" do + assert {:ok, {:object_identifier, [1, 3, 6, 1]}} = + Types.encode_value([1, 3, 6, 1], type: :object_identifier) + end + end + + describe "Unsigned32 validation" do + test "accepts valid unsigned32 values" do + assert :ok = Types.validate_unsigned32(0) + assert :ok = Types.validate_unsigned32(100) + assert :ok = Types.validate_unsigned32(4_294_967_295) + end + + test "rejects out of range unsigned32" do + assert {:error, :out_of_range} = Types.validate_unsigned32(-1) + assert {:error, :out_of_range} = Types.validate_unsigned32(4_294_967_296) + end + + test "rejects non-integer unsigned32" do + assert {:error, :not_integer} = Types.validate_unsigned32("42") + assert {:error, :not_integer} = Types.validate_unsigned32(nil) + end + end + + describe "Coercion edge cases" do + test "coerces ip_address from string" do + assert {:ok, {:ip_address, {192, 168, 1, 1}}} = + Types.coerce_value(:ip_address, "192.168.1.1") + end + + test "coerces unsigned32" do + assert {:ok, {:unsigned32, 42}} = Types.coerce_value(:unsigned32, 42) + assert {:error, :out_of_range} = Types.coerce_value(:unsigned32, -1) + end + + test "coerces octet_string" do + assert {:ok, {:octet_string, "test"}} = Types.coerce_value(:octet_string, "test") + end + + test "coerces object_identifier from list" do + assert {:ok, {:object_identifier, [1, 3, 6, 1]}} = + Types.coerce_value(:object_identifier, [1, 3, 6, 1]) + end + + test "coerces boolean" do + assert {:ok, {:boolean, true}} = Types.coerce_value(:boolean, true) + assert {:ok, {:boolean, false}} = Types.coerce_value(:boolean, false) + end + end end