Browse Source

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
Martin Hauser 16 hours ago
parent
commit
f201a54949
2 changed files with 40 additions and 9 deletions
  1. 24 7
      netbox/extras/tests/test_api.py
  2. 16 2
      netbox/extras/tests/test_views.py

+ 24 - 7
netbox/extras/tests/test_api.py

@@ -1,5 +1,6 @@
 import datetime
 import datetime
 import hashlib
 import hashlib
+import io
 from unittest.mock import MagicMock, patch
 from unittest.mock import MagicMock, patch
 
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
@@ -1013,10 +1014,14 @@ class ScriptTest(APITestCase):
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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(
         script = Script.objects.create(
             module=module,
             module=module,
             name='Test script',
             name='Test script',
@@ -1419,9 +1424,20 @@ class ScriptModuleTest(APITestCase):
         upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain')
         upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain')
         mock_storage = MagicMock()
         mock_storage = MagicMock()
         mock_storage.save.return_value = 'test_upload.py'
         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(
             response = self.client.post(
                 self.url,
                 self.url,
                 {'file': upload_file},
                 {'file': upload_file},
@@ -1432,6 +1448,7 @@ class ScriptModuleTest(APITestCase):
         self.assertEqual(response.data['file_path'], 'test_upload.py')
         self.assertEqual(response.data['file_path'], 'test_upload.py')
         mock_storage.save.assert_called_once()
         mock_storage.save.assert_called_once()
         self.assertTrue(ScriptModule.objects.filter(file_path='test_upload.py').exists())
         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):
     def test_upload_script_module_without_file_fails(self):
         self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile')
         self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile')

+ 16 - 2
netbox/extras/tests/test_views.py

@@ -924,7 +924,14 @@ class ScriptValidationErrorTest(TestCase):
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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)
         cls.script = Script.objects.create(module=module, name='Test script', is_executable=True)
 
 
     def setUp(self):
     def setUp(self):
@@ -986,7 +993,14 @@ class ScriptDefaultValuesTest(TestCase):
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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)
         cls.script = Script.objects.create(module=module, name='Test script', is_executable=True)
 
 
     def setUp(self):
     def setUp(self):