From f201a549490bf776f8c760bca0ebc3e212990efd Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Wed, 8 Apr 2026 22:31:14 +0200 Subject: [PATCH] test(extras): Stabilize ScriptModule tests and reduce CI noise Patch `ScriptModule.sync_classes()` in tests that create `ScriptModule` records without backing files to avoid setup-time import attempts for non-existent modules. For the upload API test, mock both serializer and module storage access so the uploaded script can be saved and imported during the test, and assert that the corresponding `Script` object is created. Fixes #21862 --- netbox/extras/tests/test_api.py | 31 ++++++++++++++++++++++++------- netbox/extras/tests/test_views.py | 18 ++++++++++++++++-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index c85433982..8a05fdc27 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1,5 +1,6 @@ import datetime import hashlib +import io from unittest.mock import MagicMock, patch from django.contrib.contenttypes.models import ContentType @@ -1013,10 +1014,14 @@ class ScriptTest(APITestCase): @classmethod def setUpTestData(cls): - module = ScriptModule.objects.create( - file_root=ManagedFileRootPathChoices.SCRIPTS, - file_path='script.py', - ) + # Avoid trying to import a non-existent on-disk module during setup. + # This test creates the Script row explicitly and monkey-patches + # Script.python_class below. + with patch.object(ScriptModule, 'sync_classes'): + module = ScriptModule.objects.create( + file_root=ManagedFileRootPathChoices.SCRIPTS, + file_path='script.py', + ) script = Script.objects.create( module=module, name='Test script', @@ -1419,9 +1424,20 @@ class ScriptModuleTest(APITestCase): upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain') mock_storage = MagicMock() mock_storage.save.return_value = 'test_upload.py' - with patch('extras.api.serializers_.scripts.storages') as mock_storages: - mock_storages.create_storage.return_value = mock_storage - mock_storages.backends = {'scripts': {}} + + # The upload serializer writes the file via storages.create_storage(...).save(), + # but ScriptModule.sync_classes() later imports it via storages["scripts"].open(). + # Provide both behaviors so the uploaded module can actually be loaded during the test. + mock_storage.open.side_effect = lambda *args, **kwargs: io.BytesIO(script_content) + + with ( + patch('extras.api.serializers_.scripts.storages') as mock_serializer_storages, + patch('extras.models.mixins.storages') as mock_module_storages, + ): + mock_serializer_storages.create_storage.return_value = mock_storage + mock_serializer_storages.backends = {'scripts': {}} + mock_module_storages.__getitem__.return_value = mock_storage + response = self.client.post( self.url, {'file': upload_file}, @@ -1432,6 +1448,7 @@ class ScriptModuleTest(APITestCase): self.assertEqual(response.data['file_path'], 'test_upload.py') mock_storage.save.assert_called_once() self.assertTrue(ScriptModule.objects.filter(file_path='test_upload.py').exists()) + self.assertTrue(Script.objects.filter(module__file_path='test_upload.py', name='TestScript').exists()) def test_upload_script_module_without_file_fails(self): self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile') diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 44cce289c..8d28f4602 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -924,7 +924,14 @@ class ScriptValidationErrorTest(TestCase): @classmethod def setUpTestData(cls): - module = ScriptModule.objects.create(file_root=ManagedFileRootPathChoices.SCRIPTS, file_path='test_script.py') + # Avoid trying to import a non-existent on-disk module during setup. + # This test creates the Script row explicitly and monkey-patches + # Script.python_class below. + with patch.object(ScriptModule, 'sync_classes'): + module = ScriptModule.objects.create( + file_root=ManagedFileRootPathChoices.SCRIPTS, + file_path='test_script.py', + ) cls.script = Script.objects.create(module=module, name='Test script', is_executable=True) def setUp(self): @@ -986,7 +993,14 @@ class ScriptDefaultValuesTest(TestCase): @classmethod def setUpTestData(cls): - module = ScriptModule.objects.create(file_root=ManagedFileRootPathChoices.SCRIPTS, file_path='test_script.py') + # Avoid trying to import a non-existent on-disk module during setup. + # This test creates the Script row explicitly and monkey-patches + # Script.python_class below. + with patch.object(ScriptModule, 'sync_classes'): + module = ScriptModule.objects.create( + file_root=ManagedFileRootPathChoices.SCRIPTS, + file_path='test_script.py', + ) cls.script = Script.objects.create(module=module, name='Test script', is_executable=True) def setUp(self):