mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-14 05:37:59 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5c3a9cfe0 |
@@ -2037,10 +2037,10 @@ class BulkEditSerializer(
|
||||
raise serializers.ValidationError("remove_custom_fields not specified")
|
||||
|
||||
def _validate_owner(self, owner):
|
||||
try:
|
||||
return User.objects.get(pk=owner)
|
||||
except (User.DoesNotExist, TypeError, ValueError):
|
||||
ownerUser = User.objects.get(pk=owner)
|
||||
if ownerUser is None:
|
||||
raise serializers.ValidationError("Specified owner cannot be found")
|
||||
return ownerUser
|
||||
|
||||
def _validate_parameters_set_permissions(self, parameters) -> None:
|
||||
if "set_permissions" not in parameters:
|
||||
@@ -2060,7 +2060,7 @@ class BulkEditSerializer(
|
||||
or not float(parameters["degrees"]).is_integer()
|
||||
):
|
||||
raise serializers.ValidationError("invalid rotation degrees")
|
||||
except (TypeError, ValueError):
|
||||
except ValueError:
|
||||
raise serializers.ValidationError("invalid rotation degrees")
|
||||
|
||||
def _validate_source_mode(self, parameters) -> None:
|
||||
@@ -2073,8 +2073,6 @@ class BulkEditSerializer(
|
||||
def _validate_parameters_split(self, parameters) -> None:
|
||||
if "pages" not in parameters:
|
||||
raise serializers.ValidationError("pages not specified")
|
||||
if not isinstance(parameters["pages"], str):
|
||||
raise serializers.ValidationError("invalid pages specified")
|
||||
try:
|
||||
pages = []
|
||||
docs = parameters["pages"].split(",")
|
||||
|
||||
@@ -1189,18 +1189,13 @@ def before_task_publish_handler(
|
||||
trigger_source = _determine_trigger_source(headers)
|
||||
owner_id = _extract_owner_id(task_type, task_kwargs)
|
||||
|
||||
# A retried task is republished with the same task_id, so this fires
|
||||
# again for it; get_or_create keeps the original PENDING record
|
||||
# instead of raising a duplicate-key IntegrityError on the retry.
|
||||
PaperlessTask.objects.get_or_create(
|
||||
PaperlessTask.objects.create(
|
||||
task_id=task_id,
|
||||
defaults={
|
||||
"task_type": task_type,
|
||||
"trigger_source": trigger_source,
|
||||
"status": PaperlessTask.Status.PENDING,
|
||||
"input_data": input_data,
|
||||
"owner_id": owner_id,
|
||||
},
|
||||
task_type=task_type,
|
||||
trigger_source=trigger_source,
|
||||
status=PaperlessTask.Status.PENDING,
|
||||
input_data=input_data,
|
||||
owner_id=owner_id,
|
||||
)
|
||||
except Exception: # pragma: no cover
|
||||
logger.exception("Creating PaperlessTask failed")
|
||||
|
||||
@@ -194,6 +194,56 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(config.language, None)
|
||||
self.assertEqual(config.barcode_tag_mapping, None)
|
||||
|
||||
def test_api_update_config_rejects_non_dict_user_args(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to update app config with a JSON-encoded non-dict
|
||||
value (e.g. a bare string) for the user_args JSONField
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Request is rejected with a 400, not silently accepted
|
||||
- Config is not updated
|
||||
"""
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}1/",
|
||||
json.dumps(
|
||||
{
|
||||
"user_args": json.dumps("not a dict"),
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
config = ApplicationConfiguration.objects.first()
|
||||
assert config is not None
|
||||
self.assertEqual(config.user_args, None)
|
||||
|
||||
def test_api_update_config_rejects_non_dict_barcode_tag_mapping(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to update app config with a JSON-encoded non-dict
|
||||
value (e.g. a bare list) for the barcode_tag_mapping JSONField
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Request is rejected with a 400, not silently accepted
|
||||
- Config is not updated
|
||||
"""
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}1/",
|
||||
json.dumps(
|
||||
{
|
||||
"barcode_tag_mapping": json.dumps([1, 2, 3]),
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
config = ApplicationConfiguration.objects.first()
|
||||
assert config is not None
|
||||
self.assertEqual(config.barcode_tag_mapping, None)
|
||||
|
||||
def test_api_replace_app_logo(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -1165,65 +1165,6 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
self.assertIn(b"set_permissions not specified", response.content)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_set_permissions_rejects_nonexistent_owner(self, m) -> None:
|
||||
"""
|
||||
BulkEditSerializer._validate_owner called User.objects.get(pk=owner)
|
||||
with no try/except, so a syntactically valid but nonexistent user
|
||||
id raised an uncaught User.DoesNotExist instead of a clean 400.
|
||||
"""
|
||||
self.setup_mock(m, "set_permissions")
|
||||
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_permissions",
|
||||
"parameters": {
|
||||
"set_permissions": {"view": {"users": [self.user.id]}},
|
||||
"owner": 999999,
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_set_permissions_rejects_wrong_typed_owner(self, m) -> None:
|
||||
"""
|
||||
_validate_owner only caught User.DoesNotExist -- a wrong-typed
|
||||
owner (list/dict/non-numeric string) reaches
|
||||
User.objects.get(pk=owner) and raises an uncaught TypeError or
|
||||
ValueError instead, since `parameters` is a bare DictField with
|
||||
no type checking on "owner" at that level.
|
||||
"""
|
||||
self.setup_mock(m, "set_permissions")
|
||||
|
||||
for bad_owner in (["not", "an", "id"], {"nested": "dict"}, "not-a-number"):
|
||||
with self.subTest(owner=bad_owner):
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "set_permissions",
|
||||
"parameters": {
|
||||
"set_permissions": {
|
||||
"view": {"users": [self.user.id]},
|
||||
},
|
||||
"owner": bad_owner,
|
||||
},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.set_permissions")
|
||||
def test_set_permissions_merge(self, m) -> None:
|
||||
self.setup_mock(m, "set_permissions")
|
||||
@@ -1497,53 +1438,6 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.rotate")
|
||||
def test_bulk_edit_rotate_rejects_null_degrees(self, m) -> None:
|
||||
"""
|
||||
BulkEditSerializer._validate_parameters_rotate's
|
||||
`float(parameters["degrees"])` raised an uncaught TypeError for
|
||||
None (only ValueError was caught), reachable via the legacy
|
||||
generic /api/documents/bulk_edit/ method="rotate" path (the
|
||||
dedicated /api/documents/rotate/ endpoint isn't affected, its
|
||||
`degrees` field is a typed IntegerField).
|
||||
"""
|
||||
self.setup_mock(m, "rotate")
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "rotate",
|
||||
"parameters": {"degrees": None},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.serialisers.bulk_edit.split")
|
||||
def test_bulk_edit_split_rejects_null_pages(self, m) -> None:
|
||||
"""
|
||||
BulkEditSerializer._validate_parameters_split called
|
||||
parameters["pages"].split(",") with no type check, so a null
|
||||
value raised an uncaught AttributeError instead of a clean 400.
|
||||
"""
|
||||
self.setup_mock(m, "split")
|
||||
response = self.client.post(
|
||||
"/api/documents/bulk_edit/",
|
||||
json.dumps(
|
||||
{
|
||||
"documents": [self.doc2.id],
|
||||
"method": "split",
|
||||
"parameters": {"pages": None},
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
m.assert_not_called()
|
||||
|
||||
@mock.patch("documents.views.bulk_edit.rotate")
|
||||
def test_rotate_insufficient_permissions(self, m) -> None:
|
||||
self.doc1.owner = User.objects.get(username="temp_admin")
|
||||
|
||||
@@ -106,17 +106,6 @@ class TestBeforeTaskPublishHandler:
|
||||
assert task.task_type == PaperlessTask.TaskType.TRAIN_CLASSIFIER
|
||||
assert task.trigger_source == PaperlessTask.TriggerSource.MANUAL
|
||||
|
||||
# A Celery retry republishes with the same task_id; this must not
|
||||
# raise a duplicate-key IntegrityError, and must leave the original
|
||||
# PENDING record alone.
|
||||
send_publish(
|
||||
"documents.tasks.train_classifier",
|
||||
(),
|
||||
{},
|
||||
headers={"id": task_id},
|
||||
)
|
||||
assert PaperlessTask.objects.filter(task_id=task_id).count() == 1
|
||||
|
||||
def test_creates_task_for_sanity_check(self) -> None:
|
||||
task_id = send_publish("documents.tasks.sanity_check", (), {})
|
||||
task = PaperlessTask.objects.get(task_id=task_id)
|
||||
|
||||
@@ -235,6 +235,20 @@ class ApplicationConfigurationSerializer(
|
||||
) -> list[str]:
|
||||
return sorted(name for name in os.environ if name.startswith("PAPERLESS_"))
|
||||
|
||||
def validate_user_args(self, value):
|
||||
if value is not None and not isinstance(value, dict):
|
||||
raise serializers.ValidationError(
|
||||
"user_args must be a JSON object.",
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_barcode_tag_mapping(self, value):
|
||||
if value is not None and not isinstance(value, dict):
|
||||
raise serializers.ValidationError(
|
||||
"barcode_tag_mapping must be a JSON object.",
|
||||
)
|
||||
return value
|
||||
|
||||
def run_validation(self, data):
|
||||
# Empty strings treated as None to avoid unexpected behavior
|
||||
if "user_args" in data and data["user_args"] == "":
|
||||
|
||||
Reference in New Issue
Block a user