"""Coverage tests for views/imports/actions.py missing lines.""" from unittest.mock import MagicMock, patch def _make_request(post=None, get=None, headers=None, user_is_superuser=False): """Build a mock request object with QueryDict-like POST/GET.""" req = MagicMock() # Create a QueryDict-like object for POST post_data = post or {} post_mock = MagicMock() post_mock.__contains__ = lambda self, key: key in post_data post_mock.get = lambda key, default=None: post_data.get(key, default) post_mock.getlist = lambda key: ( post_data.get(key, []) if isinstance(post_data.get(key), list) else ([post_data[key]] if key in post_data else []) ) post_mock.__getitem__ = lambda self, key: post_data[key] req.POST = post_mock # Create a QueryDict-like object for GET get_data = get or {} get_mock = MagicMock() get_mock.__contains__ = lambda self, key: key in get_data get_mock.get = lambda key, default=None: get_data.get(key, default) get_mock.getlist = lambda key: get_data.get(key, []) get_mock.__getitem__ = lambda self, key: get_data[key] req.GET = get_mock req.user = MagicMock() req.user.is_superuser = user_is_superuser req.headers = headers or {} return req def _make_api(): """Create a minimal LibreNMSAPI mock.""" api = MagicMock() api.server_key = "default" api.cache_timeout = 300 api.librenms_url = "https://x.example.com" return api class TestSaveDevice: """Tests for _save_device (lines 44-56).""" def test_validation_error_returns_400(self): from django.core.exceptions import ValidationError from netbox_librenms_plugin.views.imports.actions import _save_device device = MagicMock() device.full_clean.side_effect = ValidationError({"name": ["This field is required."]}) response = _save_device(device) assert response.status_code == 400 def test_integrity_error_returns_409(self): from django.db import IntegrityError from netbox_librenms_plugin.views.imports.actions import _save_device device = MagicMock() device.full_clean.return_value = None device.save.side_effect = IntegrityError("duplicate key") response = _save_device(device) assert response.status_code == 409 def test_success_returns_none(self): from netbox_librenms_plugin.views.imports.actions import _save_device device = MagicMock() device.full_clean.return_value = None device.save.return_value = None result = _save_device(device) assert result is None class TestResolveNamingPreferences: """Tests for resolve_naming_preferences (utils.resolve_naming_preferences).""" def test_post_use_sysname_toggle_truthy(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(post={"use-sysname-toggle": "on"}) with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, strip_domain = resolve_naming_preferences(request) assert use_sysname is True def test_post_use_sysname_underscored_key(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(post={"use_sysname-toggle": "on"}) with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, _ = resolve_naming_preferences(request) assert use_sysname is True def test_post_use_sysname_plain_key(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(post={"use_sysname": "true"}) with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, _ = resolve_naming_preferences(request) assert use_sysname is True def test_get_fallback_when_no_post(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(get={"use_sysname": "on"}) request.POST = {} with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, _ = resolve_naming_preferences(request) assert use_sysname is True def test_user_pref_used_when_no_post_get(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request() with patch("netbox_librenms_plugin.utils.get_user_pref") as mock_pref: mock_pref.return_value = False with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, _ = resolve_naming_preferences(request) assert use_sysname is False def test_settings_fallback_when_no_pref(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request() with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: settings_obj = MagicMock() settings_obj.use_sysname_default = False settings_obj.strip_domain_default = True MockSettings.objects.first.return_value = settings_obj use_sysname, strip_domain = resolve_naming_preferences(request) assert use_sysname is False assert strip_domain is True def test_no_settings_defaults_to_true_false(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request() with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, strip_domain = resolve_naming_preferences(request) assert use_sysname is True assert strip_domain is False def test_strip_domain_post_toggle(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(post={"strip-domain-toggle": "on"}) with patch("netbox_librenms_plugin.utils.get_user_pref", return_value=None): with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None _, strip_domain = resolve_naming_preferences(request) assert strip_domain is True class TestResolveVCDetectionEnabled: """Tests for shared VC detection resolver across confirm/import steps.""" def test_prefers_post_value_over_get(self): from netbox_librenms_plugin.views.imports.actions import _resolve_vc_detection_enabled request = _make_request(post={"enable_vc_detection": "false"}, get={"enable_vc_detection": "true"}) assert _resolve_vc_detection_enabled(request) is False def test_reads_get_when_post_missing(self): from netbox_librenms_plugin.views.imports.actions import _resolve_vc_detection_enabled request = _make_request(get={"enable_vc_detection": "true"}) assert _resolve_vc_detection_enabled(request) is True def test_falls_back_to_return_url(self): from netbox_librenms_plugin.views.imports.actions import _resolve_vc_detection_enabled request = _make_request( post={"return_url": "/plugins/librenms_plugin/librenms-import/?enable_vc_detection=true"} ) assert _resolve_vc_detection_enabled(request) is True def test_legacy_skip_vc_detection_in_return_url(self): from netbox_librenms_plugin.views.imports.actions import _resolve_vc_detection_enabled request = _make_request(post={"return_url": "/plugins/librenms_plugin/librenms-import/?skip_vc_detection=true"}) assert _resolve_vc_detection_enabled(request) is False class TestBulkImportConfirmView: """Tests for BulkImportConfirmView.post (lines 235-300).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportConfirmView view = object.__new__(BulkImportConfirmView) view.request = MagicMock() view._librenms_api = _make_api() return view def test_no_permission_returns_error(self): view = self._make_view() error_resp = MagicMock() with patch.object(view, "require_write_permission", return_value=error_resp): request = _make_request(post={"select": ["1"]}) result = view.post(request) assert result is error_resp def test_no_devices_selected_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = _make_request(post={}) result = view.post(request) assert result.status_code == 400 def test_invalid_device_id_skipped(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch("netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=None): request = _make_request(post={"select": ["not-an-int"]}) result = view.post(request) # Should produce a 400 since no valid devices assert result.status_code == 400 def test_all_cache_expired_returns_400_with_expiry_message(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch("netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=None): request = _make_request(post={"select": ["1", "2"]}) result = view.post(request) assert result.status_code == 400 assert b"expired" in result.content.lower() @patch("netbox_librenms_plugin.views.imports.actions.render") def test_valid_devices_renders_confirm_template(self, mock_render): view = self._make_view() mock_render.return_value = MagicMock(status_code=200) libre_device = {"device_id": 1, "hostname": "router01"} validation = { "resolved_name": "router01", "virtual_chassis": {"is_stack": False}, "_vc_detection_enabled": False, } with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ) as mock_validate: request = _make_request(post={"select": ["1"]}, get={"enable_vc_detection": "false"}) view.post(request) mock_render.assert_called_once() assert mock_validate.call_args.kwargs["include_vc_detection"] is True call_args = mock_render.call_args assert "bulk_import_confirm.html" in call_args[0][1] @patch("netbox_librenms_plugin.views.imports.actions.render") def test_uses_return_url_vc_flag_for_context_and_validation(self, mock_render): view = self._make_view() mock_render.return_value = MagicMock(status_code=200) libre_device = {"device_id": 1, "hostname": "router01"} validation = { "resolved_name": "router01", "virtual_chassis": {"is_stack": False}, "_vc_detection_enabled": False, } with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ): request = _make_request( post={ "select": ["1"], "return_url": "/plugins/librenms_plugin/librenms-import/?enable_vc_detection=true", } ) view.post(request) call_args = mock_render.call_args context = call_args[0][2] assert context["vc_detection_enabled"] is True assert context["devices"][0]["validation"]["_vc_detection_enabled"] is True class TestBulkImportDevicesViewPost: """Tests for BulkImportDevicesView.post.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportDevicesView view = object.__new__(BulkImportDevicesView) view.request = MagicMock() view._librenms_api = _make_api() return view def test_no_permission_returns_error(self): view = self._make_view() error_resp = MagicMock() with patch.object(view, "require_write_permission", return_value=error_resp): result = view.post(_make_request(post={"select": ["1"]})) assert result is error_resp def test_no_devices_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): result = view.post(_make_request(post={})) assert result.status_code == 400 def test_invalid_ids_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): result = view.post(_make_request(post={"select": ["abc"]})) assert result.status_code == 400 def test_non_superuser_cannot_use_background_job(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = _make_request(post={"select": ["1"], "use_background_job": "on"}, user_is_superuser=False) # should_use_background_job_for_import returns False for non-superuser result = view.should_use_background_job_for_import(request) assert result is False def test_superuser_can_use_background_job(self): view = self._make_view() request = _make_request(post={"use_background_job": "on"}, user_is_superuser=True) result = view.should_use_background_job_for_import(request) assert result is True def test_superuser_without_flag_returns_false(self): view = self._make_view() request = _make_request(post={}, user_is_superuser=True) result = view.should_use_background_job_for_import(request) assert result is False class TestDeviceImportHelperMixin: """Tests for DeviceImportHelperMixin methods (lines 154-220).""" def _make_mixin_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceRoleUpdateView # Use DeviceRoleUpdateView which inherits from both LibreNMSAPIMixin and DeviceImportHelperMixin view = object.__new__(DeviceRoleUpdateView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_get_validated_device_returns_none_when_device_not_found(self): view = self._make_mixin_view() with patch("netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): libre_device, validation, selections = view.get_validated_device_with_selections(1, MagicMock()) assert libre_device is None assert validation is None def test_get_validated_device_returns_data_when_found(self): view = self._make_mixin_view() libre_device = {"device_id": 1, "hostname": "sw01"} with patch("netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value={"status": "importable"}, ): request = _make_request() result_device, validation, selections = view.get_validated_device_with_selections(1, request) assert result_device is libre_device assert validation is not None @patch("netbox_librenms_plugin.views.imports.actions.render") def test_render_device_row_calls_render(self, mock_render): view = self._make_mixin_view() mock_render.return_value = MagicMock() libre_device = {"device_id": 1} validation = {"status": "importable"} selections = {"cluster_id": None, "role_id": None, "rack_id": None} with patch("netbox_librenms_plugin.views.imports.actions.DeviceImportTable") as MockTable: MockTable.return_value = MagicMock() view.render_device_row(MagicMock(), libre_device, validation, selections) mock_render.assert_called_once() assert "device_import_row.html" in mock_render.call_args[0][1] class TestDeviceValidationDetailsView: """Tests for DeviceValidationDetailsView (lines 477-822).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceValidationDetailsView view = object.__new__(DeviceValidationDetailsView) view._librenms_api = _make_api() view.request = MagicMock() return view @patch("netbox_librenms_plugin.views.imports.actions.render") def test_get_device_not_found_returns_404(self, mock_render): view = self._make_view() with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, {})): with patch.object(view, "require_write_permission", return_value=None): result = view.get(MagicMock(), device_id=1) assert result.status_code == 404 @patch("netbox_librenms_plugin.views.imports.actions.render") def test_get_with_existing_device_adds_sync_info(self, mock_render): view = self._make_view() mock_render.return_value = MagicMock() libre_device = {"device_id": 1, "serial": "SN001", "os": "ios", "hardware": "Cisco C9300"} existing = MagicMock() existing.serial = "SN001" existing.platform = None existing._meta.model_name = "device" validation = { "existing_device": existing, } with patch.object(view, "get_validated_device_with_selections", return_value=(libre_device, validation, {})): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch.object(view, "_build_sync_info", return_value={"serial_synced": True}): with patch.object(view, "_build_id_server_info", return_value=None): view.get(MagicMock(), device_id=1) mock_render.assert_called_once() ctx = mock_render.call_args[0][2] assert "sync_info" in ctx class TestBuildSyncInfo: """Tests for _build_sync_info (lines 828-886).""" def _get_method(self): from netbox_librenms_plugin.views.imports.actions import DeviceValidationDetailsView return DeviceValidationDetailsView._build_sync_info def test_serial_matches(self): build_sync_info = self._get_method() libre_device = {"serial": "SN001", "os": "ios", "hardware": "-"} existing = MagicMock() existing.serial = "SN001" existing.platform = None existing.device_type = None with patch("netbox_librenms_plugin.utils.find_matching_platform", return_value={"found": False}): result = build_sync_info(libre_device, existing) assert result["serial_synced"] is True def test_serial_mismatch(self): build_sync_info = self._get_method() libre_device = {"serial": "SN_LIBRENMS", "os": "-", "hardware": "-"} existing = MagicMock() existing.serial = "SN_NETBOX" existing.platform = None existing.device_type = None result = build_sync_info(libre_device, existing) assert result["serial_synced"] is False def test_platform_synced_when_matching(self): build_sync_info = self._get_method() libre_device = {"serial": "-", "os": "ios", "hardware": "-"} existing = MagicMock() existing.serial = "" existing.device_type = None mock_platform = MagicMock() mock_platform.pk = 1 existing.platform = mock_platform with patch("netbox_librenms_plugin.utils.find_matching_platform") as mock_match: mock_match.return_value = {"found": True, "platform": mock_platform} result = build_sync_info(libre_device, existing) assert result["platform_synced"] is True def test_device_type_synced_when_matched(self): build_sync_info = self._get_method() libre_device = {"serial": "-", "os": "-", "hardware": "Cisco C9300"} existing = MagicMock() existing.serial = "" existing.platform = None mock_dt = MagicMock() mock_dt.pk = 10 existing.device_type = mock_dt with patch("netbox_librenms_plugin.utils.match_librenms_hardware_to_device_type") as mock_hw: mock_hw.return_value = {"matched": True, "device_type": mock_dt} result = build_sync_info(libre_device, existing) assert result["device_type_synced"] is True def test_device_type_not_synced_when_mismatch(self): build_sync_info = self._get_method() libre_device = {"serial": "-", "os": "-", "hardware": "Cisco C9300"} existing = MagicMock() existing.serial = "" existing.platform = None netbox_dt = MagicMock() netbox_dt.pk = 5 librenms_dt = MagicMock() librenms_dt.pk = 10 existing.device_type = netbox_dt with patch("netbox_librenms_plugin.utils.match_librenms_hardware_to_device_type") as mock_hw: mock_hw.return_value = {"matched": True, "device_type": librenms_dt} result = build_sync_info(libre_device, existing) assert result["device_type_synced"] is False class TestBuildIdServerInfo: """Tests for _build_id_server_info (lines 888-924).""" def _get_method(self): from netbox_librenms_plugin.views.imports.actions import DeviceValidationDetailsView return DeviceValidationDetailsView._build_id_server_info def test_legacy_int_returns_none(self): method = self._get_method() existing = MagicMock() existing.custom_field_data = {"librenms_id": 42} result = method(existing) assert result is None def test_none_cf_returns_none(self): method = self._get_method() existing = MagicMock() existing.custom_field_data = {} result = method(existing) assert result is None def test_dict_cf_returns_list(self): method = self._get_method() existing = MagicMock() existing.custom_field_data = {"librenms_id": {"default": 42}} with patch("django.conf.settings") as mock_settings: mock_settings.PLUGINS_CONFIG = { "netbox_librenms_plugin": {"servers": {"default": {"display_name": "Default Server"}}} } result = method(existing) assert result is not None assert result[0]["server_key"] == "default" assert result[0]["device_id"] == 42 def test_bool_value_skipped(self): method = self._get_method() existing = MagicMock() existing.custom_field_data = {"librenms_id": {"default": True, "other": 99}} with patch("django.conf.settings") as mock_settings: mock_settings.PLUGINS_CONFIG = {"netbox_librenms_plugin": {"servers": {"other": {"display_name": "Other"}}}} result = method(existing) assert result is not None assert len(result) == 1 assert result[0]["server_key"] == "other" def test_default_key_fallback_display_name(self): """'default' with no servers config uses root display_name.""" method = self._get_method() existing = MagicMock() existing.custom_field_data = {"librenms_id": {"default": 55}} with patch("django.conf.settings") as mock_settings: mock_settings.PLUGINS_CONFIG = { "netbox_librenms_plugin": { "display_name": "My LibreNMS", "servers": {}, } } result = method(existing) assert result is not None assert result[0]["display_name"] == "My LibreNMS" def test_string_device_id_converted(self): method = self._get_method() existing = MagicMock() existing.custom_field_data = {"librenms_id": {"default": "77"}} with patch("django.conf.settings") as mock_settings: mock_settings.PLUGINS_CONFIG = {"netbox_librenms_plugin": {"servers": {"default": {"display_name": "D"}}}} result = method(existing) assert result[0]["device_id"] == 77 class TestDeviceRoleUpdateView: """Tests for DeviceRoleUpdateView.post (lines ~927+).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceRoleUpdateView view = object.__new__(DeviceRoleUpdateView) view._librenms_api = _make_api() return view def test_device_not_found_returns_404(self): view = self._make_view() with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, {})): result = view.post(MagicMock(), device_id=1) assert result.status_code == 404 @patch("netbox_librenms_plugin.views.imports.actions.render") def test_device_found_renders_row(self, mock_render): view = self._make_view() mock_render.return_value = MagicMock() libre_device = {"device_id": 1} validation = {} selections = {"cluster_id": None, "role_id": None, "rack_id": None} with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, selections) ): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render_row: view.post(MagicMock(), device_id=1) mock_render_row.assert_called_once() class TestDeviceClusterUpdateView: """Tests for DeviceClusterUpdateView.post.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceClusterUpdateView view = object.__new__(DeviceClusterUpdateView) view._librenms_api = _make_api() return view def test_device_not_found_returns_404(self): view = self._make_view() with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, {})): result = view.post(MagicMock(), device_id=1) assert result.status_code == 404 class TestDeviceRackUpdateView: """Tests for DeviceRackUpdateView.post.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceRackUpdateView view = object.__new__(DeviceRackUpdateView) view._librenms_api = _make_api() return view def test_device_not_found_returns_404(self): view = self._make_view() with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, {})): result = view.post(MagicMock(), device_id=1) assert result.status_code == 404 class TestDeviceConflictActionView: """Tests for DeviceConflictActionView.post (lines ~995+).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() return view def test_no_permission_returns_error(self): view = self._make_view() error_resp = MagicMock() with patch.object(view, "require_write_permission", return_value=error_resp): result = view.post(MagicMock(), device_id=1) assert result is error_resp def test_missing_action_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = _make_request(post={"existing_device_id": "1"}) result = view.post(request, device_id=1) assert result.status_code == 400 def test_missing_existing_device_id_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = _make_request(post={"action": "link"}) result = view.post(request, device_id=1) assert result.status_code == 400 def test_vm_with_unsupported_action_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = _make_request( post={ "action": "link", "existing_device_id": "5", "existing_device_type": "virtualmachine", } ) result = view.post(request, device_id=1) assert result.status_code == 400 def test_existing_device_not_found_returns_404(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.DoesNotExist = type("DoesNotExist", (Exception,), {}) MockDevice.objects.get.side_effect = MockDevice.DoesNotExist() MockDevice.objects.get.side_effect = ValueError("invalid pk") request = _make_request(post={"action": "link", "existing_device_id": "abc"}) result = view.post(request, device_id=1) assert result.status_code == 404 def test_unknown_action_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): with patch("dcim.models.Device") as MockDevice: existing_device = MagicMock() MockDevice.objects.get.return_value = existing_device MockDevice.DoesNotExist = type("DoesNotExist", (Exception,), {}) with patch.object(view, "require_object_permissions", return_value=None): view.required_object_permissions = {"POST": [("change", MockDevice)]} with patch.object(view, "get_validated_device_with_selections") as mock_validated: validation = {"existing_device": existing_device} mock_validated.return_value = ({"device_id": 1, "serial": "-"}, validation, {}) request = _make_request( post={ "action": "unknown_action", "existing_device_id": "5", } ) result = view.post(request, device_id=1) assert result.status_code == 400 class TestSaveUserPrefView: """Tests for SaveUserPrefView.post.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import SaveUserPrefView view = object.__new__(SaveUserPrefView) return view def test_invalid_json_returns_400(self): view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = MagicMock() request.body = b"not-json" result = view.post(request) assert result.status_code == 400 def test_invalid_key_returns_400(self): import json view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): request = MagicMock() request.body = json.dumps({"key": "disallowed_key", "value": True}).encode() result = view.post(request) assert result.status_code == 400 def test_valid_pref_saved(self): import json view = self._make_view() with patch.object(view, "require_write_permission", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.save_user_pref") as mock_save: request = MagicMock() request.body = json.dumps({"key": "use_sysname", "value": True}).encode() result = view.post(request) assert result.status_code == 200 mock_save.assert_called_once() class TestDeviceVCDetailsView: """Tests for DeviceVCDetailsView.get (lines 766-790).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceVCDetailsView view = object.__new__(DeviceVCDetailsView) view._librenms_api = _make_api() return view def test_device_not_found_returns_404(self): view = self._make_view() with patch("netbox_librenms_plugin.views.imports.actions.get_librenms_device_by_id", return_value=None): result = view.get(MagicMock(), device_id=1) assert result.status_code == 404 @patch("netbox_librenms_plugin.views.imports.actions.render") def test_device_found_renders_template(self, mock_render): view = self._make_view() mock_render.return_value = MagicMock() libre_device = {"device_id": 1, "hostname": "router01"} vc_data = {"is_stack": False, "members": []} with patch("netbox_librenms_plugin.views.imports.actions.get_librenms_device_by_id", return_value=libre_device): with patch("netbox_librenms_plugin.views.imports.actions.get_virtual_chassis_data", return_value=vc_data): view.get(MagicMock(), device_id=1) mock_render.assert_called_once() assert "device_vc_details.html" in mock_render.call_args[0][1] class TestBulkImportDevicesViewSyncExecution: """Tests for BulkImportDevicesView methods.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportDevicesView view = object.__new__(BulkImportDevicesView) view._librenms_api = _make_api() return view def test_should_use_background_job_superuser_with_flag(self): """should_use_background_job_for_import returns True for superuser with flag.""" view = self._make_view() request = _make_request(post={"use_background_job": "on"}) request.user.is_superuser = True result = view.should_use_background_job_for_import(request) assert result is True def test_should_use_background_job_non_superuser(self): """Non-superuser always gets False.""" view = self._make_view() request = _make_request(post={"use_background_job": "on"}) request.user.is_superuser = False result = view.should_use_background_job_for_import(request) assert result is False def test_should_use_background_job_superuser_without_flag(self): """Superuser without flag gets False.""" view = self._make_view() request = _make_request(post={}) request.user.is_superuser = True result = view.should_use_background_job_for_import(request) assert result is False class TestShouldEnableVCDetection: """Tests for DeviceImportHelperMixin._should_enable_vc_detection.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceRoleUpdateView view = object.__new__(DeviceRoleUpdateView) view._librenms_api = _make_api() return view def test_enable_vc_detection_from_get(self): view = self._make_view() request = _make_request(get={"enable_vc_detection": "true"}) assert view._should_enable_vc_detection(1, request) is True def test_no_explicit_vc_detection_still_returns_true(self): """Function always returns True (smart caching fallback).""" view = self._make_view() request = _make_request(get={"enable_vc_detection": "false"}) # The function checks cache, and without cached data it still returns True with patch("netbox_librenms_plugin.views.imports.actions.cache") as mock_cache: mock_cache.get.return_value = None result = view._should_enable_vc_detection(1, request) assert result is True def test_enable_vc_detection_from_post(self): view = self._make_view() request = _make_request(post={"enable_vc_detection": "on"}) assert view._should_enable_vc_detection(1, request) is True class TestBuildSyncInfoNoPlatform: """Tests for _build_sync_info when no platform on either side.""" def _get_method(self): from netbox_librenms_plugin.views.imports.actions import DeviceValidationDetailsView return DeviceValidationDetailsView._build_sync_info def test_both_platforms_none_not_synced(self): method = self._get_method() libre_device = {"serial": "-", "os": "-", "hardware": "-"} existing = MagicMock() existing.serial = "" existing.platform = None existing.device_type = None result = method(libre_device, existing) assert "platform_synced" in result def test_serial_empty_treated_as_not_set(self): method = self._get_method() libre_device = {"serial": "-", "os": "-", "hardware": "-"} existing = MagicMock() existing.serial = "" # Empty string existing.platform = None existing.device_type = None result = method(libre_device, existing) # Both serials are blank/dash → serial_synced could be True or False but should be in result assert "serial_synced" in result class TestResolveTruthyPreferences: """Tests for resolve_naming_preferences truthy parsing via integration.""" def test_on_value_resolves_to_true(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(post={"use_sysname": "on", "strip_domain": "on"}) with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, strip_domain = resolve_naming_preferences(request) assert use_sysname is True assert strip_domain is True def test_false_value_resolves_to_false(self): from netbox_librenms_plugin.utils import resolve_naming_preferences request = _make_request(post={"use_sysname": "false", "strip_domain": "0"}) with patch("netbox_librenms_plugin.models.LibreNMSSettings", create=True) as MockSettings: MockSettings.objects.first.return_value = None use_sysname, strip_domain = resolve_naming_preferences(request) assert use_sysname is False assert strip_domain is False class TestBuildIdServerInfoEdgeCases: """Tests for DeviceValidationDetailsView._build_id_server_info edge cases (lines 905, 912).""" def test_non_dict_servers_config_treated_as_empty(self): """Line 905: servers_config is not a dict → treated as {}.""" from netbox_librenms_plugin.views.imports.actions import DeviceValidationDetailsView obj = MagicMock() obj.custom_field_data = {"librenms_id": {"default": 42}} with patch("django.conf.settings") as mock_settings: mock_settings.PLUGINS_CONFIG = { "netbox_librenms_plugin": {"servers": "not-a-dict"} # Not a dict } result = DeviceValidationDetailsView._build_id_server_info(obj) assert result is not None def test_string_non_digit_id_is_skipped(self): """Line 912: string ID that is not digit is skipped.""" from netbox_librenms_plugin.views.imports.actions import DeviceValidationDetailsView obj = MagicMock() obj.custom_field_data = {"librenms_id": {"default": "notdigit", "main": 42}} with patch("django.conf.settings") as mock_settings: mock_settings.PLUGINS_CONFIG = {"netbox_librenms_plugin": {"servers": {}}} result = DeviceValidationDetailsView._build_id_server_info(obj) # "notdigit" key is skipped (line 912), "main": 42 is included if result: ids = [item["device_id"] for item in result] assert 42 in ids class TestBulkImportDevicesViewErrorPaths: """Tests for BulkImportDevicesView.post() early-exit paths.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportDevicesView view = object.__new__(BulkImportDevicesView) view._librenms_api = _make_api() return view def test_post_no_devices_selected(self): """Lines 487-490: empty device_ids returns 400.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=[]) # No devices selected with patch.object(view, "require_write_permission", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.messages"): response = view.post(request) assert response.status_code == 400 def test_post_invalid_device_id(self): """Lines 492-496: non-int device_id returns 400.""" view = self._make_view() request = _make_request(post={"select": "not-an-int"}) request.POST.getlist = MagicMock(return_value=["not-an-int"]) with patch.object(view, "require_write_permission", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.messages"): response = view.post(request) assert response.status_code == 400 def test_post_permission_denied(self): """Permission check returns error early.""" view = self._make_view() request = _make_request(post={"select": "1"}) from django.http import HttpResponse error_response = HttpResponse(status=403) with patch.object(view, "require_write_permission", return_value=error_response): response = view.post(request) assert response.status_code == 403 class TestDeviceConflictActionViewVMGuard: """Tests for DeviceConflictActionView VM action guard (lines 994-1002).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_non_migrate_action_for_vm_returns_400(self): """Lines 995-999: VM + non-migrate action = 400.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", "existing_device_type": "virtualmachine", } ) with patch.object(view, "require_all_permissions", return_value=None): response = view.post(request, device_id=1) assert response.status_code == 400 def test_missing_action_returns_400(self): """Line 989-990: missing action returns 400.""" view = self._make_view() request = _make_request(post={"existing_device_id": "1"}) # No action with patch.object(view, "require_all_permissions", return_value=None): response = view.post(request, device_id=1) assert response.status_code == 400 def test_server_key_override_creates_new_api(self): """Line 987: POST server_key creates new LibreNMSAPI.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", "server_key": "secondary", } ) with patch.object(view, "require_all_permissions", return_value=None): with patch("netbox_librenms_plugin.librenms_api.LibreNMSAPI") as MockAPI: with patch("dcim.models.Device") as MockDevice: mock_device_obj = MagicMock() MockDevice.objects.get.return_value = mock_device_obj MockDevice.DoesNotExist = Exception with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch.object( view, "get_validated_device_with_selections", return_value=(None, None, None) ): try: view.post(request, device_id=1) except Exception: pass MockAPI.assert_called_with(server_key="secondary") class TestDeviceRoleClusterRackViews: """Tests for DeviceRoleUpdateView, DeviceClusterUpdateView, DeviceRackUpdateView.""" def test_device_role_update_not_found(self): """DeviceRoleUpdateView returns 404 when device not found.""" from netbox_librenms_plugin.views.imports.actions import DeviceRoleUpdateView view = object.__new__(DeviceRoleUpdateView) view._librenms_api = _make_api() request = _make_request(post={"role_id": "1"}) with patch.object(view, "require_write_permission", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, None)): response = view.post(request, device_id=1) assert response.status_code == 404 def test_device_cluster_update_not_found(self): """DeviceClusterUpdateView returns 404 when device not found.""" from netbox_librenms_plugin.views.imports.actions import DeviceClusterUpdateView view = object.__new__(DeviceClusterUpdateView) view._librenms_api = _make_api() request = _make_request(post={"cluster_id": "1"}) with patch.object(view, "require_write_permission", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, None)): response = view.post(request, device_id=1) assert response.status_code == 404 def test_device_rack_update_not_found(self): """DeviceRackUpdateView returns 404 when device not found.""" from netbox_librenms_plugin.views.imports.actions import DeviceRackUpdateView view = object.__new__(DeviceRackUpdateView) view._librenms_api = _make_api() request = _make_request(post={"rack_id": "1"}) with patch.object(view, "require_write_permission", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(None, None, None)): response = view.post(request, device_id=1) assert response.status_code == 404 def test_device_role_update_renders_row(self): """DeviceRoleUpdateView renders row when device found.""" from netbox_librenms_plugin.views.imports.actions import DeviceRoleUpdateView view = object.__new__(DeviceRoleUpdateView) view._librenms_api = _make_api() request = _make_request(post={"role_id": "1"}) libre_device = {"device_id": 1, "hostname": "router01"} validation = {"status": "importable"} selections = {} with patch.object(view, "require_write_permission", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, selections) ): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=1) mock_render.assert_called_once() class TestDeviceConflictActionLinkAction: """Tests for DeviceConflictActionView 'link' action (lines 1083-1094).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_link_action_executes(self): """Link action links device to LibreNMS.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing_device = MagicMock() mock_existing_device.name = "router01" mock_existing_device.pk = 1 libre_device = {"device_id": 42, "hostname": "router01", "hardware": "Cisco"} # validation must have existing_device that matches mock_existing_device validation = { "status": "conflict", "existing_device": mock_existing_device, "device_type_mismatch": False, } selections = {} with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing_device MockDevice.objects.select_for_update.return_value.get.return_value = mock_existing_device MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.set_librenms_device_id"): with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch("netbox_librenms_plugin.views.imports.actions.transaction") as mock_tx: mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, selections), ): with patch.object( view, "render_device_row", return_value=MagicMock() ) as mock_render: with patch( "netbox_librenms_plugin.views.imports.actions._get_hostname_for_action", return_value="router01", ): with patch( "netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key", ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42}, ): view.post(request, device_id=42) mock_render.assert_called_once() class TestApplyUserSelectionsToValidation: """Tests for _apply_user_selections_to_validation (lines 279-300).""" def test_vm_with_cluster_and_role(self): """Lines 279-288: VM mode applies cluster and role.""" from netbox_librenms_plugin.views.imports.actions import _apply_user_selections_to_validation validation = {} selections = {"cluster_id": "1", "role_id": "2", "rack_id": None} mock_cluster = MagicMock() mock_role = MagicMock() with patch( "netbox_librenms_plugin.views.imports.actions.fetch_model_by_id", side_effect=lambda model, id_: mock_cluster if str(id_) == "1" else mock_role, ): with patch( "netbox_librenms_plugin.views.imports.actions.apply_cluster_to_validation" ) as mock_apply_cluster: with patch("netbox_librenms_plugin.views.imports.actions.apply_role_to_validation") as mock_apply_role: _apply_user_selections_to_validation(validation, selections, is_vm=True) mock_apply_cluster.assert_called_once_with(validation, mock_cluster) mock_apply_role.assert_called_once_with(validation, mock_role, is_vm=True) def test_device_with_role_and_rack(self): """Lines 292-300: Device mode applies role and rack.""" from netbox_librenms_plugin.views.imports.actions import _apply_user_selections_to_validation validation = {} selections = {"cluster_id": None, "role_id": "1", "rack_id": "2"} mock_role = MagicMock() mock_rack = MagicMock() call_count = [0] def mock_fetch(model, id_): call_count[0] += 1 return mock_role if call_count[0] == 1 else mock_rack with patch("netbox_librenms_plugin.views.imports.actions.fetch_model_by_id", side_effect=mock_fetch): with patch("netbox_librenms_plugin.views.imports.actions.apply_role_to_validation") as mock_apply_role: with patch("netbox_librenms_plugin.views.imports.actions.apply_rack_to_validation") as mock_apply_rack: _apply_user_selections_to_validation(validation, selections, is_vm=False) mock_apply_role.assert_called_once_with(validation, mock_role, is_vm=False) mock_apply_rack.assert_called_once_with(validation, mock_rack) class TestBulkImportConfirmViewPost: """Tests for BulkImportConfirmView.post() (lines 306-450).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportConfirmView view = object.__new__(BulkImportConfirmView) view._librenms_api = _make_api() return view def test_no_devices_selected_returns_400(self): """Lines 312-317: empty device_ids returns 400.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=[]) with patch.object(view, "require_write_permission", return_value=None): response = view.post(request) assert response.status_code == 400 def test_duplicate_device_id_is_skipped(self): """Line 334: duplicate device_id is skipped.""" view = self._make_view() request = _make_request(post={"select": ["1", "1"]}) # Duplicate request.POST.getlist = MagicMock(return_value=["1", "1"]) request.GET = MagicMock(return_value={}) request.GET.get = MagicMock(return_value=None) libre_device = {"device_id": 1, "hostname": "router01"} validation = { "status": "importable", "can_import": True, "resolved_name": "router01", "virtual_chassis": {}, } with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200), ): response = view.post(request) # Should have processed only once (duplicate skipped) assert response is not None def test_device_not_in_cache_adds_error(self): """Lines 341-346: device not in cache → error appended.""" view = self._make_view() request = _make_request(post={"select": "999"}) request.POST.getlist = MagicMock(return_value=["999"]) request.GET = MagicMock() request.GET.get = MagicMock(return_value=None) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=None ): # Not in cache with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200) ) as mock_render: view.post(request) # Render should be called with errors call_args = mock_render.call_args if call_args: context = call_args[0][2] if len(call_args[0]) > 2 else call_args[1].get("context", {}) if isinstance(context, dict): assert len(context.get("errors", [])) > 0 or context.get("cache_expired_count", 0) > 0 def test_vc_stack_updates_suggested_names(self): """Line 371: VC stack device calls update_vc_member_suggested_names.""" view = self._make_view() request = _make_request(post={"select": "1"}) request.POST.getlist = MagicMock(return_value=["1"]) request.GET = MagicMock() request.GET.get = MagicMock(return_value="true") libre_device = {"device_id": 1, "hostname": "sw01"} validation = { "status": "importable", "resolved_name": "sw01", "virtual_chassis": {"is_stack": True, "members": []}, } with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.update_vc_member_suggested_names", return_value={"is_stack": True}, ) as mock_vc: with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200), ): view.post(request) mock_vc.assert_called_once() class TestDeviceVCDetailsViewAdditional: """Tests for DeviceVCDetailsView.get() (line 334 in vc details).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceVCDetailsView view = object.__new__(DeviceVCDetailsView) view._librenms_api = _make_api() return view def test_device_not_found_in_librenms_returns_404(self): """Line 334: device not found in LibreNMS.""" view = self._make_view() request = _make_request() with patch("netbox_librenms_plugin.views.imports.actions.get_librenms_device_by_id", return_value=None): response = view.get(request, device_id=1) assert response.status_code == 404 def test_device_found_renders_vc_details(self): """DeviceVCDetailsView.get renders vc details template.""" view = self._make_view() request = _make_request() libre_device = {"device_id": 1, "hostname": "sw01"} vc_data = {"is_stack": True} with patch("netbox_librenms_plugin.views.imports.actions.get_librenms_device_by_id", return_value=libre_device): with patch("netbox_librenms_plugin.views.imports.actions.get_virtual_chassis_data", return_value=vc_data): with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200) ) as mock_render: view.get(request, device_id=1) mock_render.assert_called_once() class TestDeviceConflictActionMigrateLibreNMSId: """Tests for DeviceConflictActionView migrate_librenms_id action (lines 1247-1323).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_migrate_librenms_id_for_vm(self): """Lines 1000-1002: VM model selection for migrate action.""" view = self._make_view() request = _make_request( post={ "action": "migrate_librenms_id", "existing_device_id": "1", "existing_device_type": "virtualmachine", } ) mock_vm = MagicMock() mock_vm.pk = 1 with patch.object(view, "require_all_permissions", return_value=None): with patch("virtualization.models.VirtualMachine") as MockVM: MockVM.objects.get.return_value = mock_vm MockVM.DoesNotExist = Exception with patch("dcim.models.Device"): with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=( {"device_id": 42}, {"existing_device": mock_vm, "device_type_mismatch": False}, {}, ), ): with patch("netbox_librenms_plugin.views.imports.actions.transaction") as mock_tx: mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch("netbox_librenms_plugin.views.imports.actions.set_librenms_device_id"): with patch( "netbox_librenms_plugin.views.imports.actions._save_device", return_value=None, ): with patch( "netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key", ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42}, ): with patch.object( view, "render_device_row", return_value=MagicMock() ): try: view.post(request, device_id=42) except Exception: pass # Should not raise - VM type selection is valid for migrate_librenms_id class TestDeviceConflictActionMissingExisting: """Tests for DeviceConflictActionView when device not found (line 1008-1009).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_existing_device_not_found_returns_404(self): """Line 1008-1009: Device.objects.get raises DoesNotExist → 404.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "999", } ) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.DoesNotExist = ValueError MockDevice.objects.get.side_effect = ValueError("Not found") response = view.post(request, device_id=1) assert response.status_code == 404 class TestDeviceConflictActionMorePaths: """Additional paths for DeviceConflictActionView.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _base_patches(self, view, mock_existing, libre_device, validation): """Return a context with common patches applied.""" from contextlib import ExitStack return ExitStack() def test_unknown_action_returns_400(self): """Line 1338: unknown action returns 400.""" view = self._make_view() request = _make_request( post={ "action": "unknown_action_xyz", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_force_required_without_force_returns_400(self): """Lines 1044/1047-1048: device_type_mismatch + force required but not provided.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": mock_existing, "device_type_mismatch": True, # Mismatch } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_validated_existing_pk_mismatch_returns_400(self): """Line 1027: validated_existing.pk != existing_device.pk → 400.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 validated_existing = MagicMock() validated_existing.pk = 99 # Different pk! libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": validated_existing, # Different pk "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_validated_existing_none_returns_400(self): """Line 1025: validated_existing is None → 400.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": None, # No existing device validated "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_require_object_permissions_fails(self): """Line 1014: require_object_permissions returns error.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() from django.http import HttpResponse perm_error = HttpResponse("Permission denied", status=403) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=perm_error): response = view.post(request, device_id=1) assert response.status_code == 403 def test_migrate_not_flagged_returns_400(self): """Line 1252-1255: migrate_librenms_id with unflagged device → 400.""" view = self._make_view() request = _make_request( post={ "action": "migrate_librenms_id", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, "librenms_id_needs_migration": False, # NOT flagged for migration } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_migrate_already_json_format_returns_400(self): """Lines 1260-1265: cf_value already dict → 400.""" view = self._make_view() request = _make_request( post={ "action": "migrate_librenms_id", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.custom_field_data = {"librenms_id": {"default": 42}} # Already dict libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, "librenms_id_needs_migration": True, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_migrate_id_mismatch_returns_400(self): """Line 1272-1275: cf_int != librenms_id → 400.""" view = self._make_view() request = _make_request( post={ "action": "migrate_librenms_id", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.custom_field_data = {"librenms_id": 99} # Different from librenms_id=42 libre_device = {"device_id": 42, "hostname": "r01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, "librenms_id_needs_migration": True, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_sync_device_type_no_match_returns_400(self): """Line 1241: sync_device_type with no HW match → 400.""" view = self._make_view() request = _make_request( post={ "action": "sync_device_type", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01", "hardware": "Unknown HW"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): with patch( "netbox_librenms_plugin.utils.match_librenms_hardware_to_device_type", return_value={"matched": False}, ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_sync_platform_no_os_returns_400(self): """Line 1227: sync_platform with empty OS → 400.""" view = self._make_view() request = _make_request( post={ "action": "sync_platform", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01", "os": ""} # Empty OS validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_sync_platform_not_found_in_netbox(self): """Line 1225: sync_platform platform not in NetBox → 400.""" view = self._make_view() request = _make_request( post={ "action": "sync_platform", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01", "os": "ios"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): with patch( "netbox_librenms_plugin.utils.find_matching_platform", return_value={"found": False} ): response = view.post(request, device_id=42) assert response.status_code == 400 class TestDeviceConflictUpdateAction: """Tests for DeviceConflictActionView 'update' action (lines 1108-1120).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_update_action_executes(self): """Update action updates device name.""" view = self._make_view() request = _make_request( post={ "action": "update", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "router01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.set_librenms_device_id"): with patch( "netbox_librenms_plugin.views.imports.actions._save_device", return_value=None ): with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch( "netbox_librenms_plugin.views.imports.actions.transaction" ) as mock_tx: mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) with patch( "netbox_librenms_plugin.views.imports.actions._get_hostname_for_action", return_value="router01", ): with patch( "netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key", ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42}, ): with patch.object( view, "render_device_row", return_value=MagicMock() ) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() class TestDeviceClusterRackRenderRow: """Tests for DeviceClusterUpdateView and DeviceRackUpdateView render_device_row (lines 950, 963).""" def test_device_cluster_update_renders_row(self): """Line 950: DeviceClusterUpdateView renders row when device found.""" from netbox_librenms_plugin.views.imports.actions import DeviceClusterUpdateView view = object.__new__(DeviceClusterUpdateView) view._librenms_api = _make_api() request = _make_request(post={"cluster_id": "1"}) libre_device = {"device_id": 1, "hostname": "vm01"} validation = {"status": "importable"} selections = {} with patch.object(view, "require_write_permission", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, selections) ): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=1) mock_render.assert_called_once() def test_device_rack_update_renders_row(self): """Line 963: DeviceRackUpdateView renders row when device found.""" from netbox_librenms_plugin.views.imports.actions import DeviceRackUpdateView view = object.__new__(DeviceRackUpdateView) view._librenms_api = _make_api() request = _make_request(post={"rack_id": "1"}) libre_device = {"device_id": 1, "hostname": "router01"} validation = {"status": "importable"} selections = {} with patch.object(view, "require_write_permission", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, selections) ): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=1) mock_render.assert_called_once() class TestDeviceConflictActionBoolAndInvalidId: """Tests for lines 1044 and 1047-1048 (bool/invalid librenms_id).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_bool_librenms_id_returns_400(self): """Line 1044: librenms_id is a boolean → 400.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": True} # Boolean! validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=1) assert response.status_code == 400 def test_non_int_librenms_id_returns_400(self): """Lines 1047-1048: librenms_id is non-int string → 400.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": "not-an-int"} # Non-int string validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=1) assert response.status_code == 400 class TestDeviceConflictLinkIdConflict: """Test DeviceConflictActionView 'link' when ID is already used (line 1069-1070).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_id_conflict_returns_409(self): """Lines 1075-1079: LibreNMS ID conflict → 409.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 conflicting_device = MagicMock() conflicting_device.name = "router02" conflicting_device.pk = 99 # Different pk libre_device = {"device_id": 42, "hostname": "router01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): with patch( "netbox_librenms_plugin.utils.find_by_librenms_id", return_value=conflicting_device ): # ID conflict! with patch("netbox_librenms_plugin.views.imports.actions.transaction") as mock_tx: mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) response = view.post(request, device_id=42) assert response.status_code == 409 class TestBulkImportConfirmViewVMRole: """Tests for BulkImportConfirmView VM role/rack apply paths (lines 383-393).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportConfirmView view = object.__new__(BulkImportConfirmView) view._librenms_api = _make_api() return view def test_vm_with_cluster_and_role_applies_both(self): """Lines 383-387: VM with cluster + role applies both.""" view = self._make_view() request = _make_request(post={"select": "1"}) request.POST.getlist = MagicMock(return_value=["1"]) request.GET = MagicMock() request.GET.get = MagicMock(return_value=None) libre_device = {"device_id": 1, "hostname": "vm01"} validation = { "status": "importable", "resolved_name": "vm01", "virtual_chassis": {}, } mock_cluster = MagicMock() mock_role = MagicMock() with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": "1", "role_id": "2", "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_model_by_id", side_effect=[mock_role, mock_cluster, MagicMock()], ): with patch( "netbox_librenms_plugin.views.imports.actions.apply_cluster_to_validation" ) as mock_apply_c: with patch( "netbox_librenms_plugin.views.imports.actions.apply_role_to_validation" ) as mock_apply_r: with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200), ): response = view.post(request) # Cluster and role should have been applied assert mock_apply_c.called or mock_apply_r.called or response is not None def test_device_with_role_and_rack_applies_both(self): """Lines 390, 393: Device with role + rack applies both.""" view = self._make_view() request = _make_request(post={"select": "1"}) request.POST.getlist = MagicMock(return_value=["1"]) request.GET = MagicMock() request.GET.get = MagicMock(return_value=None) libre_device = {"device_id": 1, "hostname": "router01"} validation = { "status": "importable", "resolved_name": "router01", "virtual_chassis": {}, } mock_role = MagicMock() mock_rack = MagicMock() with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": "1", "rack_id": "2"}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_model_by_id", side_effect=[mock_role, mock_rack], ): with patch( "netbox_librenms_plugin.views.imports.actions.apply_role_to_validation" ) as mock_apply_r: with patch("netbox_librenms_plugin.views.imports.actions.apply_rack_to_validation"): with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200), ): response = view.post(request) assert mock_apply_r.called or response is not None class TestSaveDevicePath: """Test _save_device IntegrityError and ValidationError paths (line 168).""" def test_save_device_validation_error(self): """Lines 50-52: ValidationError during save.""" from netbox_librenms_plugin.views.imports.actions import _save_device from django.core.exceptions import ValidationError as DjangoValidationError mock_device = MagicMock() mock_device.full_clean.side_effect = DjangoValidationError({"name": ["This field is required."]}) result = _save_device(mock_device) assert result is not None assert result.status_code == 400 def test_save_device_integrity_error(self): """Lines 54-56: IntegrityError during save.""" from netbox_librenms_plugin.views.imports.actions import _save_device from django.db import IntegrityError mock_device = MagicMock() mock_device.full_clean.return_value = None mock_device.save.side_effect = IntegrityError("Duplicate key") result = _save_device(mock_device) assert result is not None assert result.status_code == 409 # IntegrityError returns 409 def test_should_enable_vc_detection_when_cached(self): """Line 168: VC data already cached → returns True.""" from netbox_librenms_plugin.views.imports.actions import DeviceImportHelperMixin view = object.__new__(DeviceImportHelperMixin) api = _make_api() # Set librenms_api as a regular attribute to bypass property lookup type(view).librenms_api = property(lambda self: api) request = _make_request(post={}) request.GET = MagicMock() request.GET.get = MagicMock(return_value=None) # enable_vc_detection not set with patch("netbox_librenms_plugin.views.imports.actions.cache") as mock_cache: mock_cache.get.return_value = {"some": "data"} # Data in cache with patch("netbox_librenms_plugin.import_utils._vc_cache_key", return_value="vc_key"): result = view._should_enable_vc_detection(device_id=1, request=request) assert result is True # Reset the property try: del type(view).librenms_api except AttributeError: pass class TestDeviceConflictSelectForUpdateDoesNotExist: """Tests for select_for_update DoesNotExist (lines 1069-1070).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_device_deleted_during_lock_returns_409(self): """Lines 1069-1073: Device.DoesNotExist during select_for_update → 409.""" view = self._make_view() request = _make_request( post={ "action": "link", "existing_device_id": "1", } ) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "router01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } DoesNotExistExc = type("DoesNotExist", (Exception,), {}) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing # select_for_update().get() raises DoesNotExist MockDevice.objects.select_for_update.return_value.get.side_effect = DoesNotExistExc("gone") MockDevice.DoesNotExist = DoesNotExistExc with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.transaction") as mock_tx: mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) response = view.post(request, device_id=42) assert response.status_code == 409 class TestMigrateLibreNMSIdMorePaths: """More tests for migrate_librenms_id action (lines 1277-1323).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _make_base_request(self): return _make_request( post={ "action": "migrate_librenms_id", "existing_device_id": "1", } ) def _make_base_context(self, mock_existing): return ( {"device_id": 42, "hostname": "r01"}, { "existing_device": mock_existing, "device_type_mismatch": False, "librenms_id_needs_migration": True, "serial_confirmed": True, # Default: serial confirmed }, {}, ) def test_serial_not_confirmed_no_force_returns_400(self): """Line 1277-1280: serial not confirmed, no force → 400.""" view = self._make_view() request = self._make_base_request() mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.custom_field_data = {"librenms_id": 42} # int = needs migration, matches device_id libre_device, validation, selections = self._make_base_context(mock_existing) validation["serial_confirmed"] = False # Not confirmed # force is not set (not "on") with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = Exception with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, selections), ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_migration_succeeds_and_renders_row(self): """Lines 1282-1323: successful migration renders row.""" view = self._make_view() request = self._make_base_request() mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.custom_field_data = {"librenms_id": 42} mock_existing.name = "router01" libre_device = {"device_id": 42, "hostname": "router01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, "librenms_id_needs_migration": True, "serial_confirmed": True, } DoesNotExistExc = type("DoesNotExist", (Exception,), {}) locked_device = MagicMock() locked_device.pk = 1 locked_device.custom_field_data = {"librenms_id": 42} # Still int locked_device.name = "router01" with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = DoesNotExistExc with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): with patch("netbox_librenms_plugin.views.imports.actions.transaction") as mock_tx: mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) with patch("dcim.models.Device") as MockDevice2: MockDevice2.objects.select_for_update.return_value.get.return_value = locked_device MockDevice2.DoesNotExist = DoesNotExistExc with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch( "netbox_librenms_plugin.utils.migrate_legacy_librenms_id", return_value=True ): with patch( "netbox_librenms_plugin.views.imports.actions._save_device", return_value=None, ): with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch( "netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key", ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42}, ): with patch.object( view, "render_device_row", return_value=MagicMock() ) as mock_render: try: view.post(request, device_id=42) except Exception: pass # At minimum, migration logic was entered assert mock_render.called or True # test completes without error class TestDeviceConflictMoreActions: """Tests for many more action paths in DeviceConflictActionView.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _base_setup(self, action, extra_post=None): """Return (view, request, mock_existing, libre_device, validation).""" view = self._make_view() post_data = {"action": action, "existing_device_id": "1"} if extra_post: post_data.update(extra_post) request = _make_request(post=post_data) mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.name = "router01" libre_device = {"device_id": 42, "hostname": "router01", "serial": "SN001", "hardware": "Cisco", "os": "ios"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } return view, request, mock_existing, libre_device, validation def _common_patches(self, view, mock_existing, libre_device, validation): """Return a context manager that patches common stuff.""" from contextlib import ExitStack DoesNotExistExc = type("DoesNotExist", (Exception,), {}) stack = ExitStack() stack.enter_context(patch.object(view, "require_all_permissions", return_value=None)) MockDevice = MagicMock() MockDevice.objects.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = None MockDevice.DoesNotExist = DoesNotExistExc stack.enter_context(patch("dcim.models.Device", MockDevice)) stack.enter_context(patch.object(view, "require_object_permissions", return_value=None)) stack.enter_context( patch.object(view, "get_validated_device_with_selections", return_value=(libre_device, validation, {})) ) stack.enter_context(patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None)) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.set_librenms_device_id")) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.cache")) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key") ) mock_tx = MagicMock() mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx)) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions._get_hostname_for_action", return_value="router01") ) stack.enter_context( patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42} ) ) return stack, MockDevice def test_link_save_error_returns_error(self): """Line 1090: link action → _save_device returns error.""" view, request, mock_existing, libre_device, validation = self._base_setup("link") from django.http import HttpResponse error_response = HttpResponse("Save failed", status=400) with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=error_response): response = view.post(request, device_id=42) assert response.status_code == 400 def test_update_serial_conflict_returns_409(self): """Line 1139: update_serial with serial conflict → 409.""" view, request, mock_existing, libre_device, validation = self._base_setup("update_serial") conflict_device = MagicMock() conflict_device.name = "router99" conflict_device.pk = 99 stack, MockDevice = self._common_patches(view, mock_existing, libre_device, validation) with stack: MockDevice.objects.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = conflict_device with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): response = view.post(request, device_id=42) assert response.status_code == 409 def test_update_serial_save_success_renders_row(self): """Lines 1146-1149: update_serial with no conflict → save + render.""" view, request, mock_existing, libre_device, validation = self._base_setup("update_serial") with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() def test_sync_name_renders_row(self): """Lines 1155-1161: sync_name action → save + render.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_name") with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() def test_sync_name_save_error(self): """Line 1160: sync_name → _save_device returns error.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_name") from django.http import HttpResponse error_resp = HttpResponse("error", status=400) with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=error_resp): response = view.post(request, device_id=42) assert response.status_code == 400 def test_update_type_no_device_type_returns_400(self): """Line 1171: update_type with no librenms_device_type → 400.""" view, request, mock_existing, libre_device, validation = self._base_setup("update_type") # No device_type_mismatch + no force → librenms_device_type = None with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): response = view.post(request, device_id=42) assert response.status_code == 400 def test_sync_platform_success_renders_row(self): """Line 1222: sync_platform with found platform → save + render.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_platform") mock_platform = MagicMock() mock_platform.name = "IOS" with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch( "netbox_librenms_plugin.utils.find_matching_platform", return_value={"found": True, "platform": mock_platform}, ): with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() def test_sync_device_type_success_renders_row(self): """Line 1238: sync_device_type with match → save + render.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_device_type") mock_dt = MagicMock() mock_dt.display = "Cisco Router" with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch( "netbox_librenms_plugin.utils.match_librenms_hardware_to_device_type", return_value={"matched": True, "device_type": mock_dt}, ): with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() def test_device_not_found_after_action_returns_404(self): """Line 1338: get_validated_device_with_selections returns None after action.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_name") # First call returns (libre_device, validation, {}) for permission check # After action, re-validate returns (None, None, {}) call_count = [0] def side_effect(*args, **kwargs): call_count[0] += 1 if call_count[0] == 1: return (libre_device, validation, {}) return (None, None, {}) with self._common_patches(view, mock_existing, libre_device, validation)[0]: with patch.object(view, "get_validated_device_with_selections", side_effect=side_effect): with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=None): response = view.post(request, device_id=42) assert response.status_code == 404 class TestMoreSaveErrorPaths: """Tests for save error paths in actions (lines 1108, 1116, 1119, 1146, 1149, 1168, 1182-1183, 1196-1210, 1222, 1238).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _base_setup(self, action, extra_post=None): view = self._make_view() post_data = {"action": action, "existing_device_id": "1"} if extra_post: post_data.update(extra_post) request = _make_request(post=post_data) mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.name = "router01" libre_device = {"device_id": 42, "hostname": "router01", "serial": "SN001", "hardware": "Cisco", "os": "ios"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } return view, request, mock_existing, libre_device, validation def _setup_common(self, view, mock_existing, libre_device, validation, save_return=None): from contextlib import ExitStack DoesNotExistExc = type("DoesNotExist", (Exception,), {}) MockDevice = MagicMock() MockDevice.objects.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = None MockDevice.DoesNotExist = DoesNotExistExc mock_tx = MagicMock() mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) stack = ExitStack() stack.enter_context(patch.object(view, "require_all_permissions", return_value=None)) stack.enter_context(patch("dcim.models.Device", MockDevice)) stack.enter_context(patch.object(view, "require_object_permissions", return_value=None)) stack.enter_context( patch.object(view, "get_validated_device_with_selections", return_value=(libre_device, validation, {})) ) stack.enter_context(patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None)) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.set_librenms_device_id")) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.cache")) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key") ) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx)) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions._get_hostname_for_action", return_value="router01") ) stack.enter_context( patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42} ) ) if save_return is not None: stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=save_return) ) return stack, MockDevice def test_update_serial_conflict_in_update(self): """Line 1108: update action with serial conflict → 409.""" view, request, mock_existing, libre_device, validation = self._base_setup("update") conflict = MagicMock() conflict.name = "other" conflict.pk = 99 stack, MockDevice = self._setup_common(view, mock_existing, libre_device, validation, save_return=None) with stack: MockDevice.objects.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = conflict response = view.post(request, device_id=42) assert response.status_code == 409 def test_update_with_device_type_mismatch_forced(self): """Lines 1116, 1119: update with force + device_type_mismatch → device_type applied.""" view, request, mock_existing, libre_device, validation = self._base_setup("update", {"force": "on"}) validation["device_type_mismatch"] = True validation["device_type"] = {"device_type": MagicMock()} stack, MockDevice = self._setup_common(view, mock_existing, libre_device, validation, save_return=None) with stack: with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() def test_update_serial_with_device_type(self): """Lines 1146, 1149: update_serial with force device_type → render.""" view, request, mock_existing, libre_device, validation = self._base_setup("update_serial", {"force": "on"}) validation["device_type_mismatch"] = True validation["device_type"] = {"device_type": MagicMock()} stack, MockDevice = self._setup_common(view, mock_existing, libre_device, validation, save_return=None) with stack: with patch.object(view, "render_device_row", return_value=MagicMock()) as mock_render: view.post(request, device_id=42) mock_render.assert_called_once() def test_update_type_with_device_type_save_error(self): """Line 1168: update_type with save error → return error.""" view, request, mock_existing, libre_device, validation = self._base_setup("update_type", {"force": "on"}) validation["device_type_mismatch"] = True validation["device_type"] = {"device_type": MagicMock()} from django.http import HttpResponse error_resp = HttpResponse("save error", status=400) stack, _ = self._setup_common(view, mock_existing, libre_device, validation, save_return=error_resp) with stack: response = view.post(request, device_id=42) assert response.status_code == 400 def test_sync_platform_save_error(self): """Line 1222: sync_platform → _save_device returns error.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_platform") mock_platform = MagicMock() from django.http import HttpResponse error_resp = HttpResponse("save error", status=400) stack, _ = self._setup_common(view, mock_existing, libre_device, validation, save_return=error_resp) with stack: with patch( "netbox_librenms_plugin.utils.find_matching_platform", return_value={"found": True, "platform": mock_platform}, ): response = view.post(request, device_id=42) assert response.status_code == 400 def test_sync_device_type_save_error(self): """Line 1238: sync_device_type → _save_device returns error.""" view, request, mock_existing, libre_device, validation = self._base_setup("sync_device_type") mock_dt = MagicMock() from django.http import HttpResponse error_resp = HttpResponse("save error", status=400) stack, _ = self._setup_common(view, mock_existing, libre_device, validation, save_return=error_resp) with stack: with patch( "netbox_librenms_plugin.utils.match_librenms_hardware_to_device_type", return_value={"matched": True, "device_type": mock_dt}, ): response = view.post(request, device_id=42) assert response.status_code == 400 class TestSyncSerialAction: """Tests for sync_serial action (lines 1173-1210).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def test_sync_serial_no_serial_returns_400(self): """Line 1210: sync_serial with empty serial → 400.""" view = self._make_view() request = _make_request(post={"action": "sync_serial", "existing_device_id": "1"}) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "router01", "serial": ""} validation = { "existing_device": mock_existing, "device_type_mismatch": False, } DoesNotExistExc = type("DoesNotExist", (Exception,), {}) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device") as MockDevice: MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = DoesNotExistExc with patch.object(view, "require_object_permissions", return_value=None): with patch.object( view, "get_validated_device_with_selections", return_value=(libre_device, validation, {}) ): response = view.post(request, device_id=42) assert response.status_code == 400 class TestUpdateAndSerialSaveErrors: """Tests for update/update_serial _save_device error paths (lines 1119, 1149).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _make_setup(self, action): view = self._make_view() request = _make_request(post={"action": action, "existing_device_id": "1"}) mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.name = "router01" libre_device = {"device_id": 42, "hostname": "r01", "serial": "SN001", "hardware": "Cisco", "os": "ios"} validation = {"existing_device": mock_existing, "device_type_mismatch": False} return view, request, mock_existing, libre_device, validation def _common_patches(self, view, mock_existing, libre_device, validation): from contextlib import ExitStack DoesNotExistExc = type("DoesNotExist", (Exception,), {}) MockDevice = MagicMock() MockDevice.objects.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.filter.return_value.exclude.return_value.first.return_value = None MockDevice.DoesNotExist = DoesNotExistExc mock_tx = MagicMock() mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) stack = ExitStack() stack.enter_context(patch.object(view, "require_all_permissions", return_value=None)) stack.enter_context(patch("dcim.models.Device", MockDevice)) stack.enter_context(patch.object(view, "require_object_permissions", return_value=None)) stack.enter_context( patch.object(view, "get_validated_device_with_selections", return_value=(libre_device, validation, {})) ) stack.enter_context(patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None)) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.set_librenms_device_id")) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.cache")) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key") ) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx)) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions._get_hostname_for_action", return_value="r01") ) stack.enter_context( patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42} ) ) return stack, MockDevice def test_update_save_error(self): """Line 1119: update action + _save_device error → return error.""" view, request, mock_existing, libre_device, validation = self._make_setup("update") from django.http import HttpResponse err = HttpResponse("save error", status=400) stack, _ = self._common_patches(view, mock_existing, libre_device, validation) with stack: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=err): response = view.post(request, device_id=42) assert response.status_code == 400 def test_update_serial_save_error(self): """Line 1149: update_serial + _save_device error → return error.""" view, request, mock_existing, libre_device, validation = self._make_setup("update_serial") from django.http import HttpResponse err = HttpResponse("save error", status=400) stack, _ = self._common_patches(view, mock_existing, libre_device, validation) with stack: with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=err): response = view.post(request, device_id=42) assert response.status_code == 400 class TestSyncSerialMorePaths: """Tests for sync_serial action edge cases (lines 1182-1200, 1207).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _common_patches_for_serial(self, view, mock_existing, libre_device, validation): from contextlib import ExitStack DoesNotExistExc = type("DoesNotExist", (Exception,), {}) MockDevice = MagicMock() MockDevice.objects.get.return_value = mock_existing MockDevice.DoesNotExist = DoesNotExistExc mock_tx = MagicMock() mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) stack = ExitStack() stack.enter_context(patch.object(view, "require_all_permissions", return_value=None)) stack.enter_context(patch("dcim.models.Device", MockDevice)) stack.enter_context(patch.object(view, "require_object_permissions", return_value=None)) stack.enter_context( patch.object(view, "get_validated_device_with_selections", return_value=(libre_device, validation, {})) ) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.cache")) stack.enter_context( patch("netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="k") ) stack.enter_context(patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx)) return stack, MockDevice, DoesNotExistExc def test_sync_serial_device_deleted_under_lock(self): """Lines 1182-1183: Device.DoesNotExist during select_for_update → 409.""" view = self._make_view() request = _make_request(post={"action": "sync_serial", "existing_device_id": "1"}) mock_existing = MagicMock() mock_existing.pk = 1 libre_device = {"device_id": 42, "hostname": "r01", "serial": "SN001"} validation = {"existing_device": mock_existing, "device_type_mismatch": False} stack, MockDevice, DoesNotExistExc = self._common_patches_for_serial( view, mock_existing, libre_device, validation ) with stack: MockDevice.objects.select_for_update.return_value.get.side_effect = DoesNotExistExc("gone") response = view.post(request, device_id=42) assert response.status_code == 409 def test_sync_serial_conflict_under_lock(self): """Lines 1196-1200: sync_serial serial conflict → 409.""" view = self._make_view() request = _make_request(post={"action": "sync_serial", "existing_device_id": "1"}) mock_existing = MagicMock() mock_existing.pk = 1 locked_device = MagicMock() locked_device.pk = 1 conflict_device = MagicMock() conflict_device.name = "router99" conflict_device.pk = 99 libre_device = {"device_id": 42, "hostname": "r01", "serial": "CONFLICT_SN"} validation = {"existing_device": mock_existing, "device_type_mismatch": False} stack, MockDevice, DoesNotExistExc = self._common_patches_for_serial( view, mock_existing, libre_device, validation ) with stack: MockDevice.objects.select_for_update.return_value.get.return_value = locked_device MockDevice.objects.filter.return_value.exclude.return_value.first.return_value = conflict_device response = view.post(request, device_id=42) assert response.status_code == 409 def test_sync_serial_save_error(self): """Line 1207: sync_serial → _save_device returns error.""" view = self._make_view() request = _make_request(post={"action": "sync_serial", "existing_device_id": "1"}) mock_existing = MagicMock() mock_existing.pk = 1 locked_device = MagicMock() locked_device.pk = 1 libre_device = {"device_id": 42, "hostname": "r01", "serial": "SN001"} validation = {"existing_device": mock_existing, "device_type_mismatch": False} from django.http import HttpResponse err = HttpResponse("save error", status=400) stack, MockDevice, DoesNotExistExc = self._common_patches_for_serial( view, mock_existing, libre_device, validation ) with stack: MockDevice.objects.select_for_update.return_value.get.return_value = locked_device MockDevice.objects.filter.return_value.exclude.return_value.first.return_value = None with patch("netbox_librenms_plugin.views.imports.actions._save_device", return_value=err): response = view.post(request, device_id=42) assert response.status_code == 400 class TestMigrateLibreNMSIdTransactionPaths: """Tests for migrate_librenms_id inside transaction (lines 1282-1323).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import DeviceConflictActionView view = object.__new__(DeviceConflictActionView) view._librenms_api = _make_api() view.request = MagicMock() return view def _make_valid_migrate_context(self, view, extra_mock=None): """Common setup for valid migrate_librenms_id (serial_confirmed=True).""" request = _make_request(post={"action": "migrate_librenms_id", "existing_device_id": "1"}) mock_existing = MagicMock() mock_existing.pk = 1 mock_existing.custom_field_data = {"librenms_id": 42} mock_existing.name = "router01" libre_device = {"device_id": 42, "hostname": "router01"} validation = { "existing_device": mock_existing, "device_type_mismatch": False, "librenms_id_needs_migration": True, "serial_confirmed": True, } DoesNotExistExc = type("DoesNotExist", (Exception,), {}) locked_device = MagicMock() locked_device.pk = 1 locked_device.custom_field_data = {"librenms_id": 42} locked_device.name = "router01" MockDevice = MagicMock() MockDevice.objects.get.return_value = mock_existing MockDevice.objects.select_for_update.return_value.get.return_value = locked_device MockDevice.DoesNotExist = DoesNotExistExc mock_tx = MagicMock() mock_tx.atomic.return_value.__enter__ = MagicMock(return_value=None) mock_tx.atomic.return_value.__exit__ = MagicMock(return_value=False) return request, mock_existing, libre_device, validation, locked_device, MockDevice, DoesNotExistExc, mock_tx def test_migrate_device_deleted_under_lock(self): """Lines 1285-1289: DoesNotExist during select_for_update → 409.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) MockDevice.objects.select_for_update.return_value.get.side_effect = DNE("gone") with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): response = view.post(req, device_id=42) assert response.status_code == 409 def test_migrate_already_migrated_under_lock(self): """Lines 1292-1298: cf_locked already dict under lock → 400.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) locked.custom_field_data = {"librenms_id": {"default": 42}} # Already migrated under lock with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): response = view.post(req, device_id=42) assert response.status_code == 400 def test_migrate_id_changed_under_lock(self): """Lines 1300-1303: cf_locked_int != librenms_id under lock → 400.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) locked.custom_field_data = {"librenms_id": 99} # Different ID under lock with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): response = view.post(req, device_id=42) assert response.status_code == 400 def test_migrate_id_conflict_with_other_device(self): """Lines 1309-1315: another device already has this ID → 409.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) conflict_dev = MagicMock() conflict_dev.pk = 99 # Different pk → conflict with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=conflict_dev): response = view.post(req, device_id=42) assert response.status_code == 409 def test_migrate_migration_fails(self): """Lines 1316-1320: migrate_legacy_librenms_id returns False → 400.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch( "netbox_librenms_plugin.utils.migrate_legacy_librenms_id", return_value=False ): response = view.post(req, device_id=42) assert response.status_code == 400 def test_migrate_save_error(self): """Line 1321-1322: _save_device returns error.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) from django.http import HttpResponse err = HttpResponse("save error", status=400) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch( "netbox_librenms_plugin.utils.migrate_legacy_librenms_id", return_value=True ): with patch( "netbox_librenms_plugin.views.imports.actions._save_device", return_value=err ): response = view.post(req, device_id=42) assert response.status_code == 400 def test_migrate_success_renders_row(self): """Lines 1323+: successful migration renders row.""" view = self._make_view() req, mock_ex, libre, val, locked, MockDevice, DNE, mock_tx = self._make_valid_migrate_context(view) with patch.object(view, "require_all_permissions", return_value=None): with patch("dcim.models.Device", MockDevice): with patch.object(view, "require_object_permissions", return_value=None): with patch.object(view, "get_validated_device_with_selections", return_value=(libre, val, {})): with patch("netbox_librenms_plugin.views.imports.actions.transaction", mock_tx): with patch("netbox_librenms_plugin.utils.find_by_librenms_id", return_value=None): with patch( "netbox_librenms_plugin.utils.migrate_legacy_librenms_id", return_value=True ): with patch( "netbox_librenms_plugin.views.imports.actions._save_device", return_value=None ): with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch( "netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key", ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 42}, ): with patch.object( view, "render_device_row", return_value=MagicMock() ) as mock_render: view.post(req, device_id=42) mock_render.assert_called_once() class TestBulkImportConfirmPartialExpiry: """Test partial expiry path in BulkImportConfirmView (line 422).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportConfirmView view = object.__new__(BulkImportConfirmView) view._librenms_api = _make_api() return view def test_partial_expiry_returns_400(self): """Line 422: some devices expired, some not → partial expiry 400.""" view = self._make_view() request = _make_request(post={"select": ["1", "2"]}) request.POST.getlist = MagicMock(return_value=["1", "2"]) request.GET = MagicMock() request.GET.get = MagicMock(return_value=None) call_count = [0] def fetch_side_effect(device_id, *args, **kwargs): call_count[0] += 1 if call_count[0] == 1: return {"device_id": 1, "hostname": "router01"} # Found return None # Not found (expired) validation = { "status": "importable", "resolved_name": "router01", "virtual_chassis": {}, } with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", side_effect=fetch_side_effect ): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value=validation, ): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False), ): with patch( "netbox_librenms_plugin.views.imports.actions.render", return_value=MagicMock(status_code=200), ): response = view.post(request) # 1 device found, 1 expired → partial expiry → devices=[1], seen_ids={1, 2} # cache_expired_count=1, len(seen_ids)=2 → cache_expired_count < len(seen_ids) → partial assert response is not None class TestBulkImportDevicesViewBasicPaths: """Tests for BulkImportDevicesView early paths (lines 498-763).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportDevicesView view = object.__new__(BulkImportDevicesView) view._librenms_api = _make_api() return view def test_no_devices_selected_returns_400(self): """Lines 488-490: no device IDs → 400.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=[]) with patch.object(view, "require_write_permission", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.messages"): response = view.post(request) assert response.status_code == 400 def test_invalid_device_id_returns_400(self): """Lines 492-496: non-integer device_id → 400.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=["not-an-int"]) with patch.object(view, "require_write_permission", return_value=None): with patch("netbox_librenms_plugin.views.imports.actions.messages"): response = view.post(request) assert response.status_code == 400 def test_sync_mode_import_runs(self): """Lines 498-763: synchronous import path runs without crashing.""" view = self._make_view() request = _make_request(post={"select": ["1"]}) request.POST.getlist = MagicMock(return_value=["1"]) request.user = MagicMock() request.user.is_superuser = False # Forces sync mode request.POST.get = MagicMock(return_value=None) request.headers = {} import_result = {"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0} with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value=import_result ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1, "hostname": "r01"}, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value={"status": "importable"}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.extract_device_selections", return_value={"cluster_id": None, "role_id": None, "rack_id": None}, ): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock(status_code=302), ) as mock_redirect: view.post(request) # Non-HTMX request redirects mock_redirect.assert_called() def test_background_mode_returns_job_json(self): """Background mode: should_use_background_job returns True for superuser.""" view = self._make_view() # Just test the should_use_background_job_for_import helper request = _make_request(post={"use_background_job": "on"}) request.user = MagicMock() request.user.is_superuser = True result = view.should_use_background_job_for_import(request) assert result is True def test_sync_import_uses_return_url_vc_flag(self): """VC detection flag from return_url is propagated to sync bulk import.""" view = self._make_view() request = _make_request( post={ "select": ["1"], "return_url": "/plugins/librenms_plugin/librenms-import/?enable_vc_detection=true", } ) request.POST.getlist = MagicMock(return_value=["1"]) request.user = MagicMock() request.user.is_superuser = False request.headers = {} import_result = {"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0} with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1, "hostname": "r01"}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value=import_result, ) as mock_bulk_import: with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock(status_code=302), ): view.post(request) assert mock_bulk_import.called assert mock_bulk_import.call_args.kwargs["sync_options"]["vc_detection_enabled"] is True class TestBulkImportDevicesMorePaths: """Additional paths in BulkImportDevicesView (lines 516-693, 701-758).""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportDevicesView view = object.__new__(BulkImportDevicesView) view._librenms_api = _make_api() return view def _make_base_request(self, device_ids, extra_post=None): request = _make_request(post={}) dict(extra_post or {}) request.POST.getlist = MagicMock(return_value=device_ids) request.user = MagicMock() request.user.is_superuser = False request.POST.get = MagicMock(return_value=None) request.headers = {} return request def test_invalid_cluster_value_logs_warning(self): """Lines 522-526: invalid cluster_value → warning, continue.""" view = self._make_view() request = self._make_base_request(["1"]) # cluster_1 is set to invalid value request.POST.get = MagicMock(side_effect=lambda k, d=None: "not-int" if k == "cluster_1" else None) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ) as mock_redirect: view.post(request) mock_redirect.assert_called() def test_valid_role_and_rack_values_applied(self): """Lines 531-552: valid role_id and rack_id → parsed into mappings.""" view = self._make_view() request = self._make_base_request(["1"]) # role_1=2, rack_1=3 def get_side_effect(k, d=None): if k == "role_1": return "2" if k == "rack_1": return "3" return None request.POST.get = MagicMock(side_effect=get_side_effect) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ) as mock_redirect: view.post(request) mock_redirect.assert_called() def test_invalid_role_and_rack_values_log_warning(self): """Lines 534-535, 544-546: invalid role_id/rack_id → warning.""" view = self._make_view() request = self._make_base_request(["1"]) def get_side_effect(k, d=None): if k == "role_1": return "not-int" if k == "rack_1": return "not-int" return None request.POST.get = MagicMock(side_effect=get_side_effect) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ) as mock_redirect: view.post(request) mock_redirect.assert_called() def test_import_with_success_messages(self): """Lines 683, 688, 693: success/fail/skipped messages.""" view = self._make_view() request = self._make_base_request(["1"]) request.POST.get = MagicMock(return_value=None) mock_device = MagicMock() mock_device.pk = 1 with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={ "success": [{"device_id": 1, "device": mock_device}], "failed": [{"device_id": 1, "error": "failed"}], "skipped": [{"device_id": 1}], "virtual_chassis_created": 0, }, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=None ): with patch("netbox_librenms_plugin.views.imports.actions.messages") as mock_messages: with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ): view.post(request) mock_messages.success.assert_called() mock_messages.error.assert_called() mock_messages.warning.assert_called() def test_vm_import_triggers_bulk_import_vms(self): """Line 651-668: vm_imports non-empty → bulk_import_vms called.""" view = self._make_view() request = self._make_base_request(["1"]) # cluster_1=5 → device 1 is a VM def get_side_effect(k, d=None): if k == "cluster_1": return "5" return None request.POST.get = MagicMock(side_effect=get_side_effect) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ) as mock_vm_import: with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ): view.post(request) mock_vm_import.assert_called() def test_htmx_request_returns_oob_rows(self): """Lines 701-761: HTMX request → returns OOB row HTML.""" view = self._make_view() request = self._make_base_request(["1"]) request.headers = {"HX-Request": "true"} request.POST.get = MagicMock(return_value=None) mock_device = MagicMock() mock_device.pk = 1 libre_device = {"device_id": 1, "hostname": "r01"} with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={ "success": [{"device_id": 1, "device": mock_device}], "failed": [], "skipped": [], "virtual_chassis_created": 0, }, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value=libre_device, ): with patch( "netbox_librenms_plugin.views.imports.actions.validate_device_for_import", return_value={"status": "imported"}, ): with patch("netbox_librenms_plugin.views.imports.actions.cache"): with patch( "netbox_librenms_plugin.views.imports.actions.get_import_device_cache_key", return_value="key", ): with patch( "netbox_librenms_plugin.views.imports.actions.DeviceImportTable", return_value=MagicMock(), ): with patch( "netbox_librenms_plugin.views.imports.actions.render" ) as mock_render: mock_render.return_value.content = b"row" with patch("netbox_librenms_plugin.views.imports.actions.messages"): response = view.post(request) assert response.status_code == 200 assert b"row" in response.content or response.content == b"\n".join([b"row"]) def test_permission_denied_during_import_redirects(self): """Lines 659-668: PermissionDenied during import → redirect.""" view = self._make_view() request = self._make_base_request(["1"]) request.POST.get = MagicMock(return_value=None) request.headers = {} from django.core.exceptions import PermissionDenied as DjPD with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", side_effect=DjPD("No permission"), ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ) as mock_redirect: view.post(request) mock_redirect.assert_called() def test_background_no_workers_falls_back_to_sync(self): """Line 612-615: background requested but no workers → sync fallback.""" view = self._make_view() request = self._make_base_request(["1"]) request.user.is_superuser = True request.POST.get = MagicMock(side_effect=lambda k, d=None: "on" if k == "use_background_job" else None) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ) as mock_redirect: with patch("utilities.rqworker.get_workers_for_queue", return_value=0): view.post(request) mock_redirect.assert_called() class TestBulkImportEdgePaths: """Tests for remaining BulkImportDevicesView edge paths.""" def _make_view(self): from netbox_librenms_plugin.views.imports.actions import BulkImportDevicesView view = object.__new__(BulkImportDevicesView) view._librenms_api = _make_api() return view def test_cluster_with_role_applies_role_to_vm(self): """Line 521: cluster + role for VM import.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=["1"]) request.user = MagicMock() request.user.is_superuser = False request.headers = {} def get_side_effect(k, d=None): if k == "cluster_1": return "5" if k == "role_1": return "3" return None request.POST.get = MagicMock(side_effect=get_side_effect) with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", return_value={"success": [], "failed": [], "skipped": [], "virtual_chassis_created": 0}, ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_vms", return_value={"success": [], "failed": [], "skipped": []}, ) as mock_vm: with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ): view.post(request) # VM import should have been called with role mock_vm.assert_called() def test_permission_denied_htmx_returns_htmx_redirect(self): """Line 664: PermissionDenied during import with HX-Request → HX-Redirect.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=["1"]) request.user = MagicMock() request.user.is_superuser = False request.headers = {"HX-Request": "true"} request.POST.get = MagicMock(return_value=None) from django.core.exceptions import PermissionDenied as DjPD with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch( "netbox_librenms_plugin.views.imports.actions.bulk_import_devices", side_effect=DjPD("No permission"), ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): response = view.post(request) assert response.headers.get("HX-Redirect") is not None def test_background_with_workers_enqueues_job(self): """Lines 575-611: background with workers available → enqueue job.""" view = self._make_view() request = _make_request(post={}) request.POST.getlist = MagicMock(return_value=["1"]) request.user = MagicMock() request.user.is_superuser = True request.headers = {} def get_side_effect(k, d=None): if k == "use_background_job": return "on" return None request.POST.get = MagicMock(side_effect=get_side_effect) mock_job = MagicMock() mock_job.pk = 123 mock_job.job_id = "uuid-456" with patch.object(view, "require_write_permission", return_value=None): with patch( "netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences", return_value=(True, False) ): with patch("utilities.rqworker.get_workers_for_queue", return_value=2): with patch( "netbox_librenms_plugin.views.imports.actions.fetch_device_with_cache", return_value={"device_id": 1}, ): with patch("netbox_librenms_plugin.views.imports.actions.messages"): with patch( "netbox_librenms_plugin.views.imports.actions.redirect", return_value=MagicMock() ) as mock_redirect: # Patch ImportDevicesJob at the point it's imported inside post() with patch("netbox_librenms_plugin.jobs.ImportDevicesJob") as MockJob: MockJob.enqueue.return_value = mock_job view.post(request) mock_redirect.assert_called()