"""Tests for SingleCableVerifyView and SingleInterfaceVerifyView VC resolution. Verifies that both views delegate VC device resolution to get_librenms_sync_device() and handle the None return gracefully (e.g. empty VC members or vc_position type errors). """ import json from unittest.mock import MagicMock, patch def _make_request(body: dict) -> MagicMock: """Create a mock POST request with JSON body.""" request = MagicMock() request.body = json.dumps(body).encode() request.user.has_perm.return_value = True return request def _make_vc_device(pk=1, name="vc-device"): """Create a mock Device that belongs to a virtual chassis.""" device = MagicMock() device.pk = pk device.id = pk device.name = name device._meta.model_name = "device" device.virtual_chassis = MagicMock() device.interfaces.filter.return_value.first.return_value = None return device # --------------------------------------------------------------------------- # SingleCableVerifyView # --------------------------------------------------------------------------- class TestSingleCableVerifyView: """SingleCableVerifyView.post() VC resolution and None guard.""" def _make_view(self): from netbox_librenms_plugin.views.base.cables_view import SingleCableVerifyView view = object.__new__(SingleCableVerifyView) view._librenms_api = MagicMock() return view @patch("netbox_librenms_plugin.views.base.cables_view.get_object_or_404") @patch("netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device") @patch("netbox_librenms_plugin.views.base.cables_view.cache") def test_vc_no_resolvable_sync_device_returns_empty_row(self, mock_cache, mock_sync, mock_get_obj): """VC where get_librenms_sync_device returns None → empty row, no crash.""" device = _make_vc_device(pk=1) mock_get_obj.return_value = device mock_sync.return_value = None view = self._make_view() request = _make_request({"device_id": 1, "local_port_id": "42"}) response = view.post(request) data = json.loads(response.content) assert data["status"] == "success" assert data["formatted_row"]["cable_status"] == "Missing Ports" mock_cache.get.assert_not_called() @patch("netbox_librenms_plugin.views.base.cables_view.get_object_or_404") @patch("netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device") @patch("netbox_librenms_plugin.views.base.cables_view.cache") def test_vc_resolved_sync_device_uses_cache(self, mock_cache, mock_sync, mock_get_obj): """VC with resolved sync device: cache is queried with that device's key.""" device = _make_vc_device(pk=1) sync_device = _make_vc_device(pk=2, name="sync-device") mock_get_obj.return_value = device mock_sync.return_value = sync_device mock_cache.get.return_value = None # No cached data view = self._make_view() request = _make_request({"device_id": 1, "local_port_id": "42"}) view.post(request) mock_sync.assert_called_once_with(device, server_key=view._librenms_api.server_key) mock_cache.get.assert_called_once() cache_key = mock_cache.get.call_args[0][0] assert "device" in cache_key assert "2" in cache_key @patch("netbox_librenms_plugin.views.base.cables_view.get_object_or_404") @patch("netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device") @patch("netbox_librenms_plugin.views.base.cables_view.cache") def test_non_vc_device_skips_sync_device_lookup(self, mock_cache, mock_sync, mock_get_obj, mock_netbox_device): """Non-VC device: get_librenms_sync_device is NOT called.""" mock_netbox_device.virtual_chassis = None mock_get_obj.return_value = mock_netbox_device mock_cache.get.return_value = None view = self._make_view() request = _make_request({"device_id": 5, "local_port_id": "10"}) view.post(request) mock_sync.assert_not_called() mock_cache.get.assert_called_once() def test_no_device_id_returns_empty_row(self): """Missing device_id: returns default empty formatted_row.""" view = self._make_view() request = _make_request({"local_port_id": "42"}) response = view.post(request) data = json.loads(response.content) assert data["status"] == "success" assert data["formatted_row"]["cable_status"] == "Missing Ports" # --------------------------------------------------------------------------- # SingleInterfaceVerifyView # --------------------------------------------------------------------------- class TestSingleInterfaceVerifyView: """SingleInterfaceVerifyView.post() VC resolution and None guard.""" def _make_view(self): from netbox_librenms_plugin.views.object_sync.devices import SingleInterfaceVerifyView view = object.__new__(SingleInterfaceVerifyView) view._librenms_api = MagicMock() return view @patch("netbox_librenms_plugin.views.object_sync.devices.get_object_or_404") @patch("netbox_librenms_plugin.views.object_sync.devices.get_librenms_sync_device") @patch("netbox_librenms_plugin.views.object_sync.devices.cache") def test_vc_no_resolvable_sync_device_returns_404(self, mock_cache, mock_sync, mock_get_obj): """VC where get_librenms_sync_device returns None → 404 JSON error, no crash.""" device = _make_vc_device(pk=1) mock_get_obj.return_value = device mock_sync.return_value = None view = self._make_view() request = _make_request( { "device_id": 1, "interface_name": "eth0", "interface_name_field": "ifName", } ) response = view.post(request) assert response.status_code == 404 data = json.loads(response.content) assert data["status"] == "error" assert "sync device" in data["message"].lower() mock_cache.get.assert_not_called() @patch("netbox_librenms_plugin.views.object_sync.devices.get_object_or_404") @patch("netbox_librenms_plugin.views.object_sync.devices.get_librenms_sync_device") @patch("netbox_librenms_plugin.views.object_sync.devices.cache") def test_vc_resolved_sync_device_uses_cache(self, mock_cache, mock_sync, mock_get_obj): """VC with resolved sync device: cache is queried with that device's key.""" device = _make_vc_device(pk=1) sync_device = _make_vc_device(pk=3, name="sync-member") mock_get_obj.return_value = device mock_sync.return_value = sync_device mock_cache.get.return_value = None view = self._make_view() request = _make_request( { "device_id": 1, "interface_name": "eth0", "interface_name_field": "ifName", } ) view.post(request) mock_sync.assert_called_once_with(device, server_key=view._librenms_api.server_key) mock_cache.get.assert_called_once() cache_key = mock_cache.get.call_args[0][0] assert "3" in cache_key @patch("netbox_librenms_plugin.views.object_sync.devices.get_object_or_404") @patch("netbox_librenms_plugin.views.object_sync.devices.get_librenms_sync_device") @patch("netbox_librenms_plugin.views.object_sync.devices.cache") def test_non_vc_device_skips_sync_device_lookup(self, mock_cache, mock_sync, mock_get_obj, mock_netbox_device): """Non-VC device: get_librenms_sync_device is NOT called.""" mock_netbox_device.virtual_chassis = None mock_get_obj.return_value = mock_netbox_device mock_cache.get.return_value = None view = self._make_view() request = _make_request( { "device_id": 5, "interface_name": "eth0", "interface_name_field": "ifName", } ) view.post(request) mock_sync.assert_not_called() mock_cache.get.assert_called_once() def test_no_device_id_returns_400(self): """Missing device_id: returns 400 error.""" view = self._make_view() request = _make_request({"interface_name": "eth0"}) response = view.post(request) assert response.status_code == 400 data = json.loads(response.content) assert data["status"] == "error"