Files
netbox-librenms-plugin/netbox_librenms_plugin/tests/test_coverage_base_views.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

2197 lines
84 KiB
Python

"""
Coverage tests for base view classes:
- views/base/cables_view.py (~30% → target 95%+)
- views/base/interfaces_view.py (~14% → target 95%+)
- views/base/ip_addresses_view.py (~34% → target 95%+)
All tests follow the project conventions:
- Plain pytest classes, NO @pytest.mark.django_db
- Mock ALL database interactions with MagicMock
- Inline imports inside test methods
- assert x == y style
- No RequestFactory — mock request objects directly
"""
from unittest.mock import MagicMock, patch
# =============================================================================
# Helpers
# =============================================================================
def _mock_obj(model_name="device", pk=1, name="test-device"):
obj = MagicMock()
obj._meta = MagicMock()
obj._meta.model_name = model_name
obj.pk = pk
obj.id = pk
obj.name = name
return obj
def _mock_request(path="/plugins/librenms/device/1/cables/"):
req = MagicMock()
req.path = path
req.GET = {}
req.POST = {}
req.headers = {}
return req
# =============================================================================
# BaseCableTableView
# =============================================================================
class TestBaseCableTableViewGetLinksData:
"""Tests for BaseCableTableView.get_links_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view.request = _mock_request()
view.librenms_id = 42
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_get_links_data_returns_none_on_api_error(self):
"""When API returns failure, get_links_data returns None."""
view = self._make_view()
view._librenms_api.get_device_links.return_value = (False, {"error": "timeout"})
obj = _mock_obj()
with patch.object(view, "get_ports_data", return_value={"ports": []}):
result = view.get_links_data(obj)
assert result is None
def test_get_links_data_returns_none_when_error_key_present(self):
"""When response has 'error' key even with success=True, returns None."""
view = self._make_view()
view._librenms_api.get_device_links.return_value = (True, {"error": "some error"})
obj = _mock_obj()
with patch.object(view, "get_ports_data", return_value={"ports": []}):
result = view.get_links_data(obj)
assert result is None
def test_get_links_data_success_returns_link_list(self):
"""Successful API call returns formatted link list."""
view = self._make_view()
links_data = {
"links": [
{
"local_port_id": 10,
"remote_port": "Gi0/1",
"remote_hostname": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
view._librenms_api.get_device_links.return_value = (True, links_data)
ports = {
"ports": [
{"port_id": 10, "ifName": "Gi0/0"},
]
}
obj = _mock_obj()
with (
patch.object(view, "get_ports_data", return_value=ports),
patch("netbox_librenms_plugin.views.base.cables_view.get_interface_name_field", return_value="ifName"),
):
result = view.get_links_data(obj)
assert result is not None
assert len(result) == 1
assert result[0]["local_port"] == "Gi0/0"
assert result[0]["remote_device"] == "switch-b"
def test_get_links_data_port_without_id_skipped(self):
"""Ports missing port_id are skipped when building local port map."""
view = self._make_view()
links_data = {"links": [{"local_port_id": 10, "remote_hostname": "sw", "remote_port": "Gi0/1"}]}
view._librenms_api.get_device_links.return_value = (True, links_data)
ports = {
"ports": [
{"port_id": None, "ifName": "Gi0/0"}, # No port_id — skipped
{"port_id": 10, "ifName": "Gi0/0"},
]
}
obj = _mock_obj()
with (
patch.object(view, "get_ports_data", return_value=ports),
patch("netbox_librenms_plugin.views.base.cables_view.get_interface_name_field", return_value="ifName"),
):
result = view.get_links_data(obj)
assert result is not None
class TestBaseCableTableViewGetDeviceByIdOrName:
"""Tests for BaseCableTableView.get_device_by_id_or_name."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_finds_device_by_librenms_id(self):
"""When remote_device_id matches librenms_id, device is returned."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
mock_device = MagicMock()
sentinel_q = object()
with (
patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice,
patch("netbox_librenms_plugin.views.base.cables_view._librenms_id_q", return_value=sentinel_q) as mock_q,
):
MockDevice.objects.get.return_value = mock_device
device, found, error = view.get_device_by_id_or_name(42, "switch.example.com")
assert found is True
assert device is mock_device
assert error is None
mock_q.assert_called_once_with("default", 42)
MockDevice.objects.get.assert_called_once_with(sentinel_q)
def test_falls_back_to_name_when_no_id(self):
"""When remote_device_id is None, falls back to name lookup."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
mock_device = MagicMock()
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
MockDevice.objects.get.return_value = mock_device
device, found, error = view.get_device_by_id_or_name(None, "switch-a")
assert found is True
assert device is mock_device
def test_falls_back_to_simple_hostname_when_fqdn_not_found(self):
"""When FQDN name not found, tries short hostname."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
mock_device = MagicMock()
call_args = []
class _DoesNotExist(Exception):
pass
def get_side_effect(name=None, **kwargs):
call_args.append(name)
if name == "switch.example.com":
raise _DoesNotExist("not found")
if name == "switch":
return mock_device
raise Exception(f"Unexpected: {name}")
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
MockDevice.DoesNotExist = _DoesNotExist
MockDevice.objects.get.side_effect = get_side_effect
device, found, error = view.get_device_by_id_or_name(None, "switch.example.com")
assert found is True
assert device is mock_device
assert "switch.example.com" in call_args
assert "switch" in call_args
def test_multiple_objects_returns_error_message(self):
"""MultipleObjectsReturned for name lookup returns error info."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
from django.core.exceptions import MultipleObjectsReturned
class _DoesNotExist(Exception):
pass
MockDevice.DoesNotExist = _DoesNotExist
MockDevice.objects.get.side_effect = MultipleObjectsReturned("Multiple")
device, found, error = view.get_device_by_id_or_name(None, "duplicate-switch")
assert found is False
assert device is None
assert error is not None
assert "duplicate-switch" in error
def test_device_not_found_returns_none_false_none(self):
"""When device not found by any method, returns (None, False, None)."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
class _DoesNotExist(Exception):
pass
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
MockDevice.DoesNotExist = _DoesNotExist
MockDevice.objects.get.side_effect = _DoesNotExist("not found")
device, found, error = view.get_device_by_id_or_name(None, "nonexistent")
assert found is False
assert device is None
class TestBaseCableTableViewEnrichLocalPort:
"""Tests for BaseCableTableView.enrich_local_port."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_no_local_port_skips_enrichment(self):
"""When local_port is absent, link dict is unchanged."""
view = self._make_view()
link = {"local_port": None}
obj = _mock_obj()
view.enrich_local_port(link, obj)
assert "local_port_url" not in link
def test_interface_found_by_librenms_id_adds_url(self):
"""When interface found by librenms_id, local_port_url and id are set."""
view = self._make_view()
iface = MagicMock()
iface.pk = 5
obj = _mock_obj()
obj.virtual_chassis = None
obj.interfaces = MagicMock()
obj.interfaces.filter.return_value.first.return_value = iface
link = {"local_port": "Gi0/0", "local_port_id": 10}
with patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/interfaces/5/"):
view.enrich_local_port(link, obj)
assert link["local_port_url"] == "/dcim/interfaces/5/"
assert link["netbox_local_interface_id"] == 5
def test_interface_found_by_name_fallback(self):
"""When librenms_id match fails, falls back to name matching."""
view = self._make_view()
iface = MagicMock()
iface.pk = 7
obj = _mock_obj()
obj.virtual_chassis = None
obj.interfaces = MagicMock()
# First filter (librenms_id) returns nothing, second (name) returns iface
obj.interfaces.filter.return_value.first.side_effect = [None, iface]
link = {"local_port": "Gi0/1", "local_port_id": 20}
with patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/interfaces/7/"):
view.enrich_local_port(link, obj)
assert link.get("local_port_url") == "/dcim/interfaces/7/"
def test_no_interface_found_leaves_link_unchanged(self):
"""When no interface found, link dict is not modified."""
view = self._make_view()
obj = _mock_obj()
obj.virtual_chassis = None
obj.interfaces = MagicMock()
obj.interfaces.filter.return_value.first.return_value = None
link = {"local_port": "Gi0/2", "local_port_id": 30}
view.enrich_local_port(link, obj)
assert "local_port_url" not in link
def test_virtual_chassis_delegates_to_chassis_member(self):
"""With VC, chassis member interfaces are queried."""
view = self._make_view()
vc = MagicMock()
member = MagicMock()
member.interfaces = MagicMock()
iface = MagicMock()
iface.pk = 3
member.interfaces.filter.return_value.first.return_value = iface
obj = _mock_obj()
obj.virtual_chassis = vc
# Non-VC fallback returns nothing to confirm member path is used
obj.interfaces = MagicMock()
obj.interfaces.filter.return_value.first.return_value = None
link = {"local_port": "Gi1/0/1", "local_port_id": 100}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=member,
) as mock_get_vc_member,
patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/interfaces/3/"),
):
view.enrich_local_port(link, obj)
mock_get_vc_member.assert_called_once_with(obj, "Gi1/0/1")
assert link["local_port_url"] == "/dcim/interfaces/3/"
class TestBaseCableTableViewCheckCableStatus:
"""Tests for BaseCableTableView.check_cable_status."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
return view
def test_cable_found_sets_cable_url(self):
"""Existing cable: cable_status='Cable Found' and cable_url set."""
view = self._make_view()
cable = MagicMock()
cable.pk = 99
local_iface = MagicMock()
local_iface.cable = cable
remote_iface = MagicMock()
remote_iface.cable = None
link = {"netbox_local_interface_id": 1, "netbox_remote_interface_id": 2}
with (
patch("netbox_librenms_plugin.views.base.cables_view.Interface") as MockIface,
patch("netbox_librenms_plugin.views.base.cables_view.reverse", return_value="/dcim/cables/99/"),
):
MockIface.objects.get.side_effect = [local_iface, remote_iface]
result = view.check_cable_status(link)
assert result["cable_status"] == "Cable Found"
assert result["cable_url"] == "/dcim/cables/99/"
assert result["can_create_cable"] is False
def test_no_cable_sets_can_create_cable(self):
"""No cable: cable_status='No Cable' and can_create_cable=True."""
view = self._make_view()
local_iface = MagicMock()
local_iface.cable = None
remote_iface = MagicMock()
remote_iface.cable = None
link = {"netbox_local_interface_id": 1, "netbox_remote_interface_id": 2}
with patch("netbox_librenms_plugin.views.base.cables_view.Interface") as MockIface:
MockIface.objects.get.side_effect = [local_iface, remote_iface]
result = view.check_cable_status(link)
assert result["cable_status"] == "No Cable"
assert result["can_create_cable"] is True
def test_missing_local_interface_id(self):
"""Only remote interface found: status = 'Local Interface Not Found in Netbox'."""
view = self._make_view()
link = {"netbox_local_interface_id": None, "netbox_remote_interface_id": 2}
result = view.check_cable_status(link)
assert "Local Interface Not Found" in result["cable_status"]
assert result["can_create_cable"] is False
def test_missing_remote_interface_id(self):
"""Only local interface found: status = 'Remote Interface Not Found in Netbox'."""
view = self._make_view()
link = {"netbox_local_interface_id": 1, "netbox_remote_interface_id": None}
result = view.check_cable_status(link)
assert "Remote Interface Not Found" in result["cable_status"]
def test_both_interfaces_missing(self):
"""Both interfaces missing: status = 'Both Interfaces Not Found in Netbox'."""
view = self._make_view()
link = {"netbox_local_interface_id": None, "netbox_remote_interface_id": None}
result = view.check_cable_status(link)
assert "Both Interfaces Not Found" in result["cable_status"]
class TestBaseCableTableViewEnrichLinksData:
"""Tests for BaseCableTableView.enrich_links_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_enrich_links_data_calls_enrich_per_link(self):
"""enrich_links_data calls enrich_local_port and process_remote_device per link."""
view = self._make_view()
link1 = {"local_port": "Gi0/0", "local_port_id": 1, "remote_device": "sw-b"}
link2 = {"local_port": "Gi0/1", "local_port_id": 2, "remote_device": None}
obj = _mock_obj()
with (
patch.object(view, "enrich_local_port") as mock_enrich_local,
patch.object(view, "process_remote_device", return_value=link1) as mock_remote,
):
view.enrich_links_data([link1, link2], obj)
assert mock_enrich_local.call_count == 2
assert mock_remote.call_count == 1 # Only link1 has remote_device
def test_enrich_links_data_sets_device_id(self):
"""Each link gets device_id set to obj.id."""
view = self._make_view()
link = {"local_port": "Gi0/0", "local_port_id": 1, "remote_device": None}
obj = _mock_obj()
obj.id = 55
with patch.object(view, "enrich_local_port"):
view.enrich_links_data([link], obj)
assert link["device_id"] == 55
def test_check_cable_status_called_when_remote_device_resolved(self):
"""check_cable_status is called when remote device resolves successfully."""
view = self._make_view()
link = {
"local_port": "Gi0/0",
"local_port_id": 1,
"remote_device": "sw-b",
"remote_device_id": 5,
}
enriched = dict(link)
enriched["netbox_remote_device_id"] = 10 # resolved
obj = _mock_obj()
with (
patch.object(view, "enrich_local_port"),
patch.object(view, "process_remote_device", return_value=enriched),
patch.object(view, "check_cable_status", return_value=enriched) as mock_cable,
):
view.enrich_links_data([link], obj)
mock_cable.assert_called_once()
class TestBaseCableTableViewPrepareContext:
"""Tests for BaseCableTableView._prepare_context."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view.request = _mock_request()
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view._librenms_api.cache_timeout = 300
return view
def test_fetch_fresh_none_links_returns_none(self):
"""When get_links_data returns None, _prepare_context returns None."""
view = self._make_view()
obj = _mock_obj()
with patch.object(view, "get_links_data", return_value=None):
result = view._prepare_context(view.request, obj, fetch_fresh=True)
assert result is None
def test_cache_miss_returns_none(self):
"""When cache has no data and fetch_fresh=False, returns None."""
view = self._make_view()
obj = _mock_obj()
with (
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="key"),
):
mock_cache.get.return_value = None
result = view._prepare_context(view.request, obj, fetch_fresh=False)
assert result is None
def test_fetch_fresh_caches_and_returns_context(self):
"""Successful fresh fetch caches data and returns context dict."""
view = self._make_view()
obj = _mock_obj()
links = [{"local_port": "Gi0/0"}]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_links_data", return_value=links),
patch.object(view, "enrich_links_data", return_value=links),
patch.object(view, "get_cache_key", return_value="cable-key"),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.cables_view.timezone") as mock_tz,
):
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
result = view._prepare_context(view.request, obj, fetch_fresh=True)
assert result is not None
assert result["object"] is obj
assert result["table"] is mock_table
mock_cache.set.assert_called()
def test_cache_hit_re_enriches_and_returns_context(self):
"""Cached data is re-enriched and returned."""
view = self._make_view()
obj = _mock_obj()
cached_links = [{"local_port": "Gi0/0", "remote_device": "sw-b"}]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="cable-key"),
patch.object(view, "enrich_links_data", return_value=cached_links),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.cables_view.timezone") as mock_tz,
):
mock_cache.get.return_value = {"links": cached_links}
mock_cache.ttl.return_value = 200
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
result = view._prepare_context(view.request, obj, fetch_fresh=False)
assert result is not None
assert result["object"] is obj
def test_non_default_server_key_forwarded_to_enrich(self):
"""When librenms_api.server_key is non-default, _prepare_context passes it to enrich_links_data."""
view = self._make_view()
view._librenms_api.server_key = "non-default"
obj = _mock_obj()
links = [
{
"local_port": "Gi0/0",
"local_port_id": "1",
"remote_device": None,
"remote_port": None,
"remote_port_id": None,
"remote_device_id": None,
}
]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_links_data", return_value=links),
patch.object(view, "enrich_links_data", return_value=links) as mock_enrich,
patch.object(view, "get_cache_key", return_value="cable-key"),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device", return_value=None),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.cables_view.timezone") as mock_tz,
):
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
result = view._prepare_context(view.request, obj, fetch_fresh=True)
assert result is not None
# enrich_links_data must be called with the non-default server_key
mock_enrich.assert_called_once()
_, enrich_kwargs = mock_enrich.call_args
assert enrich_kwargs.get("server_key") == "non-default"
class TestBaseCableTableViewGetContextData:
"""Tests for BaseCableTableView.get_context_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_returns_empty_context_when_no_cache(self):
"""When _prepare_context returns None, get_context_data returns fallback context."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with patch.object(view, "_prepare_context", return_value=None):
ctx = view.get_context_data(request, obj)
assert ctx["table"] is None
assert ctx["object"] is obj
assert ctx["cache_expiry"] is None
def test_returns_context_when_cache_populated(self):
"""When _prepare_context returns data, get_context_data returns it."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
fake_context = {"table": MagicMock(), "object": obj, "cache_expiry": None, "server_key": "default"}
with patch.object(view, "_prepare_context", return_value=fake_context):
ctx = view.get_context_data(request, obj)
assert ctx is fake_context
class TestBaseCableTableViewPost:
"""Tests for BaseCableTableView.post."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.model = MagicMock()
return view
def test_post_no_links_shows_error_and_renders(self):
"""When no links found, renders template with error message."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "_prepare_context", return_value=None),
patch("netbox_librenms_plugin.views.base.cables_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.cables_view.render") as mock_render,
):
mock_render.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.error.assert_called_once()
mock_render.assert_called_once()
def test_post_success_shows_message_and_renders(self):
"""When links found, renders template with success message."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
fake_context = {"table": MagicMock(), "object": obj, "cache_expiry": None}
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "_prepare_context", return_value=fake_context),
patch("netbox_librenms_plugin.views.base.cables_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.cables_view.render") as mock_render,
):
mock_render.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.success.assert_called_once()
mock_render.assert_called_once()
def test_get_ports_data_uses_cache_when_available(self):
"""get_ports_data returns cached data when present."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.librenms_id = 42
cached = {"ports": [{"port_id": 1, "ifName": "Gi0/0"}]}
obj = _mock_obj()
with (
patch.object(view, "get_cache_key", return_value="ports-key"),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
):
mock_cache.get.return_value = cached
result = view.get_ports_data(obj)
assert result is cached
def test_get_ports_data_fetches_from_api_on_cache_miss(self):
"""get_ports_data calls API when cache is empty."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.librenms_id = 42
api_data = {"ports": [{"port_id": 2}]}
view._librenms_api.get_ports.return_value = (True, api_data)
obj = _mock_obj()
with (
patch.object(view, "get_cache_key", return_value="ports-key"),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
):
mock_cache.get.return_value = None
result = view.get_ports_data(obj)
assert result is api_data
# =============================================================================
# BaseInterfaceTableView
# =============================================================================
class TestBaseInterfaceTableViewBasics:
"""Tests for BaseInterfaceTableView utility methods."""
def _make_view(self, model_name="device"):
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view._librenms_api.cache_timeout = 300
view.model = MagicMock()
view.model.__name__ = model_name
view.interface_name_field = None
return view
def test_get_ip_address_with_primary_ip(self):
"""Returns string IP when primary_ip is set."""
view = self._make_view()
obj = MagicMock()
obj.primary_ip.address.ip = "192.168.1.1"
result = view.get_ip_address(obj)
assert result == "192.168.1.1"
def test_get_ip_address_without_primary_ip(self):
"""Returns None when primary_ip is falsy."""
view = self._make_view()
obj = MagicMock()
obj.primary_ip = None
result = view.get_ip_address(obj)
assert result is None
def test_get_select_related_field_for_vm(self):
"""Returns 'virtual_machine' for VirtualMachine model."""
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view.model = MagicMock()
view.model.__name__ = "virtualmachine"
obj = MagicMock()
result = view.get_select_related_field(obj)
assert result == "virtual_machine"
def test_get_select_related_field_for_device(self):
"""Returns 'device' for Device model."""
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view.model = MagicMock()
view.model.__name__ = "device"
obj = MagicMock()
result = view.get_select_related_field(obj)
assert result == "device"
def test_enrich_ports_calls_parse_per_port(self):
"""_enrich_ports_with_vlan_data calls parse_port_vlan_data for each port."""
view = self._make_view()
view._librenms_api.parse_port_vlan_data.side_effect = lambda p, f: {"parsed": True}
ports = [{"port_id": 1, "ifName": "Gi0/0"}, {"port_id": 2, "ifName": "Gi0/1"}]
result = view._enrich_ports_with_vlan_data(ports, "ifName")
assert len(result) == 2
assert view._librenms_api.parse_port_vlan_data.call_count == 2
class TestBaseInterfaceTableViewPost:
"""Tests for BaseInterfaceTableView.post."""
def _make_view(self):
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view._librenms_api.cache_timeout = 300
view.model = MagicMock()
view.partial_template_name = "test_template.html"
return view
def test_post_no_librenms_id_redirects_with_error(self):
"""When librenms_id not found, error message and redirect."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
view._librenms_api.get_librenms_id.return_value = None
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "get_redirect_url", return_value="/device/1/"),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.interfaces_view.redirect") as mock_redirect,
):
mock_redirect.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.error.assert_called_once()
mock_redirect.assert_called_once_with("/device/1/")
def test_post_api_error_redirects_with_error(self):
"""When API returns failure, error message and redirect."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
view._librenms_api.get_librenms_id.return_value = 42
view._librenms_api.get_ports.return_value = (False, "Connection refused")
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "get_redirect_url", return_value="/device/1/"),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.interfaces_view.redirect") as mock_redirect,
):
mock_redirect.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.error.assert_called_once_with(request, "Connection refused")
mock_redirect.assert_called_once_with("/device/1/")
def test_post_success_caches_and_renders(self):
"""Successful fetch caches data and renders template."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
view._librenms_api.get_librenms_id.return_value = 42
view._librenms_api.get_ports.return_value = (True, {"ports": [{"port_id": 1, "ifName": "Gi0/0"}]})
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "get_redirect_url", return_value="/device/1/"),
patch.object(view, "_enrich_ports_with_vlan_data", return_value=[]),
patch.object(view, "get_context_data", return_value={}),
patch.object(view, "get_cache_key", return_value="cache-key") as mock_get_cache_key,
patch.object(view, "get_last_fetched_key", return_value="last-key") as mock_get_last_fetched_key,
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.interfaces_view.render") as mock_render,
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone"),
):
mock_render.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.success.assert_called_once()
mock_render.assert_called_once()
mock_cache.set.assert_called()
# Verify server_key is forwarded to cache key helpers (server-specific namespacing)
mock_get_cache_key.assert_called_with(obj, "ports", "default")
mock_get_last_fetched_key.assert_called_with(obj, "ports", "default")
class TestBaseInterfaceTableViewGetContextData:
"""Tests for BaseInterfaceTableView.get_context_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.model = MagicMock()
view.model.__name__ = "device"
view.interface_name_field = None
return view
def test_cache_miss_returns_empty_table(self):
"""When no cached data, table is None."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with (
patch.object(view, "get_cache_key", return_value="key"),
patch.object(view, "get_last_fetched_key", return_value="last-key"),
patch.object(view, "get_vlan_overrides_key", return_value="overrides-key"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={}),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone"),
):
mock_cache.get.return_value = None
mock_cache.ttl.return_value = None
ctx = view.get_context_data(request, obj, "ifName")
assert ctx["table"] is None
def test_cache_hit_non_vc_builds_table(self):
"""Cached data without VC produces table."""
view = self._make_view()
obj = _mock_obj()
obj.virtual_chassis = None
request = _mock_request()
cached_data = {
"ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": None, "ifDescr": "Gi0/0"}]
}
mock_iface = MagicMock()
mock_iface.name = "Gi0/0"
mock_ifaces_qs = MagicMock()
mock_ifaces_qs.select_related.return_value = [mock_iface]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="key"),
patch.object(view, "get_last_fetched_key", return_value="last-key"),
patch.object(view, "get_vlan_overrides_key", return_value="overrides-key"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
patch.object(view, "_add_vlan_group_selection"),
patch.object(view, "_add_missing_vlans_info"),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
):
mock_cache.get.side_effect = lambda key: cached_data if key == "key" else None
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
ctx = view.get_context_data(request, obj, "ifName")
assert ctx["table"] is mock_table
def test_cache_hit_with_vc_uses_vc_members(self):
"""Cached data with VC queries each chassis member's interfaces."""
view = self._make_view()
vc = MagicMock()
member1 = MagicMock()
member1.id = 10
member2 = MagicMock()
member2.id = 11
vc.members.all.return_value = [member1, member2]
obj = _mock_obj()
obj.virtual_chassis = vc
obj.id = 9999 # distinct from all member IDs so VC path is unambiguous
request = _mock_request()
cached_data = {
"ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": "test", "ifDescr": "Gi0/0"}]
}
mock_iface_qs = MagicMock()
mock_iface_qs.select_related.return_value = []
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="key"),
patch.object(view, "get_last_fetched_key", return_value="last-key"),
patch.object(view, "get_vlan_overrides_key", return_value="overrides-key"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
patch.object(view, "get_interfaces", return_value=mock_iface_qs),
patch.object(view, "_add_vlan_group_selection"),
patch.object(view, "_add_missing_vlans_info"),
patch.object(view, "get_table", return_value=mock_table),
patch(
"netbox_librenms_plugin.views.base.interfaces_view.get_virtual_chassis_member",
return_value=member1,
) as mock_get_vc_member,
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
):
mock_cache.get.side_effect = lambda key: cached_data if key == "key" else None
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
ctx = view.get_context_data(request, obj, "ifName")
# VC members should be included
assert ctx["virtual_chassis_members"] is not None
# get_virtual_chassis_member should have been called with obj and the port name
mock_get_vc_member.assert_called_once_with(obj, "Gi0/0")
class TestBaseInterfaceTableViewAddVlanGroupSelection:
"""Tests for BaseInterfaceTableView._add_vlan_group_selection."""
def _make_view(self):
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view._librenms_api = MagicMock()
return view
def test_no_vlans_produces_empty_map(self):
"""Port with no tagged or untagged VLANs gets empty vlan_group_map."""
view = self._make_view()
port = {"untagged_vlan": None, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {}}
device = MagicMock()
view._add_vlan_group_selection(port, lookup_maps, device)
assert port["vlan_group_map"] == {}
def test_single_group_for_vid_maps_directly(self):
"""When exactly one group contains the VID, it maps without ambiguity."""
view = self._make_view()
group = MagicMock()
group.pk = 1
group.name = "Corp"
port = {"untagged_vlan": 100, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {100: [group]}}
device = MagicMock()
view._add_vlan_group_selection(port, lookup_maps, device)
assert port["vlan_group_map"][100]["group_id"] == "1"
assert port["vlan_group_map"][100]["is_ambiguous"] is False
def test_multiple_groups_with_most_specific_winner(self):
"""Multiple groups: _select_most_specific_group determines winner."""
view = self._make_view()
group_a = MagicMock()
group_a.pk = 1
group_a.name = "Rack-Group"
group_b = MagicMock()
group_b.pk = 2
group_b.name = "Site-Group"
port = {"untagged_vlan": 50, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {50: [group_a, group_b]}}
device = MagicMock()
with patch.object(view, "_select_most_specific_group", return_value=group_a):
view._add_vlan_group_selection(port, lookup_maps, device)
assert port["vlan_group_map"][50]["group_id"] == "1"
assert port["vlan_group_map"][50]["is_ambiguous"] is False
def test_multiple_groups_no_winner_marks_ambiguous(self):
"""Multiple groups with no clear winner produces is_ambiguous=True."""
view = self._make_view()
group_a = MagicMock()
group_b = MagicMock()
port = {"untagged_vlan": 50, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {50: [group_a, group_b]}}
device = MagicMock()
with patch.object(view, "_select_most_specific_group", return_value=None):
view._add_vlan_group_selection(port, lookup_maps, device)
assert port["vlan_group_map"][50]["is_ambiguous"] is True
def test_vid_not_in_any_group_gets_global_entry(self):
"""VID not found in vid_to_groups produces group_id='' and group_name='Global'."""
view = self._make_view()
port = {"untagged_vlan": 999, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {}}
device = MagicMock()
view._add_vlan_group_selection(port, lookup_maps, device)
assert port["vlan_group_map"][999]["group_id"] == ""
assert port["vlan_group_map"][999]["group_name"] == "Global"
def test_vlan_group_overrides_applied(self):
"""vlan_group_overrides replace auto-selection for matching VIDs."""
view = self._make_view()
override_group = MagicMock()
override_group.pk = 99
override_group.name = "Override-Group"
port = {"untagged_vlan": 100, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {}}
device = MagicMock()
with patch("ipam.models.VLANGroup") as MockVLANGroup:
MockVLANGroup.objects.in_bulk.return_value = {99: override_group}
view._add_vlan_group_selection(port, lookup_maps, device, vlan_group_overrides={"100": "99"})
assert port["vlan_group_map"][100]["group_id"] == "99"
assert port["vlan_group_map"][100]["group_name"] == "Override-Group"
def test_override_with_empty_string_forces_global(self):
"""Override with empty string means 'No Group (Global)'."""
view = self._make_view()
port = {"untagged_vlan": 100, "tagged_vlans": []}
lookup_maps = {"vid_to_groups": {}}
device = MagicMock()
with patch("ipam.models.VLANGroup") as MockVLANGroup:
MockVLANGroup.objects.in_bulk.return_value = {}
view._add_vlan_group_selection(port, lookup_maps, device, vlan_group_overrides={"100": ""})
assert port["vlan_group_map"][100]["group_id"] == ""
assert port["vlan_group_map"][100]["group_name"] == "Global"
class TestBaseInterfaceTableViewAddMissingVlansInfo:
"""Tests for BaseInterfaceTableView._add_missing_vlans_info."""
def _make_view(self):
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
return view
def test_all_vlans_found_empty_missing(self):
"""When all VIDs are in vid_to_vlans, missing_vlans is empty."""
view = self._make_view()
port = {"untagged_vlan": 10, "tagged_vlans": [20, 30]}
lookup_maps = {"vid_to_vlans": {10: [MagicMock()], 20: [MagicMock()], 30: [MagicMock()]}}
view._add_missing_vlans_info(port, lookup_maps)
assert port["missing_vlans"] == []
def test_missing_untagged_vlan_added(self):
"""Untagged VID not in vid_to_vlans appears in missing_vlans."""
view = self._make_view()
port = {"untagged_vlan": 99, "tagged_vlans": []}
lookup_maps = {"vid_to_vlans": {}}
view._add_missing_vlans_info(port, lookup_maps)
assert 99 in port["missing_vlans"]
def test_missing_tagged_vlans_added(self):
"""Tagged VIDs not in vid_to_vlans appear in missing_vlans."""
view = self._make_view()
port = {"untagged_vlan": None, "tagged_vlans": [100, 200]}
lookup_maps = {"vid_to_vlans": {100: [MagicMock()]}} # 200 is missing
view._add_missing_vlans_info(port, lookup_maps)
assert 200 in port["missing_vlans"]
assert 100 not in port["missing_vlans"]
def test_no_vlans_produces_empty_missing(self):
"""Port with no VLANs results in empty missing_vlans."""
view = self._make_view()
port = {"untagged_vlan": None, "tagged_vlans": []}
lookup_maps = {"vid_to_vlans": {}}
view._add_missing_vlans_info(port, lookup_maps)
assert port["missing_vlans"] == []
# =============================================================================
# BaseIPAddressTableView
# =============================================================================
class TestBaseIPAddressTableViewCreateBaseIpEntry:
"""Tests for BaseIPAddressTableView._create_base_ip_entry."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
return view
def test_unified_format_preferred(self):
"""When ip_address/prefix_length present, those fields are used."""
view = self._make_view()
ip_entry = {"ip_address": "10.0.0.1", "prefix_length": 24, "port_id": 5}
obj = MagicMock()
obj.name = "router"
obj.get_absolute_url.return_value = "/dcim/devices/1/"
result = view._create_base_ip_entry(ip_entry, obj, vrfs=[])
assert result["ip_address"] == "10.0.0.1"
assert result["prefix_length"] == 24
assert result["ip_with_mask"] == "10.0.0.1/24"
def test_legacy_ipv4_format(self):
"""Legacy ipv4_address/ipv4_prefixlen format is handled."""
view = self._make_view()
ip_entry = {"ipv4_address": "192.168.1.100", "ipv4_prefixlen": 24, "port_id": 10}
obj = MagicMock()
obj.name = "router"
obj.get_absolute_url.return_value = "/dcim/devices/1/"
result = view._create_base_ip_entry(ip_entry, obj, vrfs=[])
assert result["ip_address"] == "192.168.1.100"
assert result["ip_with_mask"] == "192.168.1.100/24"
def test_legacy_ipv6_format(self):
"""Legacy ipv6_compressed/ipv6_prefixlen format is handled."""
view = self._make_view()
ip_entry = {"ipv6_compressed": "2001:db8::1", "ipv6_prefixlen": 64, "port_id": 15}
obj = MagicMock()
obj.name = "router"
obj.get_absolute_url.return_value = "/dcim/devices/1/"
result = view._create_base_ip_entry(ip_entry, obj, vrfs=[])
assert result["ip_address"] == "2001:db8::1"
assert result["ip_with_mask"] == "2001:db8::1/64"
def test_no_valid_format_raises_key_error(self):
"""When no valid IP format is found, KeyError is raised."""
import pytest
view = self._make_view()
ip_entry = {"port_id": 1} # No IP address fields
obj = MagicMock()
with pytest.raises(KeyError):
view._create_base_ip_entry(ip_entry, obj, vrfs=[])
class TestBaseIPAddressTableViewEnrichIpData:
"""Tests for BaseIPAddressTableView.enrich_ip_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_non_dict_entries_skipped(self):
"""Non-dict items in ip_data are silently skipped."""
view = self._make_view()
obj = _mock_obj()
with patch.object(
view,
"_prefetch_netbox_data",
return_value={
"vrfs": [],
"ip_addresses_map": {},
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
"all_interfaces": [],
"device": obj,
},
):
result = view.enrich_ip_data(["not-a-dict", 42, None], obj, "ifName")
assert result == []
def test_entries_without_port_id_skipped(self):
"""Entries missing port_id field are skipped."""
view = self._make_view()
obj = _mock_obj()
ip_data = [{"ip_address": "10.0.0.1", "prefix_length": 24}] # no port_id
with patch.object(
view,
"_prefetch_netbox_data",
return_value={
"vrfs": [],
"ip_addresses_map": {},
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
"all_interfaces": [],
"device": obj,
},
):
result = view.enrich_ip_data(ip_data, obj, "ifName")
assert result == []
def test_new_ip_gets_sync_status(self):
"""IP not in NetBox gets exists=False, status='sync'."""
view = self._make_view()
obj = _mock_obj()
ip_data = [{"ip_address": "10.1.1.1", "prefix_length": 24, "port_id": 10}]
prefetched = {
"vrfs": [],
"ip_addresses_map": {},
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
"all_interfaces": [],
"device": obj,
}
with (
patch.object(view, "_prefetch_netbox_data", return_value=prefetched),
patch.object(view, "_get_port_info", return_value=None),
patch.object(view, "_add_interface_info_to_ip"),
):
result = view.enrich_ip_data(ip_data, obj, "ifName")
assert len(result) == 1
assert result[0]["exists"] is False
assert result[0]["status"] == "sync"
def test_existing_ip_gets_enriched(self):
"""IP that exists in NetBox goes through enrich_existing path."""
view = self._make_view()
obj = _mock_obj()
existing_ip = MagicMock()
existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
existing_ip.vrf = None
existing_ip.assigned_object = None
ip_data = [{"ip_address": "192.168.1.1", "prefix_length": 24, "port_id": 20}]
prefetched = {
"vrfs": [],
"ip_addresses_map": {"192.168.1.1/24": existing_ip},
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
"all_interfaces": [],
"device": obj,
}
with (
patch.object(view, "_prefetch_netbox_data", return_value=prefetched),
patch.object(view, "_get_port_info", return_value=None),
patch.object(view, "_enrich_existing_ip") as mock_enrich,
patch.object(view, "_add_interface_info_to_ip"),
):
view.enrich_ip_data(ip_data, obj, "ifName")
mock_enrich.assert_called_once()
class TestBaseIPAddressTableViewGetPortInfo:
"""Tests for BaseIPAddressTableView._get_port_info."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
return view
def test_cache_miss_calls_api(self):
"""First call for a port_id queries the API."""
view = self._make_view()
port_data = {"port": [{"port_id": 10, "ifName": "Gi0/0"}]}
view._librenms_api.get_port_by_id.return_value = (True, port_data)
cache = {}
result = view._get_port_info(10, cache, "ifName")
assert result == port_data["port"][0]
view._librenms_api.get_port_by_id.assert_called_once_with(10)
def test_cache_hit_skips_api(self):
"""Subsequent call for same port_id uses cache."""
view = self._make_view()
cached_port = {"port_id": 10, "ifName": "Gi0/0"}
cache = {10: cached_port}
result = view._get_port_info(10, cache, "ifName")
assert result is cached_port
view._librenms_api.get_port_by_id.assert_not_called()
def test_api_failure_caches_none(self):
"""When API fails, caches None and returns None."""
view = self._make_view()
view._librenms_api.get_port_by_id.return_value = (False, {})
cache = {}
result = view._get_port_info(99, cache, "ifName")
assert result is None
assert cache[99] is None
def test_api_empty_port_list_caches_none(self):
"""When API returns empty port list, caches None."""
view = self._make_view()
view._librenms_api.get_port_by_id.return_value = (True, {"port": []})
cache = {}
result = view._get_port_info(99, cache, "ifName")
assert result is None
class TestBaseIPAddressTableViewEnrichExistingIp:
"""Tests for BaseIPAddressTableView._enrich_existing_ip."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_matched_by_librenms_id(self):
"""IP matched by LibreNMS port_id → status='matched'."""
view = self._make_view()
assigned_iface = MagicMock()
existing_ip = MagicMock()
existing_ip.vrf = None
existing_ip.assigned_object = assigned_iface
existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
prefetched = {
"interfaces_by_librenms_id": {"10": assigned_iface},
}
enriched = {}
view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)
assert enriched["status"] == "matched"
def test_matched_by_interface_name(self):
"""IP matched by interface name → status='matched'."""
view = self._make_view()
assigned_iface = MagicMock()
assigned_iface.name = "Gi0/0"
assigned_iface.get_absolute_url.return_value = "/dcim/interfaces/5/"
existing_ip = MagicMock()
existing_ip.vrf = None
existing_ip.assigned_object = assigned_iface
existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
prefetched = {
"interfaces_by_librenms_id": {},
}
enriched = {}
view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)
assert enriched["status"] == "matched"
def test_update_status_when_not_matched(self):
"""IP exists but interface doesn't match → status='update'."""
view = self._make_view()
other_iface = MagicMock()
other_iface.name = "Gi0/1" # Different from librenms interface
existing_ip = MagicMock()
existing_ip.vrf = None
existing_ip.assigned_object = other_iface
existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
prefetched = {
"interfaces_by_librenms_id": {}, # No librenms_id match
}
enriched = {}
view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)
assert enriched["status"] == "update"
def test_vrf_info_added_when_present(self):
"""VRF info is added to enriched_ip when IP has a VRF."""
view = self._make_view()
vrf = MagicMock()
vrf.pk = 1
vrf.name = "MGMT-VRF"
existing_ip = MagicMock()
existing_ip.vrf = vrf
existing_ip.assigned_object = None
existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
prefetched = {"interfaces_by_librenms_id": {}}
enriched = {}
view._enrich_existing_ip(enriched, existing_ip, 10, None, prefetched)
assert enriched["vrf_id"] == 1
assert enriched["vrf"] == "MGMT-VRF"
def test_not_assigned_returns_update_early(self):
"""When IP is not assigned to any object, returns early with status='update'."""
view = self._make_view()
existing_ip = MagicMock()
existing_ip.vrf = None
existing_ip.assigned_object = None
existing_ip.get_absolute_url.return_value = "/ipam/ip-addresses/1/"
prefetched = {"interfaces_by_librenms_id": {}}
enriched = {}
view._enrich_existing_ip(enriched, existing_ip, 10, "Gi0/0", prefetched)
assert enriched["status"] == "update"
class TestBaseIPAddressTableViewAddInterfaceInfo:
"""Tests for BaseIPAddressTableView._add_interface_info_to_ip."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_matches_by_librenms_id_first(self):
"""Interface matched by librenms_id takes priority over name match."""
view = self._make_view()
iface_by_id = MagicMock()
iface_by_id.name = "Gi0/0"
iface_by_id.get_absolute_url.return_value = "/dcim/interfaces/1/"
prefetched = {
"interfaces_by_librenms_id": {"10": iface_by_id},
"interfaces_by_name": {},
}
enriched = {}
view._add_interface_info_to_ip(enriched, 10, "Gi0/0", prefetched)
assert enriched["interface_name"] == "Gi0/0"
assert enriched["interface_url"] == "/dcim/interfaces/1/"
def test_falls_back_to_name_match(self):
"""When no librenms_id match, falls back to interface name match."""
view = self._make_view()
iface_by_name = MagicMock()
iface_by_name.get_absolute_url.return_value = "/dcim/interfaces/2/"
prefetched = {
"interfaces_by_librenms_id": {},
"interfaces_by_name": {"Gi0/1": iface_by_name},
}
enriched = {}
view._add_interface_info_to_ip(enriched, 20, "Gi0/1", prefetched)
assert enriched["interface_url"] == "/dcim/interfaces/2/"
def test_no_match_leaves_enriched_unchanged(self):
"""When no interface found, enriched dict is not modified."""
view = self._make_view()
prefetched = {
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
}
enriched = {"ip_address": "10.0.0.1"}
view._add_interface_info_to_ip(enriched, 30, "Gi0/2", prefetched)
assert "interface_url" not in enriched
class TestBaseIPAddressTableViewPrepareContext:
"""Tests for BaseIPAddressTableView._prepare_context."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view._librenms_api.cache_timeout = 300
return view
def test_cache_miss_fetch_fresh_false_returns_none(self):
"""When no cached data and fetch_fresh=False, returns None."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with (
patch.object(view, "get_cache_key", return_value="ip-key"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
):
mock_cache.get.return_value = None
result = view._prepare_context(request, obj, "ifName", fetch_fresh=False)
assert result is None
def test_fetch_fresh_caches_enriched_data(self):
"""When fetch_fresh=True, IP data is fetched, enriched, cached and returned."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
raw_ips = [{"ip_address": "10.0.0.1", "prefix_length": 24, "port_id": 1}]
enriched_ips = [{"ip_with_mask": "10.0.0.1/24", "status": "sync"}]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_ip_addresses", return_value=(True, raw_ips)),
patch.object(view, "enrich_ip_data", return_value=enriched_ips),
patch.object(view, "get_table", return_value=mock_table),
patch.object(view, "get_cache_key", return_value="ip-key"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.timezone") as mock_tz,
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
):
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
result = view._prepare_context(request, obj, "ifName", fetch_fresh=True)
assert result is not None
assert result["table"] is mock_table
mock_cache.set.assert_called()
def test_cache_hit_fetch_fresh_false_uses_cached_data(self):
"""Cached data available with fetch_fresh=False returns context."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
cached_ips = [{"ip_with_mask": "192.168.1.1/24", "status": "matched"}]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="ip-key"),
patch.object(view, "enrich_ip_data", return_value=cached_ips),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.timezone") as mock_tz,
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
):
mock_cache.get.return_value = {"ip_addresses": cached_ips}
mock_cache.ttl.return_value = 200
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
result = view._prepare_context(request, obj, "ifName", fetch_fresh=False)
assert result is not None
assert result["table"] is mock_table
class TestBaseIPAddressTableViewGetContextData:
"""Tests for BaseIPAddressTableView.get_context_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_returns_empty_context_when_no_cache(self):
"""When _prepare_context returns None, returns fallback context with table=None."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with (
patch.object(view, "_prepare_context", return_value=None),
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
):
ctx = view.get_context_data(request, obj)
assert ctx["table"] is None
assert ctx["object"] is obj
assert ctx["cache_expiry"] is None
def test_returns_context_when_cache_populated(self):
"""When _prepare_context returns context, that context is returned."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
prepared = {"table": MagicMock(), "object": obj, "cache_expiry": None, "server_key": "default"}
with (
patch.object(view, "_prepare_context", return_value=prepared),
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
):
ctx = view.get_context_data(request, obj)
assert ctx is prepared
class TestBaseIPAddressTableViewPost:
"""Tests for BaseIPAddressTableView.post."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.model = MagicMock()
view.partial_template_name = "test_template.html"
return view
def test_post_no_ips_renders_error(self):
"""When _prepare_context returns None, renders with error."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "_prepare_context", return_value=None),
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.render") as mock_render,
):
mock_render.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.error.assert_called_once()
mock_render.assert_called_once()
def test_post_success_renders_with_context(self):
"""Successful fetch renders template with context."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
fake_ctx = {"table": MagicMock(), "object": obj}
with (
patch.object(view, "get_object", return_value=obj),
patch.object(view, "_prepare_context", return_value=fake_ctx),
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.messages") as mock_messages,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.render") as mock_render,
):
mock_render.return_value = MagicMock()
view.post(request, pk=1)
mock_messages.success.assert_called_once()
mock_render.assert_called_once()
render_call_kwargs = mock_render.call_args[0]
assert "ip_sync" in render_call_kwargs[2]
class TestBaseIPAddressTableViewPrefetchNetboxData:
"""Tests for BaseIPAddressTableView._prefetch_netbox_data."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_builds_lookup_maps(self):
"""_prefetch_netbox_data builds interface and IP lookup maps."""
view = self._make_view()
iface = MagicMock()
iface.name = "Gi0/0"
obj = MagicMock()
obj.interfaces = MagicMock()
obj.interfaces.all.return_value = [iface]
with (
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_librenms_device_id",
return_value=10,
) as mock_get_librenms_id,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddress") as MockIP,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.VRF") as MockVRF,
):
MockIP.objects.select_related.return_value = []
MockVRF.objects.all.return_value = []
result = view._prefetch_netbox_data(obj)
assert "interfaces_by_librenms_id" in result
assert "interfaces_by_name" in result
assert "ip_addresses_map" in result
assert result["interfaces_by_name"]["Gi0/0"] is iface
assert result["interfaces_by_librenms_id"]["10"] is iface
# Verify server_key is forwarded so IDs are scoped per-server
mock_get_librenms_id.assert_called_once_with(iface, "default", auto_save=False)
class TestBaseIPAddressTableViewGetTable:
"""Tests for BaseIPAddressTableView.get_table."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.request = _mock_request()
return view
def test_get_table_sets_htmx_url(self):
"""get_table creates table and sets htmx_url with tab parameter."""
view = self._make_view()
obj = _mock_obj()
with patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddressTable") as MockTable:
mock_table = MagicMock()
MockTable.return_value = mock_table
view.get_table([], obj, view.request)
assert isinstance(mock_table.htmx_url, str)
assert "ipaddresses" in mock_table.htmx_url
assert "server_key=default" in mock_table.htmx_url
# ===========================================================================
# BaseInterfaceTableView — missing line coverage (lines 31, 44, 51, 70, 149, 203, 224-230)
# ===========================================================================
class TestBaseInterfaceTableViewMissingLines:
"""Targeted tests for remaining uncovered lines in BaseInterfaceTableView."""
def _make_view(self):
from unittest.mock import MagicMock
from netbox_librenms_plugin.views.base.interfaces_view import BaseInterfaceTableView
view = object.__new__(BaseInterfaceTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view._librenms_api.cache_timeout = 300
view.model = MagicMock()
view.model.__name__ = "device"
view.interface_name_field = None
return view
def test_get_object_calls_get_object_or_404(self):
"""get_object delegates to get_object_or_404(self.model, pk=pk)."""
from unittest.mock import MagicMock, patch
view = self._make_view()
mock_obj = MagicMock()
with patch(
"netbox_librenms_plugin.views.base.interfaces_view.get_object_or_404",
return_value=mock_obj,
) as mock_404:
result = view.get_object(42)
mock_404.assert_called_once_with(view.model, pk=42)
assert result is mock_obj
def test_get_interfaces_raises_not_implemented(self):
"""get_interfaces raises NotImplementedError — must be overridden."""
from unittest.mock import MagicMock
view = self._make_view()
try:
view.get_interfaces(MagicMock())
assert False, "Expected NotImplementedError"
except NotImplementedError:
pass
def test_get_redirect_url_raises_not_implemented(self):
"""get_redirect_url raises NotImplementedError — must be overridden."""
from unittest.mock import MagicMock
view = self._make_view()
try:
view.get_redirect_url(MagicMock())
assert False, "Expected NotImplementedError"
except NotImplementedError:
pass
def test_get_table_raises_not_implemented(self):
"""get_table raises NotImplementedError — must be overridden."""
from unittest.mock import MagicMock
view = self._make_view()
try:
view.get_table([], MagicMock(), "ifName")
assert False, "Expected NotImplementedError"
except NotImplementedError:
pass
def test_get_context_data_with_none_interface_name_field_calls_helper(self):
"""When interface_name_field=None, get_interface_name_field(request) is called."""
from unittest.mock import MagicMock, patch
view = self._make_view()
obj = MagicMock()
obj.virtual_chassis = None
obj.id = 1
request = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="k"),
patch.object(view, "get_last_fetched_key", return_value="lk"),
patch.object(view, "get_vlan_overrides_key", return_value="vk"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={}),
patch(
"netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field",
return_value="ifDescr",
) as mock_gnf,
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone"),
):
mock_cache.get.return_value = None
mock_cache.ttl.return_value = None
view.get_context_data(request, obj, interface_name_field=None)
mock_gnf.assert_called_once_with(request)
def test_ifalias_cleared_when_matches_ifdescr(self):
"""port['ifAlias'] is set to '' when it equals ifDescr (line 203)."""
from unittest.mock import MagicMock, patch
view = self._make_view()
obj = MagicMock()
obj.virtual_chassis = None
obj.id = 1
request = MagicMock()
ports_data = [
{
"port_id": 1,
"ifName": "Gi0/0",
"ifDescr": "GigabitEthernet0/0",
"ifAlias": "GigabitEthernet0/0", # matches ifDescr -> cleared
"ifAdminStatus": "up",
}
]
cached_data = {"ports": ports_data}
mock_iface = MagicMock()
mock_iface.name = "Gi0/0"
mock_ifaces_qs = MagicMock()
mock_ifaces_qs.select_related.return_value = [mock_iface]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="k"),
patch.object(view, "get_last_fetched_key", return_value="lk"),
patch.object(view, "get_vlan_overrides_key", return_value="vk"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
patch.object(view, "_add_vlan_group_selection"),
patch.object(view, "_add_missing_vlans_info"),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
):
mock_cache.get.side_effect = lambda key: cached_data if key == "k" else None
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
view.get_context_data(request, obj, "ifName")
assert ports_data[0]["ifAlias"] == ""
def test_netbox_only_interfaces_vc_device_name_lookup(self):
"""VC branch fetches device_name from VC members for netbox-only interfaces (lines 224-226)."""
from unittest.mock import MagicMock, patch
view = self._make_view()
member1 = MagicMock()
member1.id = 10
member1.name = "switch-1"
vc = MagicMock()
vc.members.all.return_value = [member1]
vc.members.get.return_value = member1
obj = MagicMock()
obj.virtual_chassis = vc
obj.id = 10
request = MagicMock()
# Gi0/0 is in LibreNMS; Gi0/1 is only in NetBox
cached_data = {"ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": "x"}]}
netbox_only_iface = MagicMock()
netbox_only_iface.name = "Gi0/1"
netbox_only_iface.id = 99
netbox_only_iface.type = "1000base-t"
mock_ifaces_qs = MagicMock()
mock_ifaces_qs.select_related.return_value = [netbox_only_iface]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="k"),
patch.object(view, "get_last_fetched_key", return_value="lk"),
patch.object(view, "get_vlan_overrides_key", return_value="vk"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
patch.object(view, "_add_vlan_group_selection"),
patch.object(view, "_add_missing_vlans_info"),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_virtual_chassis_member", return_value=member1),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
):
mock_cache.get.side_effect = lambda key: cached_data if key == "k" else None
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
ctx = view.get_context_data(request, obj, "ifName")
netbox_only = ctx.get("netbox_only_interfaces", [])
assert any(item["name"] == "Gi0/1" for item in netbox_only)
gi01 = next(i for i in netbox_only if i["name"] == "Gi0/1")
assert gi01["device_name"] == "switch-1"
def test_netbox_only_interfaces_non_vc_device_name_from_obj(self):
"""Non-VC branch uses obj.name directly for device_name (line 228)."""
from unittest.mock import MagicMock, patch
view = self._make_view()
obj = MagicMock()
obj.virtual_chassis = None
obj.id = 1
obj.name = "router-1"
request = MagicMock()
# Gi0/0 in LibreNMS; Gi0/1 only in NetBox
cached_data = {"ports": [{"port_id": 1, "ifName": "Gi0/0", "ifAdminStatus": "up", "ifAlias": None}]}
netbox_only_iface = MagicMock()
netbox_only_iface.name = "Gi0/1"
netbox_only_iface.id = 55
netbox_only_iface.type = "1000base-t"
mock_ifaces_qs = MagicMock()
mock_ifaces_qs.select_related.return_value = [netbox_only_iface]
mock_table = MagicMock()
mock_table.configure = MagicMock()
with (
patch.object(view, "get_cache_key", return_value="k"),
patch.object(view, "get_last_fetched_key", return_value="lk"),
patch.object(view, "get_vlan_overrides_key", return_value="vk"),
patch.object(view, "get_vlan_groups_for_device", return_value=[]),
patch.object(view, "_build_vlan_lookup_maps", return_value={"vid_to_groups": {}, "vid_to_vlans": {}}),
patch.object(view, "get_interfaces", return_value=mock_ifaces_qs),
patch.object(view, "_add_vlan_group_selection"),
patch.object(view, "_add_missing_vlans_info"),
patch.object(view, "get_table", return_value=mock_table),
patch("netbox_librenms_plugin.views.base.interfaces_view.get_interface_name_field", return_value="ifName"),
patch("netbox_librenms_plugin.views.base.interfaces_view.cache") as mock_cache,
patch("netbox_librenms_plugin.views.base.interfaces_view.timezone") as mock_tz,
):
mock_cache.get.side_effect = lambda key: cached_data if key == "k" else None
mock_cache.ttl.return_value = 300
mock_tz.now.return_value = MagicMock()
mock_tz.timedelta.return_value = MagicMock()
ctx = view.get_context_data(request, obj, "ifName")
netbox_only = ctx.get("netbox_only_interfaces", [])
gi01 = next((i for i in netbox_only if i["name"] == "Gi0/1"), None)
assert gi01 is not None
assert gi01["device_name"] == "router-1"