diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py index f4430dad5..94fa98fd8 100644 --- a/netbox/extras/api/serializers_/scripts.py +++ b/netbox/extras/api/serializers_/scripts.py @@ -44,15 +44,20 @@ class ScriptModuleSerializer(ValidatedModelSerializer): if upload_file is not None: data['upload_file'] = upload_file - if upload_file and data.get('data_file'): + # For multipart requests, nested serializer fields (data_source, data_file) are + # silently dropped by DRF's HTML parser, so also check initial_data for raw values. + has_data_file = data.get('data_file') or self.initial_data.get('data_file') + has_data_source = data.get('data_source') or self.initial_data.get('data_source') + + if upload_file and has_data_file: raise serializers.ValidationError( _("Cannot upload a file and sync from an existing data file.") ) - if upload_file and data.get('data_source'): + if upload_file and has_data_source: raise serializers.ValidationError( _("Cannot upload a file and sync from a data source.") ) - if self.instance is None and not upload_file and not data.get('data_file') and not data.get('data_source'): + if self.instance is None and not upload_file and not has_data_file and not has_data_source: raise serializers.ValidationError( _("Must upload a file or provide a data source or data file to sync.") ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 7e4c49c77..86e2bfb65 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1426,6 +1426,21 @@ class ScriptUploadTest(APITestCase): mock_storage.save.assert_called_once() self.assertTrue(ScriptModule.objects.filter(file_path='test_upload.py').exists()) + def test_upload_with_data_source_fails(self): + """Supplying both upload_file and data_source must be rejected.""" + self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile') + script_content = b"from extras.scripts import Script\nclass TestScript(Script):\n pass\n" + upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain') + # data_source is intentionally a raw value to exercise the multipart path where DRF's + # nested-serializer HTML parser drops the field; validation must still catch the conflict. + response = self.client.post( + self.url_list, + {'upload_file': upload_file, 'data_source': 1}, + format='multipart', + **self.header, + ) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + def test_upload_script_module_without_file_fails(self): self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile') response = self.client.post(self.url_list, {}, format='json', **self.header)