mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-27 13:13:18 +00:00
DocumentViewSet.list's trailing except Exception clause was catching the re-raised QueryParserError/INTERNAL-cause QueryError that _map_emit_error and whoosh-compat's own parse() deliberately let escape, and turning them into a generic 400 -- exactly the outcome that routing exists to prevent. Remove the catch-all (and the now-redundant QueryParserError re-raise it made pointless) so a whoosh-compat library defect surfaces as a monitorable 500 instead of blaming the user for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
276 lines
9.1 KiB
Python
276 lines
9.1 KiB
Python
"""The search list endpoint's exception handling: what becomes a 400 and
|
|
what a library defect surfaces as instead.
|
|
|
|
Companion to documents/tests/search/test_error_routing.py, which pins the
|
|
Cause -> SearchQueryError/QueryError routing inside documents/search/_query.py.
|
|
These tests pin the layer above it: DocumentViewSet.list's own except clauses,
|
|
which decide what an already-routed error becomes on the wire.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
from rest_framework import status
|
|
from whoosh_compat.errors import Cause
|
|
from whoosh_compat.errors import Diagnostic
|
|
from whoosh_compat.errors import DiagnosticKind
|
|
from whoosh_compat.errors import QueryError
|
|
|
|
from documents.search import SearchQueryError
|
|
from documents.tests.factories import DocumentFactory
|
|
|
|
if TYPE_CHECKING:
|
|
from rest_framework.test import APIClient
|
|
|
|
from documents.models import Document
|
|
|
|
pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("_search_index")]
|
|
|
|
|
|
@pytest.fixture
|
|
def indexed_document() -> Document:
|
|
from documents.search import get_backend
|
|
|
|
doc = DocumentFactory.create(title="quarterly invoice", content="acme corp")
|
|
get_backend().add_or_update(doc)
|
|
return doc
|
|
|
|
|
|
class TestSearchQueryErrorStillBecomesA400:
|
|
def test_search_query_error_becomes_a_400_naming_the_field(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_search_query_error(*args: object, **kwargs: object) -> object:
|
|
raise SearchQueryError("bad value for field 'added'")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod,
|
|
"parse_user_query",
|
|
raise_search_query_error,
|
|
)
|
|
|
|
response = admin_client.get("/api/documents/?query=anything")
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
assert "added" in str(response.data["query"])
|
|
|
|
|
|
class TestLibraryDefectsPropagate:
|
|
"""The exact regression this task exists to fix: an unexpected or
|
|
INTERNAL-cause library error must not be relabeled a 400."""
|
|
|
|
def test_unexpected_exception_is_not_converted_to_a_400(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_zero_division(*args: object, **kwargs: object) -> object:
|
|
raise ZeroDivisionError("synthetic bug, unrelated to search grammar")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod,
|
|
"parse_user_query",
|
|
raise_zero_division,
|
|
)
|
|
|
|
with pytest.raises(ZeroDivisionError):
|
|
admin_client.get("/api/documents/?query=anything")
|
|
|
|
def test_internal_cause_query_error_is_not_converted_to_a_400(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
"""Forces the one library-internal failure mode reachable from a real
|
|
query: emit() reporting a defect in itself (Cause.INTERNAL) after a
|
|
real query string went through the real parse and routing pipeline.
|
|
|
|
``tantivy_emit`` (the whoosh-compat emitter) is monkeypatched rather
|
|
than ``parse_user_query`` itself, so everything upstream of it --
|
|
the pre-parse rewrites, ``wc.parse()``, and ``_map_emit_error``'s own
|
|
Cause routing in documents/search/_query.py -- runs for real; only
|
|
the final emit call is forced to report the defect.
|
|
"""
|
|
import documents.search._query as query_mod
|
|
|
|
def raise_internal(*args: object, **kwargs: object) -> object:
|
|
raise QueryError(
|
|
Diagnostic(
|
|
kind=DiagnosticKind.BACKEND_REJECTED,
|
|
cause=Cause.INTERNAL,
|
|
message="synthetic whoosh-compat emitter defect",
|
|
),
|
|
)
|
|
|
|
monkeypatch.setattr(query_mod, "tantivy_emit", raise_internal)
|
|
|
|
with pytest.raises(QueryError):
|
|
admin_client.get("/api/documents/?query=invoice")
|
|
|
|
|
|
class TestSelectionPathsAgreeWithSearch:
|
|
"""DocumentSelectionMixin backs bulk edit, bulk download, and a
|
|
more_like_id selection filter. It catches only SearchQueryError -- the
|
|
same contract the search list endpoint enforces above -- so all three
|
|
must map SearchQueryError to a 400 and let anything else surface."""
|
|
|
|
def test_bulk_edit_maps_search_query_error_to_a_400(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_search_query_error(*args: object, **kwargs: object) -> object:
|
|
raise SearchQueryError("bad value for field 'added'")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod,
|
|
"parse_user_query",
|
|
raise_search_query_error,
|
|
)
|
|
|
|
response = admin_client.post(
|
|
"/api/documents/bulk_edit/",
|
|
{
|
|
"documents": [],
|
|
"all": True,
|
|
"filters": {"query": "anything"},
|
|
"method": "set_document_type",
|
|
"parameters": {"document_type": None},
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
assert "added" in str(response.data["query"])
|
|
|
|
def test_bulk_edit_lets_an_unexpected_exception_surface(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_zero_division(*args: object, **kwargs: object) -> object:
|
|
raise ZeroDivisionError("synthetic bug, unrelated to search grammar")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod,
|
|
"parse_user_query",
|
|
raise_zero_division,
|
|
)
|
|
|
|
with pytest.raises(ZeroDivisionError):
|
|
admin_client.post(
|
|
"/api/documents/bulk_edit/",
|
|
{
|
|
"documents": [],
|
|
"all": True,
|
|
"filters": {"query": "anything"},
|
|
"method": "set_document_type",
|
|
"parameters": {"document_type": None},
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
def test_bulk_download_maps_search_query_error_to_a_400(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_search_query_error(*args: object, **kwargs: object) -> object:
|
|
raise SearchQueryError("bad value for field 'added'")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod,
|
|
"parse_user_query",
|
|
raise_search_query_error,
|
|
)
|
|
|
|
response = admin_client.post(
|
|
"/api/documents/bulk_download/",
|
|
{
|
|
"documents": [],
|
|
"all": True,
|
|
"filters": {"query": "anything"},
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
assert "added" in str(response.data["query"])
|
|
|
|
def test_more_like_id_selection_filter_maps_search_query_error_to_a_400(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_search_query_error(*args: object, **kwargs: object) -> object:
|
|
raise SearchQueryError("similar-document lookup is unavailable")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod.TantivyBackend,
|
|
"more_like_this_ids",
|
|
raise_search_query_error,
|
|
)
|
|
|
|
response = admin_client.post(
|
|
"/api/documents/bulk_download/",
|
|
{
|
|
"documents": [],
|
|
"all": True,
|
|
"filters": {"more_like_id": indexed_document.pk},
|
|
},
|
|
format="json",
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
def test_more_like_id_selection_filter_lets_an_unexpected_exception_surface(
|
|
self,
|
|
admin_client: APIClient,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
indexed_document: Document,
|
|
) -> None:
|
|
import documents.search._backend as backend_mod
|
|
|
|
def raise_zero_division(*args: object, **kwargs: object) -> object:
|
|
raise ZeroDivisionError("synthetic bug, unrelated to similarity lookup")
|
|
|
|
monkeypatch.setattr(
|
|
backend_mod.TantivyBackend,
|
|
"more_like_this_ids",
|
|
raise_zero_division,
|
|
)
|
|
|
|
with pytest.raises(ZeroDivisionError):
|
|
admin_client.post(
|
|
"/api/documents/bulk_download/",
|
|
{
|
|
"documents": [],
|
|
"all": True,
|
|
"filters": {"more_like_id": indexed_document.pk},
|
|
},
|
|
format="json",
|
|
)
|