credo improvements

This commit is contained in:
Graham McIntire 2026-01-23 13:40:49 -06:00
parent a0cee485a4
commit 7fcfdbf2e9
No known key found for this signature in database
13 changed files with 252 additions and 285 deletions

View file

@ -144,7 +144,8 @@
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
# Disabled: We use structured logging with dynamic metadata keys
# {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},

View file

@ -76,6 +76,9 @@ defmodule SnmpKit.SnmpLib.Error do
@not_writable 17
@inconsistent_name 18
# Errors that are typically transient and worth retrying
@retriable_errors MapSet.new([:too_big, :gen_err, :resource_unavailable])
## Standard Error Codes
@doc """
@ -351,28 +354,9 @@ defmodule SnmpKit.SnmpLib.Error do
"""
@spec retriable_error?(error_status()) :: boolean()
def retriable_error?(error_status) do
case error_atom(error_status) do
:no_error -> false
:too_big -> true
:no_such_name -> false
:bad_value -> false
:read_only -> false
:gen_err -> true
:no_access -> false
:wrong_type -> false
:wrong_length -> false
:wrong_encoding -> false
:wrong_value -> false
:no_creation -> false
:inconsistent_value -> false
:resource_unavailable -> true
:commit_failed -> false
:undo_failed -> false
:authorization_error -> false
:not_writable -> false
:inconsistent_name -> false
_ -> false
end
error_status
|> error_atom()
|> then(&MapSet.member?(@retriable_errors, &1))
end
@doc """

View file

@ -111,6 +111,30 @@ defmodule SnmpKit.SnmpLib.ErrorHandler do
quarantine_until: integer() | nil
}
# Error classification map for O(1) lookup
@error_classifications %{
# Network-related transient errors
:timeout => :transient,
:nxdomain => :transient,
:network_unreachable => :transient,
:connection_refused => :transient,
# Device overload (transient)
:device_busy => :transient,
:too_big => :transient,
:resource_unavailable => :transient,
# Permanent configuration errors
:authentication_failed => :permanent,
:community_mismatch => :permanent,
:unsupported_version => :permanent,
:no_such_name => :permanent,
:bad_value => :permanent,
:read_only => :permanent,
# Performance degradation
:slow_response => :degraded,
:partial_failure => :degraded,
:high_error_rate => :degraded
}
defstruct device_stats: %{},
global_stats: %{
total_operations: 0,
@ -351,33 +375,8 @@ defmodule SnmpKit.SnmpLib.ErrorHandler do
:degraded = SnmpKit.SnmpLib.ErrorHandler.classify_error(:slow_response)
"""
@spec classify_error(any()) :: error_class()
def classify_error(error) do
case error do
# Network-related transient errors
:timeout -> :transient
:nxdomain -> :transient
:network_unreachable -> :transient
:connection_refused -> :transient
{:network_error, _} -> :transient
# Device overload (transient)
:device_busy -> :transient
:too_big -> :transient
:resource_unavailable -> :transient
# Permanent configuration errors
:authentication_failed -> :permanent
:community_mismatch -> :permanent
:unsupported_version -> :permanent
:no_such_name -> :permanent
:bad_value -> :permanent
:read_only -> :permanent
# Performance degradation
:slow_response -> :degraded
:partial_failure -> :degraded
:high_error_rate -> :degraded
# Default to unknown for unclassified errors
_ -> :unknown
end
end
def classify_error({:network_error, _}), do: :transient
def classify_error(error), do: Map.get(@error_classifications, error, :unknown)
@doc """
Puts a device into quarantine for a specified duration.

View file

@ -13,6 +13,94 @@ defmodule SnmpKit.SnmpLib.MIB.SnmpTokenizer do
# State record equivalent
defstruct line: 1, chars: [], get_line_fun: nil
# Reserved words from SNMP/SMI - complete list from Erlang tokenizer
# Using a Map for O(1) lookup and reduced cyclomatic complexity
@reserved_words %{
"DEFINITIONS" => :DEFINITIONS,
"BEGIN" => :BEGIN,
"END" => :END,
"IMPORTS" => :IMPORTS,
"FROM" => :FROM,
"EXPORTS" => :EXPORTS,
"OBJECT" => :OBJECT,
"IDENTIFIER" => :IDENTIFIER,
"OBJECT-TYPE" => :"OBJECT-TYPE",
"SYNTAX" => :SYNTAX,
"ACCESS" => :ACCESS,
"MAX-ACCESS" => :"MAX-ACCESS",
"STATUS" => :STATUS,
"DESCRIPTION" => :DESCRIPTION,
"REFERENCE" => :REFERENCE,
"INDEX" => :INDEX,
"AUGMENTS" => :AUGMENTS,
"DEFVAL" => :DEFVAL,
"UNITS" => :UNITS,
"SEQUENCE" => :SEQUENCE,
"OF" => :OF,
"CHOICE" => :CHOICE,
"SIZE" => :SIZE,
"INTEGER" => :INTEGER,
"OCTET" => :OCTET,
"STRING" => :STRING,
"NULL" => :NULL,
"IpAddress" => :IpAddress,
"Counter" => :Counter,
"Counter32" => :Counter32,
"Counter64" => :Counter64,
"Gauge" => :Gauge,
"Gauge32" => :Gauge32,
"TimeTicks" => :TimeTicks,
"Unsigned32" => :Unsigned32,
"Integer32" => :Integer32,
"Opaque" => :Opaque,
"BITS" => :BITS,
"MODULE-IDENTITY" => :"MODULE-IDENTITY",
"OBJECT-IDENTITY" => :"OBJECT-IDENTITY",
"TEXTUAL-CONVENTION" => :"TEXTUAL-CONVENTION",
"OBJECT-GROUP" => :"OBJECT-GROUP",
"NOTIFICATION-GROUP" => :"NOTIFICATION-GROUP",
"MODULE-COMPLIANCE" => :"MODULE-COMPLIANCE",
"AGENT-CAPABILITIES" => :"AGENT-CAPABILITIES",
"NOTIFICATION-TYPE" => :"NOTIFICATION-TYPE",
"TRAP-TYPE" => :"TRAP-TYPE",
"LAST-UPDATED" => :"LAST-UPDATED",
"ORGANIZATION" => :ORGANIZATION,
"CONTACT-INFO" => :"CONTACT-INFO",
"REVISION" => :REVISION,
"DISPLAY-HINT" => :"DISPLAY-HINT",
"IMPLIED" => :IMPLIED,
"OBJECTS" => :OBJECTS,
"NOTIFICATIONS" => :NOTIFICATIONS,
"MANDATORY-GROUPS" => :"MANDATORY-GROUPS",
"GROUP" => :GROUP,
"MODULE" => :MODULE,
"WRITE-SYNTAX" => :"WRITE-SYNTAX",
"MIN-ACCESS" => :"MIN-ACCESS",
"PRODUCT-RELEASE" => :"PRODUCT-RELEASE",
"SUPPORTS" => :SUPPORTS,
"INCLUDES" => :INCLUDES,
"VARIATION" => :VARIATION,
"CREATION-REQUIRES" => :"CREATION-REQUIRES",
"ENTERPRISE" => :ENTERPRISE,
"VARIABLES" => :VARIABLES,
"APPLICATION" => :APPLICATION,
"IMPLICIT" => :IMPLICIT,
"EXPLICIT" => :EXPLICIT,
"UNIVERSAL" => :UNIVERSAL,
"PRIVATE" => :PRIVATE,
"MACRO" => :MACRO,
"TYPE" => :TYPE,
"NOTATION" => :NOTATION,
"VALUE" => :VALUE,
# Access values
"read-only" => :"read-only",
"read-write" => :"read-write",
"write-only" => :"write-only",
"not-accessible" => :"not-accessible",
"accessible-for-notify" => :"accessible-for-notify",
"read-create" => :"read-create"
}
@type state() :: %__MODULE__{
line: pos_integer(),
chars: charlist(),
@ -590,100 +678,5 @@ defmodule SnmpKit.SnmpLib.MIB.SnmpTokenizer do
end
end
# Reserved words from SNMP/SMI - complete list from Erlang tokenizer
defp reserved_word(word) do
case word do
"DEFINITIONS" -> :DEFINITIONS
"BEGIN" -> :BEGIN
"END" -> :END
"IMPORTS" -> :IMPORTS
"FROM" -> :FROM
"EXPORTS" -> :EXPORTS
"OBJECT" -> :OBJECT
"IDENTIFIER" -> :IDENTIFIER
"OBJECT-TYPE" -> :"OBJECT-TYPE"
"SYNTAX" -> :SYNTAX
"ACCESS" -> :ACCESS
"MAX-ACCESS" -> :"MAX-ACCESS"
"STATUS" -> :STATUS
"DESCRIPTION" -> :DESCRIPTION
"REFERENCE" -> :REFERENCE
"INDEX" -> :INDEX
"AUGMENTS" -> :AUGMENTS
"DEFVAL" -> :DEFVAL
"UNITS" -> :UNITS
"SEQUENCE" -> :SEQUENCE
"OF" -> :OF
"CHOICE" -> :CHOICE
"SIZE" -> :SIZE
"INTEGER" -> :INTEGER
"OCTET" -> :OCTET
"STRING" -> :STRING
"NULL" -> :NULL
"IpAddress" -> :IpAddress
"Counter" -> :Counter
"Counter32" -> :Counter32
"Counter64" -> :Counter64
"Gauge" -> :Gauge
"Gauge32" -> :Gauge32
"TimeTicks" -> :TimeTicks
"Unsigned32" -> :Unsigned32
"Integer32" -> :Integer32
"Opaque" -> :Opaque
"BITS" -> :BITS
"MODULE-IDENTITY" -> :"MODULE-IDENTITY"
"OBJECT-IDENTITY" -> :"OBJECT-IDENTITY"
"TEXTUAL-CONVENTION" -> :"TEXTUAL-CONVENTION"
"OBJECT-GROUP" -> :"OBJECT-GROUP"
"NOTIFICATION-GROUP" -> :"NOTIFICATION-GROUP"
"MODULE-COMPLIANCE" -> :"MODULE-COMPLIANCE"
"AGENT-CAPABILITIES" -> :"AGENT-CAPABILITIES"
"NOTIFICATION-TYPE" -> :"NOTIFICATION-TYPE"
"TRAP-TYPE" -> :"TRAP-TYPE"
"LAST-UPDATED" -> :"LAST-UPDATED"
"ORGANIZATION" -> :ORGANIZATION
"CONTACT-INFO" -> :"CONTACT-INFO"
"REVISION" -> :REVISION
"DISPLAY-HINT" -> :"DISPLAY-HINT"
"IMPLIED" -> :IMPLIED
"OBJECTS" -> :OBJECTS
"NOTIFICATIONS" -> :NOTIFICATIONS
"MANDATORY-GROUPS" -> :"MANDATORY-GROUPS"
"GROUP" -> :GROUP
"MODULE" -> :MODULE
"WRITE-SYNTAX" -> :"WRITE-SYNTAX"
"MIN-ACCESS" -> :"MIN-ACCESS"
"PRODUCT-RELEASE" -> :"PRODUCT-RELEASE"
"SUPPORTS" -> :SUPPORTS
"INCLUDES" -> :INCLUDES
"VARIATION" -> :VARIATION
"CREATION-REQUIRES" -> :"CREATION-REQUIRES"
"ENTERPRISE" -> :ENTERPRISE
"VARIABLES" -> :VARIABLES
"APPLICATION" -> :APPLICATION
"IMPLICIT" -> :IMPLICIT
"EXPLICIT" -> :EXPLICIT
"UNIVERSAL" -> :UNIVERSAL
"PRIVATE" -> :PRIVATE
"MACRO" -> :MACRO
"TYPE" -> :TYPE
"NOTATION" -> :NOTATION
"VALUE" -> :VALUE
# Status values - removed to let grammar handle context
# "current" -> :'current'
# "deprecated" -> :'deprecated'
# "obsolete" -> :'obsolete'
# Access values
"read-only" -> :"read-only"
"read-write" -> :"read-write"
"write-only" -> :"write-only"
"not-accessible" -> :"not-accessible"
"accessible-for-notify" -> :"accessible-for-notify"
"read-create" -> :"read-create"
# Special values - removed to let grammar handle context
# "mandatory" -> :'mandatory'
# "optional" -> :'optional'
_ -> nil
end
end
defp reserved_word(word), do: Map.get(@reserved_words, word)
end

View file

@ -344,42 +344,39 @@ defmodule SnmpKit.SnmpLib.Security do
## Private Implementation
defp validate_user_config(config) do
# Validate authentication protocol
auth_protocol = config[:auth_protocol] || :none
priv_protocol = config[:priv_protocol] || :none
if auth_protocol in [:none, :md5, :sha1, :sha256, :sha384, :sha512] do
# Validate privacy protocol
priv_protocol = config[:priv_protocol] || :none
if priv_protocol in [:none, :des, :aes128, :aes192, :aes256] do
# Validate protocol compatibility
if priv_protocol != :none and auth_protocol == :none do
{:error, :priv_requires_auth}
else
# Validate required passwords
if auth_protocol != :none and is_nil(config[:auth_password]) do
{:error, :missing_auth_password}
else
if priv_protocol != :none and is_nil(config[:priv_password]) do
{:error, :missing_priv_password}
else
# Validate engine ID
if is_nil(config[:engine_id]) do
{:error, :missing_engine_id}
else
{:ok, config}
end
end
end
end
else
{:error, :invalid_priv_protocol}
end
else
{:error, :invalid_auth_protocol}
with :ok <- validate_auth_protocol(auth_protocol),
:ok <- validate_priv_protocol(priv_protocol),
:ok <- validate_protocol_compatibility(auth_protocol, priv_protocol),
:ok <- validate_required_passwords(config, auth_protocol, priv_protocol),
:ok <- validate_engine_id(config) do
{:ok, config}
end
end
defp validate_auth_protocol(protocol) when protocol in [:none, :md5, :sha1, :sha256, :sha384, :sha512], do: :ok
defp validate_auth_protocol(_), do: {:error, :invalid_auth_protocol}
defp validate_priv_protocol(protocol) when protocol in [:none, :des, :aes128, :aes192, :aes256], do: :ok
defp validate_priv_protocol(_), do: {:error, :invalid_priv_protocol}
defp validate_protocol_compatibility(:none, priv) when priv != :none, do: {:error, :priv_requires_auth}
defp validate_protocol_compatibility(_, _), do: :ok
defp validate_required_passwords(config, auth_protocol, priv_protocol) do
cond do
auth_protocol != :none and is_nil(config[:auth_password]) -> {:error, :missing_auth_password}
priv_protocol != :none and is_nil(config[:priv_password]) -> {:error, :missing_priv_password}
true -> :ok
end
end
defp validate_engine_id(config) do
if is_nil(config[:engine_id]), do: {:error, :missing_engine_id}, else: :ok
end
defp derive_auth_key(config) do
case config[:auth_protocol] do
:none ->

View file

@ -583,18 +583,22 @@ defmodule SnmpKit.SnmpLib.Utils do
# Standard host:port parsing for IPv4 and simple hostnames
true ->
case String.split(target_str, ":", parts: 2) do
[host_str] ->
{host_str, nil}
parse_standard_host_port(target_str)
end
end
[host_str, port_str] ->
# Check if host part looks like IPv4 or simple hostname
if ipv4_or_simple_hostname?(host_str) do
{host_str, port_str}
else
# Complex hostname with colons, treat as hostname without port
{target_str, nil}
end
defp parse_standard_host_port(target_str) do
case String.split(target_str, ":", parts: 2) do
[host_str] ->
{host_str, nil}
[host_str, port_str] ->
# Check if host part looks like IPv4 or simple hostname
if ipv4_or_simple_hostname?(host_str) do
{host_str, port_str}
else
# Complex hostname with colons, treat as hostname without port
{target_str, nil}
end
end
end
@ -667,58 +671,46 @@ defmodule SnmpKit.SnmpLib.Utils do
end
end
defp valid_ip_tuple?({a, b, c, d}) when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) do
a >= 0 and a <= 255 and
b >= 0 and b <= 255 and
c >= 0 and c <= 255 and
d >= 0 and d <= 255
defp valid_ip_tuple?({a, b, c, d}) do
Enum.all?([a, b, c, d], &valid_ipv4_octet?/1)
end
defp valid_ip_tuple?(_), do: false
defp valid_ipv6_tuple?({a, b, c, d, e, f, g, h})
when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) and is_integer(e) and is_integer(f) and
is_integer(g) and is_integer(h) do
a >= 0 and a <= 65_535 and
b >= 0 and b <= 65_535 and
c >= 0 and c <= 65_535 and
d >= 0 and d <= 65_535 and
e >= 0 and e <= 65_535 and
f >= 0 and f <= 65_535 and
g >= 0 and g <= 65_535 and
h >= 0 and h <= 65_535
defp valid_ipv4_octet?(n), do: is_integer(n) and n >= 0 and n <= 255
defp valid_ipv6_tuple?({a, b, c, d, e, f, g, h}) do
Enum.all?([a, b, c, d, e, f, g, h], &valid_ipv6_segment?/1)
end
defp valid_ipv6_tuple?(_), do: false
defp valid_ipv6_segment?(n), do: is_integer(n) and n >= 0 and n <= 65_535
defp ipv4_or_simple_hostname?(host_str) do
# Check if it looks like IPv4 (has 3 dots and only digits/dots)
case String.split(host_str, ".") do
[a, b, c, d] ->
# Looks like IPv4, check if all parts are numeric
Enum.all?([a, b, c, d], fn part ->
case Integer.parse(part) do
{num, ""} when num >= 0 and num <= 255 -> true
_ -> false
end
end)
_ ->
# Not IPv4 format, check if it's a simple hostname
# Exclude anything that looks like IPv6 (contains :: or multiple colons)
cond do
# IPv6
String.contains?(host_str, "::") -> false
# IPv6 or complex
host_str |> String.graphemes() |> Enum.count(&(&1 == ":")) > 1 -> false
# Contains colon, not simple
String.contains?(host_str, ":") -> false
# Simple hostname pattern
true -> String.match?(host_str, ~r/^[a-zA-Z0-9\-\.\_]+$/)
end
[a, b, c, d] -> Enum.all?([a, b, c, d], &valid_ipv4_octet_string?/1)
_ -> simple_hostname?(host_str)
end
end
defp valid_ipv4_octet_string?(part) do
case Integer.parse(part) do
{num, ""} when num >= 0 and num <= 255 -> true
_ -> false
end
end
defp simple_hostname?(host_str) do
# Not IPv4 format, check if it's a simple hostname
# Exclude anything that looks like IPv6 (contains :: or multiple colons)
not String.contains?(host_str, "::") and
count_colons(host_str) <= 1 and
not String.contains?(host_str, ":") and
String.match?(host_str, ~r/^[a-zA-Z0-9\-\.\_]+$/)
end
defp format_pdu_type(:get_request), do: "GET Request"
defp format_pdu_type(:get_next_request), do: "GET-NEXT Request"
defp format_pdu_type(:get_bulk_request), do: "GET-BULK Request"

View file

@ -29,6 +29,27 @@ defmodule SnmpKit.SnmpMgr.Core do
@type oid :: binary() | list(non_neg_integer())
@type opts :: keyword()
# Type normalization map for manager-expected atoms
@type_normalization %{
:string => :string,
:octet_string => :string,
:octetString => :string,
:integer => :integer,
:unsigned32 => :integer,
:gauge32 => :gauge32,
:counter32 => :counter32,
:counter64 => :counter64,
:timeticks => :timeticks,
:timeTicks => :timeticks,
:ip_address => :ip_address,
:ipAddress => :ip_address,
:object_identifier => :object_identifier,
:objectId => :object_identifier,
:oid => :object_identifier,
:null => :null,
:opaque => :opaque
}
@doc """
Sends an SNMP GET request and returns the response.
@ -266,28 +287,7 @@ defmodule SnmpKit.SnmpMgr.Core do
# Normalize typed values to the atoms that SnmpKit.SnmpLib.Manager expects
defp normalize_manager_typed({type, val}) when is_atom(type) do
mapped_type =
case type do
:string -> :string
:octet_string -> :string
:octetString -> :string
:integer -> :integer
:unsigned32 -> :integer
:gauge32 -> :gauge32
:counter32 -> :counter32
:counter64 -> :counter64
:timeticks -> :timeticks
:timeTicks -> :timeticks
:ip_address -> :ip_address
:ipAddress -> :ip_address
:object_identifier -> :object_identifier
:objectId -> :object_identifier
:oid -> :object_identifier
:null -> :null
:opaque -> :opaque
other -> other
end
mapped_type = Map.get(@type_normalization, type, type)
{mapped_type, val}
end
@ -539,16 +539,7 @@ defmodule SnmpKit.SnmpMgr.Core do
cond do
# RFC 3986 bracket notation: [IPv6]:port
String.starts_with?(target, "[") and String.contains?(target, "]:") ->
case String.split(target, "]:", parts: 2) do
[_ipv6_part, port_part] ->
case Integer.parse(port_part) do
{port, ""} when port > 0 and port <= 65_535 -> true
_ -> false
end
_ ->
false
end
target |> extract_bracket_port() |> valid_port_string?()
# Plain IPv6 addresses (contain :: or multiple colons) - no port embedded
String.contains?(target, "::") ->
@ -559,16 +550,7 @@ defmodule SnmpKit.SnmpMgr.Core do
# IPv4 or simple hostname with port
String.contains?(target, ":") ->
case String.split(target, ":", parts: 2) do
[_host_part, port_part] ->
case Integer.parse(port_part) do
{port, ""} when port > 0 and port <= 65_535 -> true
_ -> false
end
_ ->
false
end
target |> extract_simple_port() |> valid_port_string?()
# No colon at all
true ->
@ -577,4 +559,27 @@ defmodule SnmpKit.SnmpMgr.Core do
end
defp target_contains_port?(_), do: false
defp extract_bracket_port(target) do
case String.split(target, "]:", parts: 2) do
[_ipv6_part, port_part] -> port_part
_ -> nil
end
end
defp extract_simple_port(target) do
case String.split(target, ":", parts: 2) do
[_host_part, port_part] -> port_part
_ -> nil
end
end
defp valid_port_string?(nil), do: false
defp valid_port_string?(port_str) do
case Integer.parse(port_str) do
{port, ""} when port > 0 and port <= 65_535 -> true
_ -> false
end
end
end

