""" Tests for views/imports/list.py — targeting ≥95% coverage. Tests the LibreNMSImportView: get_required_permission, should_use_background_job, _load_job_results, get, get_queryset, get_table, and _get_import_queryset. Conventions: - Plain pytest classes (no Django TestCase) - No @pytest.mark.django_db — all DB interactions mocked - Inline imports inside test methods - object.__new__(ViewClass) for instantiation - MagicMock for all external dependencies """ from unittest.mock import MagicMock, patch class TestGetRequiredPermission: """Tests for get_required_permission().""" def test_returns_device_view_permission(self): """get_required_permission returns the 'view' permission for Device model.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) with patch("netbox_librenms_plugin.views.imports.list.Device") as mock_device: with patch("utilities.permissions.get_permission_for_model") as mock_perm: mock_perm.return_value = "dcim.view_device" result = view.get_required_permission() assert result == "dcim.view_device" mock_perm.assert_called_once_with(mock_device, "view") class TestShouldUseBackgroundJob: """Tests for should_use_background_job().""" def test_non_superuser_returns_false(self): """Non-superusers always get synchronous mode.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._filter_form_data = {"use_background_job": True} view.request = MagicMock() view.request.user.is_superuser = False assert view.should_use_background_job() is False def test_superuser_use_background_true(self): """Superuser with use_background_job=True returns True.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._filter_form_data = {"use_background_job": True} view.request = MagicMock() view.request.user.is_superuser = True assert view.should_use_background_job() is True def test_superuser_use_background_false(self): """Superuser with use_background_job=False returns False.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._filter_form_data = {"use_background_job": False} view.request = MagicMock() view.request.user.is_superuser = True assert view.should_use_background_job() is False def test_superuser_field_missing_defaults_true(self): """When use_background_job key absent, default is True for superusers.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._filter_form_data = {} view.request = MagicMock() view.request.user.is_superuser = True assert view.should_use_background_job() is True class TestLoadJobResults: """Tests for _load_job_results().""" def test_job_not_found_returns_empty(self): """Returns [] when job doesn't exist (DoesNotExist path lines 79-81).""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) class MockDoesNotExist(Exception): pass with patch("netbox_librenms_plugin.views.imports.list.logger") as mock_logger: with patch("core.models.Job") as mock_job_cls: mock_job_cls.DoesNotExist = MockDoesNotExist mock_job_cls.objects.get.side_effect = MockDoesNotExist("not found") result = view._load_job_results(999) assert result == [] mock_logger.warning.assert_called() def test_job_not_completed_returns_empty(self): """Returns [] when job status is not 'completed'.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) with patch("netbox_librenms_plugin.views.imports.list.logger"): mock_job = MagicMock() mock_job.status = "running" with patch("core.models.Job") as mock_job_cls: mock_job_cls.objects.get.return_value = mock_job result = view._load_job_results(42) assert result == [] def test_empty_device_ids_returns_empty(self): """Returns [] when job data has no device_ids.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) with patch("netbox_librenms_plugin.views.imports.list.logger"): mock_job = MagicMock() mock_job.status = "completed" mock_job.data = {"device_ids": [], "filters": {}, "server_key": "default"} with patch("core.models.Job") as mock_job_cls: mock_job_cls.objects.get.return_value = mock_job result = view._load_job_results(42) assert result == [] def test_devices_loaded_from_cache(self): """Returns validated devices found in cache for each device_id.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) mock_device_a = {"device_id": 1, "hostname": "router1"} mock_device_b = {"device_id": 2, "hostname": "router2"} with patch("netbox_librenms_plugin.views.imports.list.logger"): mock_job = MagicMock() mock_job.status = "completed" mock_job.data = { "device_ids": [1, 2], "filters": {}, "server_key": "default", "vc_detection_enabled": False, "use_sysname": True, "strip_domain": False, "cached_at": "2024-01-01T00:00:00", "cache_timeout": 300, } with patch("core.models.Job") as mock_job_cls: mock_job_cls.objects.get.return_value = mock_job with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: with patch("netbox_librenms_plugin.import_utils.get_validated_device_cache_key") as mock_key: mock_key.side_effect = lambda **kwargs: f"key_{kwargs['device_id']}" mock_cache.get.side_effect = lambda key: mock_device_a if key == "key_1" else mock_device_b result = view._load_job_results(42) assert len(result) == 2 assert mock_device_a in result assert mock_device_b in result def test_load_job_results_sets_vc_detection_enabled_from_job_data(self): """vc_detection_enabled from job data is preserved on the view instance.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) with patch("netbox_librenms_plugin.views.imports.list.logger"): mock_job = MagicMock() mock_job.status = "completed" mock_job.data = { "device_ids": [1], "filters": {}, "server_key": "default", "vc_detection_enabled": True, "use_sysname": True, "strip_domain": False, } with patch("core.models.Job") as mock_job_cls: mock_job_cls.objects.get.return_value = mock_job with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: with patch("netbox_librenms_plugin.import_utils.get_validated_device_cache_key") as mock_key: mock_key.return_value = "key_1" mock_cache.get.return_value = {"device_id": 1} result = view._load_job_results(42) assert len(result) == 1 assert view._vc_detection_enabled is True def test_cache_miss_skips_device(self): """Devices missing from cache are silently skipped.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) mock_device = {"device_id": 1, "hostname": "router1"} with patch("netbox_librenms_plugin.views.imports.list.logger"): mock_job = MagicMock() mock_job.status = "completed" mock_job.data = { "device_ids": [1, 2], "filters": {}, "server_key": "default", "vc_detection_enabled": False, "use_sysname": True, "strip_domain": False, } with patch("core.models.Job") as mock_job_cls: mock_job_cls.objects.get.return_value = mock_job with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: with patch("netbox_librenms_plugin.import_utils.get_validated_device_cache_key") as mock_key: mock_key.side_effect = lambda **kwargs: f"key_{kwargs['device_id']}" # device_id=2 is missing from cache (returns None) mock_cache.get.side_effect = lambda key: mock_device if key == "key_1" else None result = view._load_job_results(42) assert len(result) == 1 assert result[0] == mock_device def test_all_cache_expired_logs_error(self): """When all devices missing from cache, logs error and returns [].""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) with patch("netbox_librenms_plugin.views.imports.list.logger") as mock_logger: mock_job = MagicMock() mock_job.status = "completed" mock_job.data = { "device_ids": [1, 2], "filters": {}, "server_key": "default", "vc_detection_enabled": False, "use_sysname": True, "strip_domain": False, } with patch("core.models.Job") as mock_job_cls: mock_job_cls.objects.get.return_value = mock_job with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: with patch("netbox_librenms_plugin.import_utils.get_validated_device_cache_key") as mock_key: mock_key.return_value = "some_key" mock_cache.get.return_value = None result = view._load_job_results(42) assert result == [] mock_logger.error.assert_called_once() class TestGetTable: """Tests for get_table().""" def test_returns_table_with_import_data(self): """get_table returns a DeviceImportTable populated from _import_data.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._import_data = [{"device_id": 1, "hostname": "router1"}] request = MagicMock() request.GET.get.return_value = None with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable") as mock_table_cls: mock_table = MagicMock() mock_table_cls.return_value = mock_table result = view.get_table([], request, bulk_actions=True) assert result is mock_table mock_table_cls.assert_called_once_with( view._import_data, order_by=None, ) def test_get_table_loads_import_data_when_missing(self): """When _import_data is absent, get_table calls _get_import_queryset.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) # No _import_data set view._job_results_loaded = False view._filters_submitted = False request = MagicMock() request.GET.get.return_value = None with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable") as mock_table_cls: mock_table_cls.return_value = MagicMock() view.get_table([], request, bulk_actions=True) assert hasattr(view, "_import_data") class TestGetQueryset: """Tests for get_queryset().""" def test_returns_empty_device_queryset(self): """get_queryset always returns Device.objects.none().""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = False view._filters_submitted = False request = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.Device") as mock_device: mock_device.objects.none.return_value = [] result = view.get_queryset(request) assert result == [] mock_device.objects.none.assert_called_once() def test_sets_import_data(self): """get_queryset sets _import_data via _get_import_queryset.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = False view._filters_submitted = False request = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.Device") as mock_device: mock_device.objects.none.return_value = [] view.get_queryset(request) assert hasattr(view, "_import_data") assert view._import_data == [] class TestGetImportQueryset: """Tests for _get_import_queryset().""" def test_job_results_loaded_returns_existing(self): """When _job_results_loaded is True, returns existing _import_data.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = True view._import_data = [{"device_id": 1}] result = view._get_import_queryset() assert result == [{"device_id": 1}] def test_filters_not_submitted_returns_empty(self): """When no filters submitted, returns empty list.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = False view._filters_submitted = False result = view._get_import_queryset() assert result == [] def test_filter_warning_returns_empty(self): """When _filter_warning is set, returns empty list.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = False view._filters_submitted = True view._filter_warning = "Some warning" result = view._get_import_queryset() assert result == [] def test_calls_process_device_filters(self): """When filters submitted and valid, calls process_device_filters.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = False view._filters_submitted = True view._filter_warning = None view._filter_form_data = { "librenms_location": "DC1", "enable_vc_detection": False, "clear_cache": False, "show_disabled": False, "exclude_existing": False, } view._vc_detection_enabled = False view._cache_cleared = False view._use_sysname = True view._strip_domain = False mock_request = MagicMock() view._request = mock_request mock_api = MagicMock() mock_api.server_key = "default" view._librenms_api = mock_api with patch("netbox_librenms_plugin.views.imports.list.process_device_filters") as mock_process: mock_process.return_value = ([{"device_id": 1}], False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings_cls: mock_settings_cls.objects.first.return_value = None with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None result = view._get_import_queryset() mock_process.assert_called_once() assert result == [{"device_id": 1}] class TestGetView: """Tests for the get() method of LibreNMSImportView.""" def _make_view_with_request(self, superuser=True, query_params=None): """Helper to set up a view instance with a mock request.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) request = MagicMock() request.user.is_superuser = superuser request.user.username = "testuser" params = query_params or {} request.GET.get = lambda key, default=None: params.get(key, default) request.GET.__contains__ = lambda self, key: key in params view.request = request return view, request def test_get_job_id_loads_results(self): """When job_id is in GET params, _load_job_results is called.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"job_id": "42"}) mock_devices = [{"device_id": 1, "hostname": "router1"}] mock_api = MagicMock() mock_api.server_key = "default" with patch.object(view, "_load_job_results", return_value=mock_devices) as mock_load: with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) mock_load.assert_called_once_with(42) def test_get_job_id_preserves_vc_flag_when_query_flag_missing(self): """job_id pages keep vc_detection_enabled from loaded job results.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"job_id": "42"}) mock_api = MagicMock() mock_api.server_key = "default" def _load_job_side_effect(_job_id): view._vc_detection_enabled = True return [{"device_id": 1, "hostname": "router1"}] with patch.object(view, "_load_job_results", side_effect=_load_job_side_effect) as mock_load: with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref", return_value=None): mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches", return_value=[], ): with patch.object(view, "get_server_info", return_value={}): view.get(request) mock_load.assert_called_once_with(42) context = mock_render.call_args[0][2] assert context["vc_detection_enabled"] is True def test_get_invalid_job_id_logs_warning(self): """Invalid (non-integer) job_id is caught and logged.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"job_id": "not-an-int"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch("netbox_librenms_plugin.views.imports.list.logger") as mock_logger: view.get(request) mock_logger.warning.assert_called() def test_get_no_job_id_renders_template(self): """Normal GET without job_id renders the import template.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request() mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) mock_render.assert_called_once() # Verify called with correct template call_args = mock_render.call_args assert "librenms_import.html" in call_args[0][1] def test_get_settings_exception_falls_back_to_none(self): """LibreNMSSettings exception during GET is caught and settings set to None.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request() mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.side_effect = Exception("DB error") mock_settings.objects.get_or_create.side_effect = Exception("DB error") with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): # Should not raise view.get(request) mock_render.assert_called_once() ctx = mock_render.call_args[0][2] assert ctx["settings"] is None def test_get_filters_submitted_with_valid_form(self): """When filters are submitted and form is valid, processes filters.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"apply_filters": "1", "librenms_location": "DC1"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": False, } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch( "netbox_librenms_plugin.import_utils.get_cache_metadata_key" ) as mock_meta_key: mock_meta_key.return_value = "meta_key" with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.return_value = 5 with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None with patch.object(view, "_get_import_queryset", return_value=[]): view.get(request) mock_render.assert_called_once() def test_get_background_job_enqueued_for_superuser(self): """Superuser with workers available triggers background job enqueue.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request( superuser=True, query_params={"apply_filters": "1", "librenms_location": "DC1"}, ) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": True, # Background mode } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.get_workers_for_queue") as mock_workers: mock_workers.return_value = 1 # Workers available with patch("netbox_librenms_plugin.import_utils.get_cache_metadata_key") as mock_meta: mock_meta.return_value = "meta_key" with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None # No cached results with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.return_value = 10 with patch("netbox_librenms_plugin.jobs.FilterDevicesJob") as mock_job_cls: mock_job = MagicMock() mock_job.pk = 123 mock_job.job_id = "uuid-123" mock_job_cls.enqueue.return_value = mock_job import json from django.http import JsonResponse result = view.get(request) assert isinstance(result, JsonResponse) data = json.loads(result.content) assert "job_pk" in data assert "poll_url" in data def test_get_no_workers_falls_back_to_sync(self): """With no RQ workers, falls back to synchronous processing.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request( superuser=True, query_params={"apply_filters": "1", "librenms_location": "DC1"}, ) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": True, } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.get_workers_for_queue") as mock_workers: mock_workers.return_value = 0 # No workers with patch("netbox_librenms_plugin.import_utils.get_cache_metadata_key") as mock_meta: mock_meta.return_value = "meta_key" with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.return_value = 5 with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch( "netbox_librenms_plugin.views.imports.list.messages" ) as mock_messages: with patch.object( view, "_get_import_queryset", return_value=[] ): view.get(request) # Should render page (synchronous fallback) mock_render.assert_called_once() mock_messages.warning.assert_called_once() def test_get_job_results_expired_shows_warning(self): """When job results are empty, shows warning message to user.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"job_id": "42"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch.object(view, "_load_job_results", return_value=[]): with patch("netbox_librenms_plugin.views.imports.list.messages") as mock_messages: with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) mock_messages.warning.assert_called_once() def test_get_legacy_skip_vc_detection_flag(self): """Legacy skip_vc_detection=true sets vc_detection_enabled=False.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"skip_vc_detection": "true"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) assert view._vc_detection_enabled is False def test_get_enable_vc_detection_flag(self): """enable_vc_detection=1 sets vc_detection_enabled=True.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"enable_vc_detection": "1"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) assert view._vc_detection_enabled is True def test_get_context_includes_can_use_background_jobs(self): """Rendered context includes can_use_background_jobs keyed on is_superuser.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(superuser=True) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) ctx = mock_render.call_args[0][2] assert ctx["can_use_background_jobs"] is True def test_get_form_non_field_errors_set_warning(self): """Non-field form validation errors are stored as _filter_warning.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view_with_request(query_params={"apply_filters": "1"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None expected_warning = "At least one filter is required." mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = False mock_form.non_field_errors.return_value = [expected_warning] mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): view.get(request) assert view._filter_warning == expected_warning class TestGetViewFilterFields: """Tests that exercise individual filter field extraction paths in get().""" def _make_view(self, query_params=None): """Helper to create a view with a mock request containing specific params.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) request = MagicMock() request.user.is_superuser = False request.user.username = "testuser" params = query_params or {} request.GET.get = lambda key, default=None: params.get(key, default) request.GET.__contains__ = lambda self, key: key in params view.request = request return view, request def test_get_all_filter_fields_extracted(self): """All six filter fields are extracted into libre_filters for background jobs.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view( query_params={ "apply_filters": "1", "librenms_location": "DC1", "librenms_type": "network", "librenms_os": "ios", "librenms_hostname": "router", "librenms_sysname": "sw1", "librenms_hardware": "Cisco C9300", } ) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": False, } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch( "netbox_librenms_plugin.import_utils.get_cache_metadata_key" ) as mock_meta: mock_meta.return_value = "meta_key" with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.return_value = 5 with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None with patch.object(view, "_get_import_queryset", return_value=[]): view.get(request) # All filters were submitted — filter count passed to device count mock_count.assert_called_once() args, kwargs = mock_count.call_args filters = ( kwargs.get("filters") if kwargs.get("filters") is not None else (args[0] if args else None) ) assert filters.get("location") == "DC1" assert filters.get("type") == "network" assert filters.get("os") == "ios" assert filters.get("hostname") == "router" assert filters.get("sysname") == "sw1" assert filters.get("hardware") == "Cisco C9300" def test_get_settings_exception_is_caught(self): """LibreNMSSettings exception at the top of get() is caught and settings set to None.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view(query_params={"apply_filters": "1", "librenms_location": "DC1"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.side_effect = Exception("DB error") mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": False, } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch( "netbox_librenms_plugin.import_utils.get_cache_metadata_key" ) as mock_meta: mock_meta.return_value = "meta_key" with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.return_value = 3 with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None # Should not raise despite the settings exception with patch.object(view, "_get_import_queryset", return_value=[]): view.get(request) mock_render.assert_called_once() def test_get_device_count_exception_defaults_zero(self): """Device count exception falls back to 0 (lines 304-306).""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view(query_params={"apply_filters": "1", "librenms_location": "DC1"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": False, } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch( "netbox_librenms_plugin.import_utils.get_cache_metadata_key" ) as mock_meta: mock_meta.return_value = "meta_key" with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.side_effect = Exception("API error") with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None with patch( "netbox_librenms_plugin.views.imports.list.logger" ) as mock_logger: with patch.object(view, "_get_import_queryset", return_value=[]): view.get(request) mock_render.assert_called_once() mock_logger.error.assert_called() context = mock_render.call_args[0][2] assert context["device_count"] == 0 def test_get_cache_check_exception_continues(self): """Cache check exception is logged and processing continues (lines 293-294).""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view, request = self._make_view(query_params={"apply_filters": "1", "librenms_location": "DC1"}) mock_api = MagicMock() mock_api.server_key = "default" with patch.object(LibreNMSImportView, "librenms_api", new_callable=lambda: property(lambda self: mock_api)): with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None mock_settings.objects.get_or_create.return_value = (None, False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None mock_form_cls = MagicMock() mock_form = MagicMock() mock_form.is_valid.return_value = True mock_form.cleaned_data = { "enable_vc_detection": False, "clear_cache": False, "use_background_job": False, } mock_form_cls.return_value = mock_form view.filterset_form = mock_form_cls with patch("netbox_librenms_plugin.views.imports.list.render") as mock_render: mock_render.return_value = MagicMock() with patch("netbox_librenms_plugin.views.imports.list.DeviceImportTable"): with patch( "netbox_librenms_plugin.views.imports.list.get_active_cached_searches" ) as mock_searches: mock_searches.return_value = [] with patch.object(view, "get_server_info", return_value={}): with patch( "netbox_librenms_plugin.import_utils.get_cache_metadata_key" ) as mock_meta: # Cache check raises exception mock_meta.side_effect = Exception("cache error") with patch( "netbox_librenms_plugin.import_utils.get_device_count_for_filters" ) as mock_count: mock_count.return_value = 5 with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None # Should not raise with patch.object(view, "_get_import_queryset", return_value=[]): view.get(request) mock_render.assert_called_once() class TestGetImportQuerysetFilterFields: """Tests for individual filter field branches in _get_import_queryset().""" def _make_view(self, filter_data=None): """Helper to create a configured view instance.""" from netbox_librenms_plugin.views.imports.list import LibreNMSImportView view = object.__new__(LibreNMSImportView) view._job_results_loaded = False view._filters_submitted = True view._filter_warning = None view._filter_form_data = filter_data or {} view._vc_detection_enabled = False view._cache_cleared = False view._request = MagicMock() mock_api = MagicMock() mock_api.server_key = "default" view._librenms_api = mock_api view._use_sysname = True view._strip_domain = False return view def test_all_filter_fields_passed_to_process(self): """All 6 filter fields present in filter_data are forwarded to process_device_filters.""" view = self._make_view( filter_data={ "librenms_location": "DC1", "librenms_type": "network", "librenms_os": "ios", "librenms_hostname": "router01", "librenms_sysname": "sw1", "librenms_hardware": "Cisco", "enable_vc_detection": False, "clear_cache": False, "show_disabled": False, "exclude_existing": False, } ) with patch("netbox_librenms_plugin.views.imports.list.process_device_filters") as mock_process: mock_process.return_value = ([], False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None view._get_import_queryset() call_kwargs = mock_process.call_args[1] filters = call_kwargs["filters"] assert filters["location"] == "DC1" assert filters["type"] == "network" assert filters["os"] == "ios" assert filters["hostname"] == "router01" assert filters["sysname"] == "sw1" assert filters["hardware"] == "Cisco" def test_get_import_queryset_returns_empty_on_no_results(self): """_get_import_queryset returns [] when process_device_filters returns empty.""" view = self._make_view( filter_data={ "librenms_location": "DC1", "enable_vc_detection": False, "clear_cache": False, } ) with patch("netbox_librenms_plugin.views.imports.list.process_device_filters") as mock_process: mock_process.return_value = ([], False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = None result = view._get_import_queryset() assert result == [] def test_cache_metadata_found_sets_timestamps(self): """When cache metadata is found, timestamps are set (lines 523-527).""" mock_device = {"device_id": 1, "_validation": {}} view = self._make_view( filter_data={ "librenms_location": "DC1", "enable_vc_detection": True, "clear_cache": False, } ) with patch("netbox_librenms_plugin.views.imports.list.process_device_filters") as mock_process: mock_process.return_value = ([mock_device], False) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: mock_cache.get.return_value = { "cached_at": "2024-01-01T00:00:00", "cache_timeout": 600, } with patch("netbox_librenms_plugin.import_utils.get_cache_metadata_key") as mock_meta_key: mock_meta_key.return_value = "meta_key" result = view._get_import_queryset() assert len(result) == 1 assert view._cache_timestamp == "2024-01-01T00:00:00" assert view._cache_timeout == 600 # _vc_detection_enabled is propagated to device validation assert result[0]["_validation"]["_vc_detection_enabled"] is True def test_cache_metadata_missing_sets_flag(self): """When cache metadata is absent, _cache_metadata_missing is set True.""" mock_device = {"device_id": 1, "_validation": {}} view = self._make_view( filter_data={ "librenms_location": "DC1", "enable_vc_detection": False, "clear_cache": False, } ) with patch("netbox_librenms_plugin.views.imports.list.process_device_filters") as mock_process: mock_process.return_value = ([mock_device], True) with patch("netbox_librenms_plugin.views.imports.list.get_user_pref") as mock_pref: mock_pref.return_value = None with patch("netbox_librenms_plugin.views.imports.list.LibreNMSSettings") as mock_settings: mock_settings.objects.first.return_value = None with patch("netbox_librenms_plugin.views.imports.list.cache") as mock_cache: # Cache metadata not found mock_cache.get.return_value = None with patch("netbox_librenms_plugin.import_utils.get_cache_metadata_key") as mock_meta_key: mock_meta_key.return_value = "meta_key" view._get_import_queryset() assert view._cache_metadata_missing is True