Files
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

1218 lines
48 KiB
Python

"""Coverage tests for librenms_api.py missing lines."""
from unittest.mock import MagicMock, patch
import requests
def _make_api(url="https://librenms.example.com", token="test-token"):
"""Create a LibreNMSAPI instance without database calls."""
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
mock_cfg.side_effect = lambda plugin, key, default=None: {
"servers": None,
"librenms_url": url,
"api_token": token,
"cache_timeout": 300,
"verify_ssl": True,
}.get(key, default)
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
return LibreNMSAPI(server_key="default")
class TestLibreNMSAPIInitFallback:
"""Tests for __init__ fallback when no server_key (lines 35-36)."""
def test_init_reads_selected_server_from_settings(self):
"""When no server_key, tries to get selected_server from LibreNMSSettings."""
servers_config = {
"primary": {
"librenms_url": "https://primary.example.com",
"api_token": "tok",
"cache_timeout": 300,
"verify_ssl": True,
}
}
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
mock_cfg.side_effect = lambda plugin, key, default=None: servers_config if key == "servers" else default
mock_settings_obj = MagicMock()
mock_settings_obj.selected_server = "primary"
mock_settings_class = MagicMock()
mock_settings_class.objects.first.return_value = mock_settings_obj
# LibreNMSSettings is imported inline in __init__, patch via models
with patch.dict(
"sys.modules", {"netbox_librenms_plugin.models": MagicMock(LibreNMSSettings=mock_settings_class)}
):
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI()
assert api.server_key == "primary"
assert api.librenms_url == "https://primary.example.com"
assert api.api_token == "tok"
def test_init_settings_import_error_defaults_to_default(self):
"""When LibreNMSSettings can't be imported, defaults to 'default'."""
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
mock_cfg.side_effect = lambda plugin, key, default=None: {
"servers": None,
"librenms_url": "https://x.example.com",
"api_token": "tok",
"cache_timeout": 300,
"verify_ssl": True,
}.get(key, default)
# Simulate AttributeError when accessing LibreNMSSettings (covers except branch)
mock_models = MagicMock()
mock_models.LibreNMSSettings.objects.first.side_effect = AttributeError("no attr")
with patch.dict("sys.modules", {"netbox_librenms_plugin.models": mock_models}):
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
api = LibreNMSAPI()
assert api.server_key == "default"
assert api.librenms_url == "https://x.example.com"
assert api.api_token == "tok"
class TestTestConnectionErrors:
"""Tests for test_connection error paths (lines 116, 121, 137, 146-147, 157-171)."""
def test_http_403_returns_error(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 403
with patch("requests.get", return_value=mock_resp):
result = api.test_connection()
assert result["error"] is True
assert "forbidden" in result["message"].lower()
def test_http_404_returns_error(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch("requests.get", return_value=mock_resp):
result = api.test_connection()
assert result["error"] is True
assert "not found" in result["message"].lower()
def test_http_500_returns_error(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
with patch("requests.get", return_value=mock_resp):
result = api.test_connection()
assert result["error"] is True
assert "server error" in result["message"].lower()
def test_http_unexpected_code_returns_error(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 302
with patch("requests.get", return_value=mock_resp):
result = api.test_connection()
assert result["error"] is True
assert "302" in result["message"]
def test_ssl_error_returns_error(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.SSLError("cert failed")):
result = api.test_connection()
assert result["error"] is True
assert "SSL" in result["message"] or "ssl" in result["message"].lower()
def test_connection_error_returns_error(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("unreachable")):
result = api.test_connection()
assert result["error"] is True
assert "Connection failed" in result["message"]
def test_timeout_returns_error(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.Timeout("timed out")):
result = api.test_connection()
assert result["error"] is True
assert "timeout" in result["message"].lower()
def test_generic_exception_returns_error(self):
api = _make_api()
with patch("requests.get", side_effect=ValueError("something weird")):
result = api.test_connection()
assert result["error"] is True
assert "Unexpected error" in result["message"]
class TestGetAvailableServersLegacy:
"""Tests for get_available_servers legacy path (line 231)."""
def test_legacy_config_no_servers(self):
"""When no servers_config, returns default server with legacy URL."""
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
def side_effect(plugin, key, default=None):
if key == "servers":
return None
if key == "librenms_url":
return "https://legacy.example.com"
return default
mock_cfg.side_effect = side_effect
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
result = LibreNMSAPI.get_available_servers()
assert "default" in result
assert "legacy.example.com" in result["default"]
def test_no_legacy_url_returns_default_label(self):
"""When no servers_config and no legacy URL, returns default label."""
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_cfg:
def side_effect(plugin, key, default=None):
return None
mock_cfg.side_effect = side_effect
from netbox_librenms_plugin.librenms_api import LibreNMSAPI
result = LibreNMSAPI.get_available_servers()
assert "default" in result
assert result["default"] == "Default Server"
class TestGetLibreNMSIdDictServerKey:
"""Tests for get_librenms_id → _store_librenms_id with dict CF (lines 259-262)."""
def test_dict_cf_routes_to_get_librenms_device_id(self):
"""When CF has a dict 'librenms_id', get_librenms_id uses get_librenms_device_id(obj, server_key, auto_save=False)."""
api = _make_api()
obj = MagicMock()
obj.cf = {"librenms_id": {"default": None}}
obj.custom_field_data = {"librenms_id": {"default": None}}
obj._meta.model_name = "device"
obj.pk = 42
obj.primary_ip = None
obj.name = None
with patch("netbox_librenms_plugin.utils.get_librenms_device_id", return_value=None) as mock_get_id:
with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache:
mock_cache.get.return_value = None
result = api.get_librenms_id(obj)
assert result is None
mock_get_id.assert_called_once_with(obj, "default", auto_save=False)
def test_store_librenms_id_via_hostname_lookup(self):
"""get_librenms_id reaches _store_librenms_id when CF/cache miss but hostname API hit."""
api = _make_api()
obj = MagicMock()
obj.cf = {"librenms_id": None} # CF key present so _store_librenms_id uses CF path
obj.custom_field_data = {}
obj._meta.model_name = "device"
obj.pk = 99
obj.primary_ip = None
obj.name = "hostname"
with patch("netbox_librenms_plugin.utils.get_librenms_device_id", return_value=None):
with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache:
mock_cache.get.return_value = None
with patch.object(api, "get_device_id_by_hostname", return_value=42) as mock_by_hostname:
with patch("netbox_librenms_plugin.utils.set_librenms_device_id") as mock_set_id:
result = api.get_librenms_id(obj)
assert result == 42
mock_by_hostname.assert_called_once_with("hostname")
mock_set_id.assert_called_once_with(obj, 42, "default")
class TestGetPortsErrors:
"""Tests for get_ports error paths (lines 373, 375-376)."""
def test_http_error_404_returns_false(self):
api = _make_api()
http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=404))
with patch("requests.get", side_effect=http_err):
ok, msg = api.get_ports(1)
assert ok is False
assert "not found" in msg.lower()
def test_http_error_other_returns_false(self):
api = _make_api()
http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=500))
http_err.response = MagicMock(status_code=500)
with patch("requests.get", side_effect=http_err):
ok, msg = api.get_ports(1)
assert ok is False
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("conn error")):
ok, msg = api.get_ports(1)
assert ok is False
class TestGetInventoryFilteredErrors:
"""Tests for get_inventory_filtered error paths (lines 405, 407, 409, 411)."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, result = api.get_inventory_filtered(1)
assert ok is False
assert isinstance(result, str) # error message string
def test_empty_results_with_no_filters_returns_true_empty_list(self):
"""Empty inventory with status:ok is a valid successful empty response."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "inventory": []}
with patch("requests.get", return_value=mock_resp):
ok, result = api.get_inventory_filtered(1)
assert ok is True
assert result == []
def test_fallback_to_all_endpoint_when_filtered_empty(self):
"""If filtered endpoint returns empty and params present, falls back to /all."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "inventory": []}
all_inventory = [{"entPhysicalClass": "chassis", "entPhysicalContainedIn": 0}]
with patch("requests.get", return_value=mock_resp):
with patch.object(api, "get_device_inventory", return_value=(True, all_inventory)):
ok, result = api.get_inventory_filtered(1, ent_physical_class="chassis")
assert ok is True
assert len(result) == 1
def test_fallback_fails_when_all_endpoint_fails(self):
"""If /all fallback also fails, returns False, []."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "inventory": []}
with patch("requests.get", return_value=mock_resp):
with patch.object(api, "get_device_inventory", return_value=(False, [])):
ok, result = api.get_inventory_filtered(1, ent_physical_class="chassis")
assert ok is False
assert result == []
class TestGetDeviceVlansErrors:
"""Tests for get_device_vlans error paths (lines 474-480)."""
def test_http_error_404_returns_false(self):
api = _make_api()
http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=404))
http_err.response = MagicMock(status_code=404)
with patch("requests.get", side_effect=http_err):
ok, msg = api.get_device_vlans(1)
assert ok is False
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
ok, msg = api.get_device_vlans(1)
assert ok is False
def test_non_200_returns_http_status(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 503
http_err = requests.exceptions.HTTPError("503 Service Unavailable")
http_err.response = mock_resp
mock_resp.raise_for_status.side_effect = http_err
with patch("requests.get", return_value=mock_resp):
ok, msg = api.get_device_vlans(1)
assert ok is False
assert "503" in msg
class TestGetDeviceLinksErrors:
"""Tests for get_device_links error paths (lines 505-508)."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
ok, msg = api.get_device_links(1)
assert ok is False
def test_request_exception_base_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.get_device_links(1)
assert ok is False
class TestListDevicesErrors:
"""Tests for list_devices error paths (lines 542-547)."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
ok, msg = api.list_devices()
assert ok is False
def test_non_200_returns_empty(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "error"}
with patch("requests.get", return_value=mock_resp):
ok, result = api.list_devices()
assert ok is False
class TestGetDeviceIpsErrors:
"""Tests for get_device_ips error paths (lines 580-585)."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
ok, msg = api.get_device_ips(1)
assert ok is False
class TestGetDeviceInfoErrors:
"""Tests for get_device_info error paths (lines 606-607)."""
def test_non_200_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
mock_resp.raise_for_status.return_value = None
with patch("requests.get", return_value=mock_resp):
ok, data = api.get_device_info(1)
assert ok is False
assert data is None
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, data = api.get_device_info(1)
assert ok is False
assert data is None
class TestGetPortVlanDetailsErrors:
"""Tests for get_port_vlan_details error paths."""
def test_http_error_404_returns_false(self):
api = _make_api()
http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=404))
http_err.response = MagicMock(status_code=404)
with patch("requests.get", side_effect=http_err):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
assert "not found" in msg.lower()
def test_non_404_http_error_returns_false(self):
api = _make_api()
http_err = requests.exceptions.HTTPError(response=MagicMock(status_code=500))
http_err.response = MagicMock(status_code=500)
with patch("requests.get", side_effect=http_err):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.ConnectionError("error")):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
def test_non_200_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 503
mock_resp.raise_for_status.return_value = None
with patch("requests.get", return_value=mock_resp):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
def test_request_exception_base_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
def test_http_404_via_raise_for_status_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
http_error = requests.exceptions.HTTPError(response=mock_resp)
mock_resp.raise_for_status.side_effect = http_error
with patch("requests.get", return_value=mock_resp):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
class TestListDevicesSuccess:
"""Tests for list_devices success (lines 805+)."""
def test_list_devices_with_filters(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "devices": [{"device_id": 1}]}
with patch("requests.get", return_value=mock_resp) as mock_get:
ok, result = api.list_devices({"type": "network"})
assert ok is True
assert len(result) == 1
# Verify the filter was forwarded to the outgoing request
assert mock_get.call_args[1]["params"] == {"type": "network"}
def test_list_devices_no_filters(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "devices": []}
with patch("requests.get", return_value=mock_resp):
ok, result = api.list_devices()
assert ok is True
assert result == []
class TestGetPoller:
"""Tests for get_poller_groups error path."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, result = api.get_poller_groups()
assert ok is False
def test_non_ok_status_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "error"}
with patch("requests.get", return_value=mock_resp):
ok, result = api.get_poller_groups()
assert ok is False
class TestAddDeviceErrors:
"""Tests for add_device errors."""
def _make_device_data(self):
return {"hostname": "router01", "snmp_version": "v2c", "community": "public", "force_add": False}
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.post", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.add_device(self._make_device_data())
assert ok is False
def test_non_ok_response_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "error", "message": "Already exists"}
with patch("requests.post", return_value=mock_resp):
ok, msg = api.add_device(self._make_device_data())
assert ok is False
class TestGetLocationsErrors:
"""Tests for get_locations errors."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.get_locations()
assert ok is False
class TestUpdateDeviceFieldErrors:
"""Tests for update_device_field errors."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.patch", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.update_device_field(1, {"field": "value"})
assert ok is False
class TestGetDeviceIdByIPErrors:
"""Tests for get_device_id_by_ip errors."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
result = api.get_device_id_by_ip("192.168.1.1")
assert result is None
def test_non_200_returns_none(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
http_error = requests.exceptions.HTTPError(response=mock_resp)
mock_resp.raise_for_status.side_effect = http_error
with patch("requests.get", return_value=mock_resp):
result = api.get_device_id_by_ip("192.168.1.1")
assert result is None
def test_null_devices_field_returns_none(self):
"""API returns {"devices": null} — TypeError must be caught, not propagate."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"devices": None}
with patch("requests.get", return_value=mock_resp):
result = api.get_device_id_by_ip("192.168.1.1")
assert result is None
def test_empty_devices_list_returns_none(self):
"""API returns {"devices": []} — no match, returns None."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"devices": []}
with patch("requests.get", return_value=mock_resp):
result = api.get_device_id_by_ip("192.168.1.1")
assert result is None
class TestGetDeviceIdByHostnameErrors:
"""Tests for get_device_id_by_hostname errors."""
def test_request_exception_returns_none(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
result = api.get_device_id_by_hostname("router01")
assert result is None
def test_null_devices_field_returns_none(self):
"""API returns {"devices": null} — TypeError must be caught, not propagate."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"devices": None}
with patch("requests.get", return_value=mock_resp):
result = api.get_device_id_by_hostname("router01")
assert result is None
def test_empty_devices_list_returns_none(self):
"""API returns {"devices": []} — no match, returns None."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"devices": []}
with patch("requests.get", return_value=mock_resp):
result = api.get_device_id_by_hostname("router01")
assert result is None
class TestStorelibrenmsId:
"""Tests for _store_librenms_id (lines 259-262)."""
def test_stores_via_set_librenms_device_id_when_cf_has_key(self):
api = _make_api()
obj = MagicMock()
obj.cf = {"librenms_id": {"default": None}}
obj.custom_field_data = {"librenms_id": {"default": None}}
with patch("netbox_librenms_plugin.utils.set_librenms_device_id") as mock_set:
api._store_librenms_id(obj, 42)
mock_set.assert_called_once_with(obj, 42, "default")
obj.save.assert_called_once()
def test_stores_in_cache_when_no_cf_key(self):
api = _make_api()
obj = MagicMock()
obj.cf = {} # No 'librenms_id' key
with patch("netbox_librenms_plugin.librenms_api.cache") as mock_cache:
api._store_librenms_id(obj, 42)
mock_cache.set.assert_called_once()
cache_key_used = mock_cache.set.call_args[0][0]
assert api.server_key in cache_key_used
class TestParsePortVlanData:
"""Tests for parse_port_vlan_data (lines 978+)."""
def test_no_if_vlan_returns_mode_none(self):
api = _make_api()
port_data = {"port_id": 1, "ifName": "Gi0/1", "ifDescr": "GigabitEthernet", "ifVlan": ""}
result = api.parse_port_vlan_data(port_data)
assert result["mode"] is None
def test_trunk_mode_set_correctly(self):
api = _make_api()
port_data = {
"port_id": 1,
"ifName": "Gi0/1",
"ifDescr": "GE",
"ifVlan": "100",
"ifTrunk": "dot1Q",
"vlans": [{"vlan": 100, "untagged": 0}, {"vlan": 200, "untagged": 0}],
}
result = api.parse_port_vlan_data(port_data)
assert result["mode"] == "tagged"
assert 100 in result["tagged_vlans"]
assert 200 in result["tagged_vlans"]
def test_access_mode_from_vlan_array(self):
api = _make_api()
port_data = {
"port_id": 1,
"ifName": "Gi0/2",
"ifDescr": "GE",
"ifVlan": "100",
"vlans": [{"vlan": 100, "untagged": 1}],
}
result = api.parse_port_vlan_data(port_data)
assert result["mode"] == "access"
assert result["untagged_vlan"] == 100
def test_fallback_to_if_vlan_when_no_vlans_array(self):
api = _make_api()
port_data = {"port_id": 1, "ifName": "Gi0/3", "ifDescr": "GE", "ifVlan": "50", "ifTrunk": None}
result = api.parse_port_vlan_data(port_data)
assert result["mode"] == "access"
assert result["untagged_vlan"] == 50
def test_invalid_if_vlan_fallback_returns_none(self):
"""Lines 1028-1029: ValueError when ifVlan is not an integer."""
api = _make_api()
port_data = {"port_id": 1, "ifName": "Gi0/4", "ifDescr": "GE", "ifVlan": "not-a-number"}
result = api.parse_port_vlan_data(port_data)
assert result["untagged_vlan"] is None
def test_if_descr_used_as_interface_name(self):
api = _make_api()
port_data = {"port_id": 1, "ifName": "Gi0/5", "ifDescr": "GigabitEthernet0/5", "ifVlan": ""}
result = api.parse_port_vlan_data(port_data, interface_name_field="ifDescr")
assert result["interface_name"] == "GigabitEthernet0/5"
def test_string_vlan_id_normalized_to_int(self):
"""VLAN ID as string '50' must be coerced to int 50 to avoid string sort."""
api = _make_api()
port_data = {
"port_id": 1,
"ifName": "Gi0/6",
"ifDescr": "GE",
"ifVlan": "50",
"vlans": [{"vlan": "50", "untagged": 1}, {"vlan": "100", "untagged": 0}],
}
result = api.parse_port_vlan_data(port_data)
assert result["untagged_vlan"] == 50
assert result["tagged_vlans"] == [100]
def test_none_vlan_id_skipped(self):
"""Entry with missing 'vlan' key (None) must be skipped."""
api = _make_api()
port_data = {
"port_id": 1,
"ifName": "Gi0/7",
"ifDescr": "GE",
"ifVlan": "200",
"vlans": [{"untagged": 0}, {"vlan": 200, "untagged": 1}],
}
result = api.parse_port_vlan_data(port_data)
assert result["untagged_vlan"] == 200
assert result["tagged_vlans"] == []
def test_malformed_vlan_id_skipped(self):
"""Non-numeric string vlan ID is skipped gracefully."""
api = _make_api()
port_data = {
"port_id": 1,
"ifName": "Gi0/8",
"ifDescr": "GE",
"ifVlan": "300",
"vlans": [{"vlan": "N/A", "untagged": 0}, {"vlan": 300, "untagged": 1}],
}
result = api.parse_port_vlan_data(port_data)
assert result["untagged_vlan"] == 300
assert result["tagged_vlans"] == []
def test_empty_vlans_array_falls_back_to_if_vlan(self):
"""Empty vlans array should use ifVlan fallback."""
api = _make_api()
port_data = {"port_id": 1, "ifName": "Gi0/9", "ifDescr": "GE", "ifVlan": "10", "vlans": []}
result = api.parse_port_vlan_data(port_data)
assert result["untagged_vlan"] == 10
class TestGetDeviceInfoResponseFormats:
"""Tests for get_device_info with unusual API response shapes."""
def test_null_devices_returns_false(self):
"""{"devices": null} must return (False, None), not raise TypeError."""
from unittest.mock import MagicMock, patch
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"devices": None}
with patch("requests.get", return_value=mock_resp):
success, result = api.get_device_info(1)
assert success is False
assert result is None
def test_empty_devices_list_returns_false(self):
"""{"devices": []} must return (False, None)."""
from unittest.mock import MagicMock, patch
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"devices": []}
with patch("requests.get", return_value=mock_resp):
success, result = api.get_device_info(1)
assert success is False
assert result is None
def test_non_200_returns_false(self):
"""Non-200 status code returns (False, None)."""
from unittest.mock import MagicMock, patch
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch("requests.get", return_value=mock_resp):
success, result = api.get_device_info(1)
assert success is False
assert result is None
def test_valid_device_returns_device_dict(self):
"""Normal response returns (True, device_dict)."""
from unittest.mock import MagicMock, patch
api = _make_api()
device = {"device_id": 42, "hostname": "router01"}
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"devices": [device]}
with patch("requests.get", return_value=mock_resp):
success, result = api.get_device_info(42)
assert success is True
assert result["device_id"] == 42
class TestGetPortByIdErrors:
"""Tests for get_port_by_id errors."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.get_port_by_id(1)
assert ok is False
class TestGetDeviceInventoryErrors:
"""Tests for get_device_inventory errors."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.get", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.get_device_inventory(1)
assert ok is False
class TestGetAvailableServersMultiConfig:
"""Tests for get_available_servers with multi-server config (lines 161-165)."""
def test_multi_server_config_returns_dict(self):
api = _make_api()
servers_config = {
"primary": {"display_name": "Primary Server"},
"secondary": {"display_name": "Secondary Server"},
}
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_config:
mock_config.side_effect = lambda plugin, key, default=None: servers_config if key == "servers" else None
result = api.get_available_servers()
assert result == {"primary": "Primary Server", "secondary": "Secondary Server"}
def test_multi_server_config_uses_key_when_no_display_name(self):
api = _make_api()
servers_config = {
"main": {}, # No display_name key
}
with patch("netbox_librenms_plugin.librenms_api.get_plugin_config") as mock_config:
mock_config.side_effect = lambda plugin, key, default=None: servers_config if key == "servers" else None
result = api.get_available_servers()
assert result == {"main": "main"}
class TestAddDeviceWithOptionalFields:
"""Tests for add_device with optional fields (lines 405, 407, 409, 411)."""
def _make_base_data(self):
return {"hostname": "router01", "snmp_version": "v2c", "community": "public", "force_add": False}
def test_add_device_with_port(self):
api = _make_api()
data = {**self._make_base_data(), "port": 161}
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "message": "Device added"}
with patch("requests.post", return_value=mock_resp) as mock_post:
ok, msg = api.add_device(data)
assert ok is True
assert "port" in mock_post.call_args[1]["json"]
def test_add_device_with_transport(self):
api = _make_api()
data = {**self._make_base_data(), "transport": "udp6"}
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "message": "ok"}
with patch("requests.post", return_value=mock_resp) as mock_post:
ok, msg = api.add_device(data)
assert ok is True
assert "transport" in mock_post.call_args[1]["json"]
def test_add_device_with_port_association_mode(self):
api = _make_api()
data = {**self._make_base_data(), "port_association_mode": "ifName"}
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "message": "ok"}
with patch("requests.post", return_value=mock_resp) as mock_post:
ok, msg = api.add_device(data)
assert ok is True
assert "port_association_mode" in mock_post.call_args[1]["json"]
def test_add_device_with_poller_group(self):
api = _make_api()
data = {**self._make_base_data(), "poller_group": 2}
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok", "message": "ok"}
with patch("requests.post", return_value=mock_resp) as mock_post:
ok, msg = api.add_device(data)
assert ok is True
assert "poller_group" in mock_post.call_args[1]["json"]
class TestUpdateDeviceFieldUnexpected:
"""Tests for update_device_field non-ok status (line 474)."""
def test_non_ok_status_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "error", "message": "Failed"}
with patch("requests.patch", return_value=mock_resp):
ok, msg = api.update_device_field(1, {"field": "value"})
assert ok is False
def test_request_exception_with_json_response(self):
"""Lines 477-479: extract message from JSON error response."""
api = _make_api()
mock_response = MagicMock()
mock_response.json.return_value = {"message": "Detailed error"}
exc = requests.exceptions.RequestException("error")
exc.response = mock_response
with patch("requests.patch", side_effect=exc):
ok, msg = api.update_device_field(1, {"field": "value"})
assert ok is False
assert "Detailed error" in msg
class TestGetLocationsNoLocations:
"""Tests for get_locations when no locations found (line 505)."""
def test_no_locations_in_response(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "ok"} # No 'locations' key
with patch("requests.get", return_value=mock_resp):
ok, msg = api.get_locations()
assert ok is False
class TestAddLocationErrors:
"""Tests for add_location error paths (lines 542-547)."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.post", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.add_location({"location": "TestSite", "lat": 0, "lng": 0})
assert ok is False
def test_request_exception_with_json_response(self):
api = _make_api()
mock_response = MagicMock()
mock_response.json.return_value = {"message": "Detailed error"}
exc = requests.exceptions.RequestException("error")
exc.response = mock_response
with patch("requests.post", side_effect=exc):
ok, msg = api.add_location({"location": "TestSite", "lat": 0, "lng": 0})
assert ok is False
assert "Detailed error" in msg
class TestUpdateLocationErrors:
"""Tests for update_location error paths (lines 580-585)."""
def test_request_exception_returns_false(self):
api = _make_api()
with patch("requests.patch", side_effect=requests.exceptions.RequestException("error")):
ok, msg = api.update_location("TestSite", {"lat": 0, "lng": 0})
assert ok is False
def test_request_exception_with_json_response(self):
api = _make_api()
mock_response = MagicMock()
mock_response.json.return_value = {"message": "Update failed"}
exc = requests.exceptions.RequestException("error")
exc.response = mock_response
with patch("requests.patch", side_effect=exc):
ok, msg = api.update_location("TestSite", {"lat": 0, "lng": 0})
assert ok is False
assert "Update failed" in msg
class TestGetInventoryFilteredNonOk:
"""Tests for get_inventory_filtered non-200 response (line 689 + 791, 799)."""
def test_non_200_response_returns_false(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
mock_resp.raise_for_status.return_value = None
with patch("requests.get", return_value=mock_resp):
ok, data = api.get_inventory_filtered(1)
assert ok is False
assert isinstance(data, str) # error message, not empty list
def test_ent_physical_contained_in_filter(self):
"""Line 791: ent_physical_contained_in filter exercised — API returns already-filtered list."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
# The real LibreNMS API filters server-side; mock returns only the matching item.
inventory = [
{"entPhysicalContainedIn": "1", "entPhysicalName": "slot1"},
]
mock_resp.json.return_value = {"status": "ok", "inventory": inventory}
with patch("requests.get", return_value=mock_resp) as mock_get:
ok, data = api.get_inventory_filtered(1, ent_physical_contained_in="1")
assert ok is True
assert len(data) == 1
mock_get.assert_called_once()
_, call_kwargs = mock_get.call_args
assert call_kwargs.get("params", {}).get("entPhysicalContainedIn") == "1"
def test_empty_inventory_returns_empty(self):
"""Line 799: when response lacks status:ok (even with an empty inventory list), returns False."""
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"inventory": []} # Empty inventory
with patch("requests.get", return_value=mock_resp):
ok, data = api.get_inventory_filtered(1)
# No "status":"ok" in response → falls through to return False, message
assert ok is False
assert isinstance(data, str) # error message, not empty list
class TestGetDeviceVlansHttpError:
"""Tests for get_device_vlans HTTP error paths (lines 918, 924)."""
def test_http_404_returns_not_found(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 404
exc = requests.exceptions.HTTPError(response=mock_resp)
mock_resp.raise_for_status.side_effect = exc
exc.response = mock_resp
with patch("requests.get", side_effect=exc):
ok, msg = api.get_device_vlans(1)
assert ok is False
def test_http_5xx_returns_error(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
exc = requests.exceptions.HTTPError(response=mock_resp)
exc.response = mock_resp
with patch("requests.get", side_effect=exc):
ok, msg = api.get_device_vlans(1)
assert ok is False
class TestGetPortVlanDetailsHttpError:
"""Tests for get_port_vlan_details HTTP error paths (line 974)."""
def test_http_non_404_returns_http_error(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
exc = requests.exceptions.HTTPError(response=mock_resp)
exc.response = mock_resp
with patch("requests.get", side_effect=exc):
ok, msg = api.get_port_vlan_details(1)
assert ok is False
assert "HTTP error" in msg
class TestGetInventoryFilteredNonOkStatus:
"""Line 689: non-200 returns False with error message."""
def test_non_200_status(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.raise_for_status.return_value = None
with patch("requests.get", return_value=mock_resp):
ok, data = api.get_inventory_filtered(1)
assert ok is False
assert isinstance(data, str) # error message, not empty list
class TestGetDeviceVlansNonOkResponse:
"""Line 918: get_device_vlans when status != ok."""
def test_vlans_response_status_not_ok(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"status": "error", "message": "Failed to retrieve VLANs"}
with patch("requests.get", return_value=mock_resp):
ok, msg = api.get_device_vlans(1)
assert ok is False
assert "Failed" in msg
class TestGetDeviceInventoryNonOkStatus:
"""Line 689: get_device_inventory non-200 returns False, []."""
def test_non_200_status(self):
api = _make_api()
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.raise_for_status.side_effect = requests.exceptions.HTTPError("500 Server Error")
with patch("requests.get", return_value=mock_resp):
ok, data = api.get_device_inventory(1)
assert ok is False
class TestMalformedPayloads:
"""Tests for malformed-payload guards in API methods (inventory, devices, vlans)."""
def _ok_resp(self, body: dict):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = body
return mock_resp
def test_get_device_inventory_none_inventory(self):
"""get_device_inventory: inventory=None returns (False, ...)."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "inventory": None})):
ok, msg = api.get_device_inventory(1)
assert ok is False
assert msg is not None
def test_get_device_inventory_non_list_inventory(self):
"""get_device_inventory: inventory as dict returns (False, ...)."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "inventory": {}})):
ok, msg = api.get_device_inventory(1)
assert ok is False
def test_get_device_inventory_non_dict_inventory_item(self):
"""get_device_inventory: list containing non-dict items returns (False, ...)."""
api = _make_api()
body = {"status": "ok", "inventory": [None, {"entPhysicalName": "slot1"}]}
with patch("requests.get", return_value=self._ok_resp(body)):
ok, msg = api.get_device_inventory(1)
assert ok is False
assert msg is not None
def test_get_inventory_filtered_none_inventory(self):
"""get_inventory_filtered: inventory=None in filtered path returns (False, ...) without calling get_device_inventory."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "inventory": None})):
with patch.object(api, "get_device_inventory") as mock_get_inv:
ok, msg = api.get_inventory_filtered(1, ent_physical_class="chassis")
assert ok is False
assert msg is not None
mock_get_inv.assert_not_called()
def test_get_inventory_filtered_non_dict_inventory_item(self):
"""get_inventory_filtered: list containing non-dict items returns (False, ...) without fallback."""
api = _make_api()
body = {"status": "ok", "inventory": ["bad"]}
with patch("requests.get", return_value=self._ok_resp(body)):
with patch.object(api, "get_device_inventory") as mock_get_inv:
ok, msg = api.get_inventory_filtered(1, ent_physical_class="chassis")
assert ok is False
assert msg is not None
mock_get_inv.assert_not_called()
def test_list_devices_none_devices(self):
"""list_devices: devices=None returns (False, ...)."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "devices": None})):
ok, msg = api.list_devices()
assert ok is False
assert msg is not None
def test_list_devices_non_list_devices(self):
"""list_devices: devices as string returns (False, ...)."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "devices": "bad"})):
ok, msg = api.list_devices()
assert ok is False
def test_get_device_vlans_none_vlans(self):
"""get_device_vlans: vlans=None returns (False, ...)."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "vlans": None})):
ok, msg = api.get_device_vlans(1)
assert ok is False
assert msg is not None
def test_get_device_vlans_skips_non_dict_items(self):
"""get_device_vlans: non-dict items in vlans list are skipped safely."""
api = _make_api()
vlans = [None, "bad", {"device_id": 1, "vlan_id": 10}]
with patch("requests.get", return_value=self._ok_resp({"status": "ok", "vlans": vlans})):
ok, data = api.get_device_vlans(1)
assert ok is True
assert len(data) == 1
assert data[0]["vlan_id"] == 10
def test_get_device_ips_none_addresses(self):
"""get_device_ips: addresses=None returns (False, ...)."""
api = _make_api()
with patch("requests.get", return_value=self._ok_resp({"addresses": None})):
ok, _ = api.get_device_ips(1)
assert ok is False