"""Tests for netbox_librenms_plugin.__init__ module. Covers the _ensure_librenms_id_custom_field post_migrate signal handler. """ from unittest.mock import MagicMock, patch # ============================================================================= # TestEnsureLibreNMSIdCustomField - 6 tests # ============================================================================= class TestEnsureLibreNMSIdCustomField: """Test _ensure_librenms_id_custom_field signal handler.""" def setup_method(self): """Reset per-alias execution tracking before each test.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field _ensure_librenms_id_custom_field._executed_aliases = set() def _setup_cf_mock(self, MockCustomField, mock_cf, created): """Wire MockCustomField so that objects.using(alias).get_or_create(...) returns (mock_cf, created).""" MockCustomField.objects.using.return_value.get_or_create.return_value = (mock_cf, created) def _setup_ct_mock(self, MockContentType, mock_ct): """Wire MockContentType so that objects.db_manager(alias).get_for_model(...) returns mock_ct.""" MockContentType.objects.db_manager.return_value.get_for_model.return_value = mock_ct @patch("dcim.models.Interface", new_callable=MagicMock) @patch("dcim.models.Device", new_callable=MagicMock) @patch("virtualization.models.VMInterface", new_callable=MagicMock) @patch("virtualization.models.VirtualMachine", new_callable=MagicMock) @patch("django.contrib.contenttypes.models.ContentType") @patch("extras.models.CustomField") def test_creates_custom_field_when_missing( self, MockCustomField, MockContentType, mock_vm, mock_vmif, mock_device, mock_iface ): """Custom field is created with correct defaults when it does not exist.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field mock_cf = MagicMock() mock_cf.object_types.values_list.return_value = [] self._setup_cf_mock(MockCustomField, mock_cf, True) mock_ct = MagicMock() mock_ct.pk = 1 self._setup_ct_mock(MockContentType, mock_ct) with patch("logging.getLogger") as mock_get_logger: _ensure_librenms_id_custom_field(sender=None) MockCustomField.objects.using.return_value.get_or_create.assert_called_once_with( name="librenms_id", defaults={ "type": "json", "label": "LibreNMS ID", "description": "LibreNMS Device ID for synchronization (auto-created by plugin)", "required": False, "ui_visible": "if-set", "ui_editable": "yes", "is_cloneable": False, }, ) # Should have added content types for all 4 models assert mock_cf.object_types.add.call_count == 4 # Should log when created mock_get_logger.assert_called_with("netbox_librenms_plugin") def test_skips_when_already_executed(self): """Handler is a no-op on second invocation for the same DB alias.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field _ensure_librenms_id_custom_field._executed_aliases = {"default"} with patch("extras.models.CustomField") as MockCustomField: _ensure_librenms_id_custom_field(sender=None) MockCustomField.objects.using.assert_not_called() @patch("dcim.models.Interface", new_callable=MagicMock) @patch("dcim.models.Device", new_callable=MagicMock) @patch("virtualization.models.VMInterface", new_callable=MagicMock) @patch("virtualization.models.VirtualMachine", new_callable=MagicMock) @patch("django.contrib.contenttypes.models.ContentType") @patch("extras.models.CustomField") def test_existing_field_not_recreated( self, MockCustomField, MockContentType, mock_vm, mock_vmif, mock_device, mock_iface ): """When custom field already exists, it is not recreated but types are checked.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field mock_cf = MagicMock() mock_cf.object_types.values_list.return_value = [1, 2, 3, 4] self._setup_cf_mock(MockCustomField, mock_cf, False) mock_ct = MagicMock() mock_ct.pk = 1 self._setup_ct_mock(MockContentType, mock_ct) _ensure_librenms_id_custom_field(sender=None) # All pks already present, no types should be added mock_cf.object_types.add.assert_not_called() @patch("dcim.models.Interface", new_callable=MagicMock) @patch("dcim.models.Device", new_callable=MagicMock) @patch("virtualization.models.VMInterface", new_callable=MagicMock) @patch("virtualization.models.VirtualMachine", new_callable=MagicMock) @patch("django.contrib.contenttypes.models.ContentType") @patch("extras.models.CustomField") def test_adds_missing_content_types( self, MockCustomField, MockContentType, mock_vm, mock_vmif, mock_device, mock_iface ): """When some content types are missing, only those are added.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field mock_cf = MagicMock() mock_cf.object_types.values_list.return_value = [1, 2] self._setup_cf_mock(MockCustomField, mock_cf, False) ct_existing = MagicMock() ct_existing.pk = 1 ct_new = MagicMock() ct_new.pk = 99 MockContentType.objects.db_manager.return_value.get_for_model.side_effect = [ ct_existing, ct_existing, ct_new, ct_new, ] _ensure_librenms_id_custom_field(sender=None) assert mock_cf.object_types.add.call_count == 2 mock_cf.object_types.add.assert_any_call(ct_new) @patch("extras.models.CustomField") def test_exception_does_not_propagate(self, MockCustomField): """Exceptions during custom field creation are caught and logged.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field MockCustomField.objects.using.return_value.get_or_create.side_effect = Exception("DB not ready") with patch("logging.getLogger") as mock_get_logger: # Should not raise _ensure_librenms_id_custom_field(sender=None) # Verify the exception was logged logger_instance = mock_get_logger.return_value logger_instance.exception.assert_called_once() call_args = logger_instance.exception.call_args assert "librenms_id" in call_args[0][0] @patch("dcim.models.Interface", new_callable=MagicMock) @patch("dcim.models.Device", new_callable=MagicMock) @patch("virtualization.models.VMInterface", new_callable=MagicMock) @patch("virtualization.models.VirtualMachine", new_callable=MagicMock) @patch("django.contrib.contenttypes.models.ContentType") @patch("extras.models.CustomField") def test_no_log_when_field_already_exists( self, MockCustomField, MockContentType, mock_vm, mock_vmif, mock_device, mock_iface ): """No log message when the custom field already existed.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field mock_cf = MagicMock() mock_cf.object_types.values_list.return_value = [1, 2, 3, 4] self._setup_cf_mock(MockCustomField, mock_cf, False) mock_ct = MagicMock() mock_ct.pk = 1 self._setup_ct_mock(MockContentType, mock_ct) with patch("logging.getLogger") as mock_get_logger: _ensure_librenms_id_custom_field(sender=None) # When the field already exists (created=False), the info log should # not be emitted. We verify via the logger instance rather than # asserting getLogger was never called, which is fragile. logger_instance = mock_get_logger.return_value logger_instance.info.assert_not_called() @patch("dcim.models.Interface", new_callable=MagicMock) @patch("dcim.models.Device", new_callable=MagicMock) @patch("virtualization.models.VMInterface", new_callable=MagicMock) @patch("virtualization.models.VirtualMachine", new_callable=MagicMock) @patch("django.contrib.contenttypes.models.ContentType") @patch("extras.models.CustomField") def test_integer_field_migrated_to_json( self, MockCustomField, MockContentType, mock_vm, mock_vmif, mock_device, mock_iface ): """When existing field has type='integer', it is migrated to 'json' and saved on the given alias.""" from netbox_librenms_plugin import _ensure_librenms_id_custom_field db_alias = "other" mock_cf = MagicMock() mock_cf.type = "integer" mock_cf.object_types.values_list.return_value = [1, 2, 3, 4] self._setup_cf_mock(MockCustomField, mock_cf, False) mock_ct = MagicMock() mock_ct.pk = 1 self._setup_ct_mock(MockContentType, mock_ct) _ensure_librenms_id_custom_field(sender=None, using=db_alias) assert mock_cf.type == "json" # Alias must flow through both the CustomField lookup and the ContentType lookup, # exercising the per-alias guard path (_executed_aliases). MockCustomField.objects.using.assert_called_with(db_alias) MockContentType.objects.db_manager.assert_called_with(db_alias) mock_cf.save.assert_called_once_with(using=db_alias, update_fields=["type"]) assert db_alias in _ensure_librenms_id_custom_field._executed_aliases