From a93aae12fae0170222a74f072e85fd7416ce410d Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Thu, 9 Apr 2026 16:33:55 +0200 Subject: [PATCH] Closes #21862: Stabilize ScriptModule tests and reduce CI noise (#21867) --- 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):