View file

@ -74,7 +74,7 @@ defmodule SnmpKit.SnmpLib.MIB.CompilerTest do
assert {:error, errors} = Compiler.compile_string(mib_content)
assert is_list(errors)
assert length(errors) > 0
assert errors != []
end
test "respects compile options" do

View file

@ -26,7 +26,7 @@ defmodule SnmpKit.SnmpLib.MIB.ComprehensiveMibTest do
{:ok, files} ->
mib_files = filter_mib_files(files)
assert length(mib_files) > 0, "No MIB files found in #{unquote(dir_name)} directory"
assert mib_files != [], "No MIB files found in #{unquote(dir_name)} directory"
results = test_mib_files(dir_path, mib_files)
@ -34,9 +34,9 @@ defmodule SnmpKit.SnmpLib.MIB.ComprehensiveMibTest do
failed = Enum.filter(results, fn {status, _} -> status == :error end)
# Log results for visibility
IO.puts("\n#{String.upcase(unquote(dir_name))} MIBs: #{successful}/#{length(mib_files)} successful")
IO.puts("\n#{String.upcase(unquote(dir_name))} MIBs: #{successful}/#{Enum.count(mib_files)} successful")
if length(failed) > 0 do
if failed != [] do
IO.puts("Failed files:")
for {:error, {file, reason}} <- failed do

