Files
netbox-librenms-plugin/netbox_librenms_plugin/tests/test_librenms_api.py
Vlastislav Svatek 673e67106e
Some checks failed
ci / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
first commit
2026-06-05 10:39:05 +02:00

1348 lines
51 KiB
Python

"""
Comprehensive tests for LibreNMSAPI client.
This module provides 100% test coverage for netbox_librenms_plugin/librenms_api.py,
with particular focus on HTTP method correctness to prevent regression bugs.
"""
from unittest.mock import MagicMock, patch
import pytest
import requests
# Import the autouse fixture from helpers
pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"]
# =============================================================================
# Test Class 1: Initialization (3 tests)
# =============================================================================
class TestLibreNMSAPIInit:
"""Test LibreNMSAPI initialization and configuration loading."""
def test_init_with_multi_server_config(self, mock_librenms_config):
"""Verify initialization with multi-server configuration."""
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
assert api.librenms_url == "https://librenms.example.com"
assert api.api_token == "test-token"
assert api.cache_timeout == 300
assert api.verify_ssl is True
def test_init_with_legacy_config(self, mock_librenms_config):
"""Verify initialization with legacy single-server config."""
mock_config = mock_librenms_config["mock_config"]
# Return None for 'servers' to trigger legacy path
def config_side_effect(plugin, key, default=None):
if key == "servers":
return None
legacy = {
"librenms_url": "https://legacy.example.com",
"api_token": "legacy-token",
"cache_timeout": 600,
"verify_ssl": False,
}
return legacy.get(key, default)
mock_config.side_effect = config_side_effect
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI()
assert api.librenms_url == "https://legacy.example.com"
def test_init_missing_config_raises_valueerror(self, mock_librenms_config):
"""Verify ValueError raised when configuration is missing."""
mock_config = mock_librenms_config["mock_config"]
mock_config.return_value = None
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
with pytest.raises(ValueError):
LibreNMSAPI(server_key="nonexistent")
def test_init_nonexistent_server_key_raises_keyerror(self, mock_librenms_config):
"""Verify KeyError raised when specific server_key doesn't exist."""
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
with pytest.raises(KeyError, match="nonexistent"):
LibreNMSAPI(server_key="nonexistent")
def test_init_default_falls_back_to_first_server(self, mock_librenms_config):
"""Verify 'default' key falls back to first configured server."""
mock_config = mock_librenms_config["mock_config"]
mock_config.return_value = {
"primary": {
"librenms_url": "https://primary.example.com",
"api_token": "primary-token",
}
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
assert api.server_key == "primary"
assert api.librenms_url == "https://primary.example.com"
# =============================================================================
# Test Class 2: Connection Testing (4 tests)
# =============================================================================
class TestLibreNMSAPIConnection:
"""Test connection testing functionality."""
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_connection_success(self, mock_get, mock_librenms_config):
"""Verify successful connection test."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"system": [{"local_ver": "24.1.0"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.test_connection()
assert result is not None
assert "error" not in result
mock_get.assert_called_once()
assert "/api/v0/system" in mock_get.call_args[0][0]
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_connection_auth_failure_401(self, mock_get, mock_librenms_config):
"""Verify 401 unauthorized handling."""
mock_get.return_value.status_code = 401
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.test_connection()
assert result.get("error") is True
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_connection_auth_failure_403(self, mock_get, mock_librenms_config):
"""Verify 403 forbidden handling."""
mock_get.return_value.status_code = 403
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.test_connection()
assert result.get("error") is True
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_connection_timeout(self, mock_get, mock_librenms_config):
"""Verify timeout exception handling."""
mock_get.side_effect = requests.exceptions.Timeout("Connection timed out")
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.test_connection()
assert result.get("error") is True
assert "timeout" in result.get("message", "").lower()
# =============================================================================
# Test Class 3: HTTP Methods - CRITICAL (18 tests)
# =============================================================================
class TestLibreNMSAPIHttpMethods:
"""
CRITICAL: Verify each API method uses the correct HTTP verb.
These tests prevent regression bugs where HTTP methods are accidentally
changed during refactoring (e.g., GET changed to DELETE).
"""
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.post")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_info_uses_get(self, mock_get, mock_post, mock_delete, mock_librenms_config):
"""Verify get_device_info uses GET, never DELETE."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 1}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_device_info(device_id=1)
mock_get.assert_called_once()
mock_delete.assert_not_called()
mock_post.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_device_uses_post(self, mock_post, mock_get, mock_delete, mock_librenms_config):
"""Verify add_device uses POST."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"status": "ok", "device_id": 42}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.add_device(
data={
"hostname": "test.example.com",
"snmp_version": "v2c",
"community": "public",
}
)
mock_post.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.patch")
def test_update_device_field_uses_patch(self, mock_patch, mock_delete, mock_librenms_config):
"""Verify update_device_field uses PATCH."""
mock_patch.return_value.status_code = 200
mock_patch.return_value.json.return_value = {"status": "ok"}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.update_device_field(device_id=1, field_data={"field": ["location"], "data": ["DC1"]})
mock_patch.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_id_by_ip_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_device_id_by_ip uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 10}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_device_id_by_ip("192.168.1.1")
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_id_by_hostname_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_device_id_by_hostname uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 20}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_device_id_by_hostname("test-host")
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_ports_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_ports uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "ports": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_ports(device_id=1)
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_locations_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_locations uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "locations": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_locations()
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_location_uses_post(self, mock_post, mock_delete, mock_librenms_config):
"""Verify add_location uses POST."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"status": "ok",
"message": "Location created #1",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.add_location(location_data={"location": "DC1"})
mock_post.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.patch")
def test_update_location_uses_patch(self, mock_patch, mock_delete, mock_librenms_config):
"""Verify update_location uses PATCH."""
mock_patch.return_value.status_code = 200
mock_patch.return_value.json.return_value = {
"status": "ok",
"message": "Location updated",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.update_location(location_name="DC1", location_data={"location": "DC1-Updated"})
mock_patch.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_links_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_device_links uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "links": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_device_links(device_id=1)
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_ips_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_device_ips uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "addresses": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_device_ips(device_id=1)
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_port_by_id_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_port_by_id uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "port": [{}]}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_port_by_id(port_id=1)
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_inventory_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_device_inventory uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "inventory": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_device_inventory(device_id=1)
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_poller_groups_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_poller_groups uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "groups": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_poller_groups()
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_inventory_filtered_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify get_inventory_filtered uses GET."""
mock_get.return_value.status_code = 200
# Return non-empty inventory so it doesn't fall back to get_device_inventory
mock_get.return_value.json.return_value = {
"status": "ok",
"inventory": [{"entPhysicalClass": "chassis"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.get_inventory_filtered(device_id=1, ent_physical_class="chassis")
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.delete")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_list_devices_uses_get(self, mock_get, mock_delete, mock_librenms_config):
"""Verify list_devices uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "devices": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.list_devices()
mock_get.assert_called_once()
mock_delete.assert_not_called()
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_test_connection_uses_get(self, mock_get, mock_librenms_config):
"""Verify test_connection uses GET."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok"}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
api.test_connection()
mock_get.assert_called_once()
# ====================================================================================
# Test Class 4: Device Lookup (6 tests)
# ====================================================================================
class TestLibreNMSAPIDeviceLookup:
"""Test device lookup functionality."""
def test_get_librenms_id_from_custom_field(self, mock_librenms_config):
"""Returns ID when already stored in cf['librenms_id']."""
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
device = MagicMock()
device.name = "test-device"
device.cf = {"librenms_id": 42}
result = api.get_librenms_id(device)
assert result == 42
def test_get_librenms_id_normalizes_string_to_int(self, mock_librenms_config):
"""Returns int for string-stored librenms_id; read path uses auto_save=False so no write-back."""
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
device = MagicMock()
device.name = "test-device"
device.cf = {"librenms_id": "42"}
device.custom_field_data = {"librenms_id": "42"}
result = api.get_librenms_id(device)
assert result == 42
# auto_save=False in this read path — normalization is NOT written back
device.save.assert_not_called()
def test_get_librenms_id_empty_string_falls_through_to_discovery(self, mock_librenms_config):
"""An empty-string librenms_id is treated as not set (falls through to API discovery)."""
from unittest.mock import patch
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
device = MagicMock()
device.name = "test-device"
device.cf = {"librenms_id": ""}
device.primary_ip = None
with patch.object(api, "get_device_id_by_hostname", return_value=None):
result = api.get_librenms_id(device)
assert result is None
@patch("netbox_librenms_plugin.librenms_api.cache")
def test_get_librenms_id_from_cache(self, mock_cache, mock_librenms_config):
"""Returns ID from Django cache when not in custom field."""
mock_cache.get.return_value = 99
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
device = MagicMock()
device.name = "test-device"
device.cf = {}
result = api.get_librenms_id(device)
assert result == 99
@patch("netbox_librenms_plugin.librenms_api.cache")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_librenms_id_by_ip_lookup(self, mock_get, mock_cache, mock_librenms_config):
"""Performs IP lookup and caches result."""
mock_cache.get.return_value = None
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 55}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
device = MagicMock()
device.name = "test-device"
device.cf = {}
device.primary_ip4 = MagicMock()
device.primary_ip4.address = MagicMock()
device.primary_ip4.address.ip = "10.0.0.1"
result = api.get_librenms_id(device)
assert result == 55
mock_cache.set.assert_called_once()
@patch("netbox_librenms_plugin.librenms_api.cache")
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_librenms_id_by_hostname_lookup(self, mock_get, mock_cache, mock_librenms_config):
"""Falls back to hostname lookup."""
mock_cache.get.return_value = None
# First call (IP lookup) returns empty, second call (hostname lookup) returns device
call_count = [0]
def side_effect(*args, **kwargs):
resp = MagicMock()
resp.status_code = 200
call_count[0] += 1
if call_count[0] == 2: # Second call is hostname lookup
resp.json.return_value = {
"status": "ok",
"devices": [{"device_id": 77}],
}
else:
resp.json.return_value = {"status": "ok", "devices": []}
return resp
mock_get.side_effect = side_effect
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
device = MagicMock()
device.name = "test-device"
device.cf = {}
device.primary_ip4 = MagicMock()
device.primary_ip4.address = MagicMock()
device.primary_ip4.address.ip = "10.0.0.1"
result = api.get_librenms_id(device)
assert result == 77
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_id_by_ip_not_found(self, mock_get, mock_librenms_config):
"""Returns None when IP not found in LibreNMS."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "devices": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.get_device_id_by_ip("192.168.99.99")
assert result is None
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_id_by_hostname_not_found(self, mock_get, mock_librenms_config):
"""Returns None when hostname not found."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "devices": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.get_device_id_by_hostname("nonexistent-host")
assert result is None
# =============================================================================
# Test Class 5: Device Operations (6 tests)
# =============================================================================
class TestLibreNMSAPIDeviceOperations:
"""Test device CRUD operations."""
pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"]
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_device_success(self, mock_post, mock_librenms_config):
"""Verify successful device addition."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"status": "ok",
"message": "Device added successfully",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.add_device(
data={
"hostname": "test-device.example.com",
"snmp_version": "v2c",
"community": "public",
}
)
assert result[0] is True
assert result[1] == "Device added successfully."
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_device_snmpv1_success(self, mock_post, mock_librenms_config):
"""Verify successful device addition using SNMPv1."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"status": "ok",
"message": "Device added successfully",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.add_device(
data={
"hostname": "legacy-device.example.com",
"snmp_version": "v1",
"community": "public",
}
)
assert result[0] is True
assert result[1] == "Device added successfully."
# Verify the payload includes correct snmpver and community
call_args = mock_post.call_args
payload = call_args.kwargs.get("json") or call_args[1].get("json")
assert payload["snmpver"] == "v1"
assert payload["community"] == "public"
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_device_duplicate_error(self, mock_post, mock_librenms_config):
"""Verify duplicate device handling."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"status": "error",
"message": "Device already exists",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.add_device(
data={
"hostname": "duplicate-device.example.com",
"snmp_version": "v2c",
"community": "public",
}
)
assert result[0] is False
assert "Device already exists" in result[1]
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_device_snmpv3_success(self, mock_post, mock_librenms_config):
"""Verify successful device addition using SNMPv3 with all required fields."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"status": "ok",
"message": "Device added successfully",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.add_device(
data={
"hostname": "secure-device.example.com",
"snmp_version": "v3",
"authlevel": "authPriv",
"authname": "snmpuser",
"authpass": "authpassword123",
"authalgo": "SHA",
"cryptopass": "cryptopassword456",
"cryptoalgo": "AES",
}
)
assert result[0] is True
assert result[1] == "Device added successfully."
# Verify the payload includes correct snmpver and all v3 fields
call_args = mock_post.call_args
payload = call_args.kwargs.get("json") or call_args[1].get("json")
assert payload["snmpver"] == "v3"
assert payload["authlevel"] == "authPriv"
assert payload["authname"] == "snmpuser"
assert payload["authpass"] == "authpassword123"
assert payload["authalgo"] == "SHA"
assert payload["cryptopass"] == "cryptopassword456"
assert payload["cryptoalgo"] == "AES"
# Ensure community is NOT included for v3
assert "community" not in payload
@patch("netbox_librenms_plugin.librenms_api.requests.patch")
def test_update_device_field_success(self, mock_patch, mock_librenms_config):
"""Verify successful device field update."""
mock_patch.return_value.status_code = 200
mock_patch.return_value.json.return_value = {
"status": "ok",
"message": "Device field updated",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, message = api.update_device_field(device_id=123, field_data={"field": "notes", "data": "Updated note"})
assert success is True
assert "updated" in message.lower()
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_info_success(self, mock_get, mock_librenms_config):
"""Verify retrieving device info."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 123, "hostname": "test-device"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, device_data = api.get_device_info(device_id=123)
assert success is True
assert device_data is not None
assert device_data["device_id"] == 123
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_info_not_found(self, mock_get, mock_librenms_config):
"""Empty devices list returns (False, None) without raising."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "devices": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, result = api.get_device_info(1)
assert success is False
assert result is None
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_list_devices_with_filters(self, mock_get, mock_librenms_config):
"""Verify listing devices with filter parameter."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 1}, {"device_id": 2}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, devices = api.list_devices(filters={"type": "network"})
assert success is True
assert len(devices) == 2
# =============================================================================
# Test Class 6: Location Operations (4 tests)
# =============================================================================
class TestLibreNMSAPILocationOperations:
"""Test location CRUD operations."""
pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"]
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_locations_success(self, mock_get, mock_librenms_config):
"""Verify retrieving all locations."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"locations": [{"id": 1, "location": "DC1"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, locations = api.get_locations()
assert success is True
assert len(locations) == 1
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_location_success(self, mock_post, mock_librenms_config):
"""Verify successful location addition."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"status": "ok",
"message": "Location created #5",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, result_dict = api.add_location(location_data={"location": "DC2"})
assert success is True
assert result_dict["id"] == "5"
assert "message" in result_dict
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_add_location_error(self, mock_post, mock_librenms_config):
"""Verify location addition error handling."""
mock_post.return_value.status_code = 500
mock_post.return_value.json.return_value = {
"status": "error",
"message": "Invalid location data",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, error_msg = api.add_location(location_data={})
assert success is False
assert "Invalid location data" in error_msg
@patch("netbox_librenms_plugin.librenms_api.requests.patch")
def test_update_location_success(self, mock_patch, mock_librenms_config):
"""Verify successful location update."""
mock_patch.return_value.status_code = 200
mock_patch.return_value.json.return_value = {
"status": "ok",
"message": "Location updated",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, message = api.update_location(location_name="DC1", location_data={"location": "DC1-Updated"})
assert success is True
@patch("netbox_librenms_plugin.librenms_api.requests.patch")
def test_update_location_not_found(self, mock_patch, mock_librenms_config):
"""Verify updating non-existent location."""
mock_patch.return_value.status_code = 404
mock_patch.return_value.json.return_value = {
"status": "error",
"message": "Location not found",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, message = api.update_location(location_name="NonExistent", location_data={})
assert success is False
# =============================================================================
# Test Class 7: Ports and Inventory (9 tests)
# =============================================================================
class TestLibreNMSAPIPortsAndInventory:
"""Test ports and inventory operations."""
pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"]
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_ports_all(self, mock_get, mock_librenms_config):
"""Verify retrieving all ports for a device."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"ports": [{"port_id": 1}, {"port_id": 2}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, data = api.get_ports(device_id=123)
assert success is True
assert "ports" in data
assert len(data["ports"]) == 2
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_port_by_id_success(self, mock_get, mock_librenms_config):
"""Verify retrieving port by ID."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"port": [{"port_id": 1, "ifName": "GigabitEthernet0/1"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, port_data = api.get_port_by_id(port_id=1)
assert success is True
assert port_data is not None
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_port_by_id_error(self, mock_get, mock_librenms_config):
"""Verify handling of port retrieval error."""
mock_get.side_effect = requests.exceptions.RequestException("Connection error")
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, error_msg = api.get_port_by_id(port_id=999)
assert success is False
assert isinstance(error_msg, str)
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_inventory_success(self, mock_get, mock_librenms_config):
"""Verify retrieving device inventory."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"inventory": [{"entPhysicalClass": "chassis"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, inventory = api.get_device_inventory(device_id=123)
assert success is True
assert len(inventory) == 1
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_inventory_filtered_by_class(self, mock_get, mock_librenms_config):
"""Verify filtering inventory by physical class."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"inventory": [{"entPhysicalClass": "chassis", "entPhysicalName": "Chassis"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, inventory = api.get_inventory_filtered(device_id=123, ent_physical_class="chassis")
assert success is True
assert len(inventory) == 1
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_inventory_filtered_by_container(self, mock_get, mock_librenms_config):
"""Verify filtering inventory by container."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"inventory": [{"entPhysicalContainedIn": "0"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, inventory = api.get_inventory_filtered(device_id=123, ent_physical_contained_in=0)
assert success is True
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_links_success(self, mock_get, mock_librenms_config):
"""Verify retrieving device links."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"links": [{"id": 1, "local_port_id": 10, "remote_port_id": 20}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, links_dict = api.get_device_links(device_id=123)
assert success is True
assert "links" in links_dict
assert len(links_dict["links"]) == 1
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_ips_success(self, mock_get, mock_librenms_config):
"""Verify retrieving device IP addresses."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"addresses": [{"ipv4_address": "10.0.0.1"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, ips = api.get_device_ips(device_id=123)
assert success is True
assert len(ips) == 1
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_device_ips_empty(self, mock_get, mock_librenms_config):
"""Verify handling device with no IPs."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "addresses": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, ips = api.get_device_ips(device_id=123)
assert success is True
assert len(ips) == 0
# =============================================================================
# Test Class 8: Poller and Devices (4 tests)
# =============================================================================
class TestLibreNMSAPIPollerAndDevices:
"""Test poller groups and device listing operations."""
pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"]
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_poller_groups_success(self, mock_get, mock_librenms_config):
"""Verify retrieving poller groups."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"get_poller_group": [{"id": 1, "group_name": "primary"}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, groups = api.get_poller_groups()
assert success is True
assert len(groups) == 1
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_get_poller_groups_empty(self, mock_get, mock_librenms_config):
"""Verify handling empty poller groups."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"get_poller_group": [],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, groups = api.get_poller_groups()
assert success is True
assert len(groups) == 0
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_list_devices_all(self, mock_get, mock_librenms_config):
"""Verify listing all devices."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {
"status": "ok",
"devices": [{"device_id": 1}, {"device_id": 2}, {"device_id": 3}],
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, devices = api.list_devices()
assert success is True
assert len(devices) == 3
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_list_devices_empty(self, mock_get, mock_librenms_config):
"""Verify handling empty device list."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok", "devices": []}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, devices = api.list_devices()
assert success is True
assert len(devices) == 0
# =============================================================================
# Test Class 9: Error Handling (6 tests)
# =============================================================================
class TestLibreNMSAPIErrorHandling:
"""Test error handling and edge cases."""
pytest_plugins = ["netbox_librenms_plugin.tests.test_librenms_api_helpers"]
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_network_error_handling(self, mock_get, mock_librenms_config):
"""Verify handling of network errors."""
mock_get.side_effect = requests.exceptions.ConnectionError("Network unreachable")
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, result = api.get_device_info(device_id=123)
assert success is False
assert result is None
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_timeout_error_handling(self, mock_get, mock_librenms_config):
"""Verify handling of timeout errors."""
mock_get.side_effect = requests.exceptions.Timeout("Request timed out")
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, result = api.get_device_info(device_id=123)
assert success is False
assert result is None
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_invalid_json_response(self, mock_get, mock_librenms_config):
"""Verify handling of invalid JSON responses — ValueError is now caught gracefully."""
mock_get.return_value.status_code = 200
mock_get.return_value.json.side_effect = ValueError("Invalid JSON")
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
# ValueError is now caught, returning (False, None) instead of propagating
success, result = api.get_device_info(device_id=123)
assert success is False
assert result is None
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_http_500_error_handling(self, mock_get, mock_librenms_config):
"""Verify handling of HTTP 500 errors."""
mock_get.return_value.status_code = 500
mock_get.return_value.json.return_value = {
"status": "error",
"message": "Internal server error",
}
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
success, result = api.get_device_info(device_id=123)
assert success is False
assert result is None
@patch("netbox_librenms_plugin.librenms_api.requests.post")
def test_malformed_api_response(self, mock_post, mock_librenms_config):
"""Verify handling of malformed API responses."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {} # Missing expected fields
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.add_device(
data={
"hostname": "test.example.com",
"snmp_version": "v2c",
"community": "public",
}
)
# Should handle missing fields gracefully
assert result[0] is False
@patch("netbox_librenms_plugin.librenms_api.requests.get")
def test_ssl_verification_error(self, mock_get, mock_librenms_config):
"""Verify handling of SSL verification errors."""
mock_get.side_effect = requests.exceptions.SSLError("SSL certificate verification failed")
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
result = api.test_connection()
assert result.get("error") is True
# ====================================================================================
# Test Class 5-9 continuing in next part due to length...
# Run `make unittest` to execute all tests
# ====================================================================================
# ====================================================================================
# Guard tests: int conversion guard and VLAN dict guard
# ====================================================================================
class TestGetLibreNMSIdIntGuard:
"""Tests for the int conversion guard in get_librenms_id."""
def test_non_integer_string_from_ip_returns_none(self, mock_librenms_config):
"""When get_device_id_by_ip returns a non-int string, _store_librenms_id must not be called."""
from unittest.mock import MagicMock, patch
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
obj = MagicMock()
obj.primary_ip.address.ip = "192.168.1.1"
obj.primary_ip.dns_name = ""
obj.name = "test-device"
obj._meta.model_name = "device"
obj.pk = 1
with (
patch.object(api, "_get_cache_key", return_value="test_cache_key"),
patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache,
patch.object(api, "get_device_id_by_ip", return_value="not-an-int"),
patch.object(api, "get_device_id_by_hostname", return_value=None),
patch.object(api, "_store_librenms_id") as mock_store,
):
mock_cache.get.return_value = None
result = api.get_librenms_id(obj)
assert result is None
mock_store.assert_not_called()
def test_valid_integer_string_stores_and_returns(self, mock_librenms_config):
"""When get_device_id_by_ip returns a valid int string, it should be stored and returned."""
from unittest.mock import MagicMock, patch
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
obj = MagicMock()
obj.primary_ip.address.ip = "192.168.1.1"
obj.primary_ip.dns_name = ""
obj.name = "test-device"
obj._meta.model_name = "device"
obj.pk = 1
with (
patch.object(api, "_get_cache_key", return_value="test_cache_key"),
patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache,
patch.object(api, "get_device_id_by_ip", return_value="42"),
patch.object(api, "_store_librenms_id") as mock_store,
):
mock_cache.get.return_value = None
result = api.get_librenms_id(obj)
assert result == 42
mock_store.assert_called_once_with(obj, 42)
class TestVlanEntryDictGuard:
"""Tests for the isinstance(vlan_entry, dict) guard in _parse_port_vlan_info."""
def test_non_dict_entry_is_skipped(self, mock_librenms_config):
"""Non-dict entries in vlans_data should be skipped without error."""
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
port_data = {
"ifName": "eth0",
"ifDescr": "eth0",
"port_id": 1,
"ifTrunk": "dot1Q",
"ifVlan": None,
"vlans": [{"vlan": 10, "untagged": 1}, "bad_entry", {"vlan": 20}],
}
result = api.parse_port_vlan_data(port_data)
# Only VIDs 10 and 20 should be parsed; string entry is skipped
assert result["untagged_vlan"] == 10
assert 20 in result["tagged_vlans"]
assert len(result["tagged_vlans"]) == 1
def test_mixed_bad_entries_no_exception(self, mock_librenms_config):
"""Multiple non-dict entries mixed with valid dicts should not raise."""
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI(server_key="default")
port_data = {
"ifName": "eth0",
"ifDescr": "eth0",
"port_id": 2,
"ifTrunk": "dot1Q",
"ifVlan": None,
"vlans": [None, 123, "unexpected", {"vlan": 30}],
}
result = api.parse_port_vlan_data(port_data)
assert result["tagged_vlans"] == [30]
assert result["untagged_vlan"] is None