mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-31 22:53:21 +02:00
Validate upload_file + data_source conflict for multipart requests
DRF 3.16 Serializer.get_value() uses parse_html_dict() or empty for all
HTML/multipart input. A flat key like data_source=2 produces an empty
dict ({}), which is falsy, so it falls back to empty and the nested
field is silently skipped. data.get('data_source') is therefore always
None in multipart requests, bypassing the conflict check.
Fix: also check self.initial_data for data_source and data_file in all
three guards in validate(), so the raw submitted value is detected even
when DRF's HTML parser drops the deserialized object.
Add test_upload_with_data_source_fails to cover the multipart conflict
path explicitly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,15 +44,20 @@ class ScriptModuleSerializer(ValidatedModelSerializer):
|
|||||||
if upload_file is not None:
|
if upload_file is not None:
|
||||||
data['upload_file'] = upload_file
|
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(
|
raise serializers.ValidationError(
|
||||||
_("Cannot upload a file and sync from an existing data file.")
|
_("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(
|
raise serializers.ValidationError(
|
||||||
_("Cannot upload a file and sync from a data source.")
|
_("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(
|
raise serializers.ValidationError(
|
||||||
_("Must upload a file or provide a data source or data file to sync.")
|
_("Must upload a file or provide a data source or data file to sync.")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1426,6 +1426,21 @@ class ScriptUploadTest(APITestCase):
|
|||||||
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())
|
||||||
|
|
||||||
|
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):
|
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')
|
||||||
response = self.client.post(self.url_list, {}, format='json', **self.header)
|
response = self.client.post(self.url_list, {}, format='json', **self.header)
|
||||||
|
|||||||
Reference in New Issue
Block a user