View file

@ -52,7 +52,7 @@ defmodule SnmpKit.SnmpLib.MIB.DocsisMibTest do
not match?({:ok, _}, result) and not match?({:warning, _, _}, result)
end)
if length(failed_mibs) > 0 do
if failed_mibs != [] do
error_details =
Enum.map(failed_mibs, fn
{name, {:error, errors}} when is_list(errors) ->
@ -72,13 +72,13 @@ defmodule SnmpKit.SnmpLib.MIB.DocsisMibTest do
{:ok, mib} ->
assert mib.name != nil, "#{name} should have a valid MIB name"
assert is_list(mib.definitions), "#{name} should have definitions list"
assert length(mib.definitions) > 0, "#{name} should have at least one definition"
assert mib.definitions != [], "#{name} should have at least one definition"
{:warning, mib, warnings} ->
assert mib.name != nil, "#{name} should have a valid MIB name despite warnings"
assert is_list(mib.definitions), "#{name} should have definitions list"
assert length(warnings) > 0,
assert warnings != [],
"#{name} should have warnings if returning warning result"
{:error, _} ->
@ -136,7 +136,7 @@ defmodule SnmpKit.SnmpLib.MIB.DocsisMibTest do
# Should contain MODULE-COMPLIANCE for QoS conformance
# Note: Some MIBs might not have module compliance definitions
# Just verify parsing succeeded
assert length(mib.definitions) > 0, "Should have parsed at least some definitions"
assert mib.definitions != [], "Should have parsed at least some definitions"
end
end

View file

@ -39,8 +39,8 @@ defmodule SnmpKit.SnmpLib.MIB.ParserTest do
assert {:ok, mib} = Parser.parse(mib_content)
assert %{__type__: :mib, name: "TEST-MIB"} = mib
assert length(mib.imports) == 1
assert length(mib.definitions) >= 1
assert match?([_], mib.imports)
assert mib.definitions != []
end
test "parses simple object identifier assignment" do
@ -145,8 +145,8 @@ defmodule SnmpKit.SnmpLib.MIB.ParserTest do
assert {:ok, mib} = Parser.parse(mib_content)
assert %{__type__: :mib, name: "TEST-MIB"} = mib
assert length(mib.imports) >= 1
assert length(mib.definitions) >= 1
assert mib.imports != []
assert mib.definitions != []
end
test "handles comments in MIB content" do

View file

@ -365,7 +365,7 @@ defmodule SnmpKit.SnmpLib.SNMPv3EdgeCasesTest do
case V3Encoder.decode_message(encoded, user) do
{:ok, decoded} ->
# Verify we have at least one varbind and it's reasonably long
assert length(decoded.msg_data.pdu.varbinds) >= 1
assert decoded.msg_data.pdu.varbinds != []
[{decoded_oid, _, _} | _] = decoded.msg_data.pdu.varbinds
# Allow some tolerance for encoding limits
assert length(decoded_oid) >= 50

View file

@ -2,10 +2,6 @@ defmodule SnmpKit.SnmpLib.SNMPv3IntegrationTest do
use ExUnit.Case, async: false
alias SnmpKit.SnmpLib.PDU.V3Encoder
alias SnmpKit.SnmpLib.Security
alias SnmpKit.SnmpLib.Security.Keys
alias SnmpKit.SnmpLib.Security.Priv
alias SnmpKit.SnmpLib.Security.USM
@moduletag :snmpv3