Files
paperless-ngx/src/documents/tests/test_admin.py
Trenton H 50f6b2d4c3 feat(search): wire Tantivy backend into all callsites; remove Whoosh
- Replace all `from documents import index` + Whoosh writer usage across
  admin.py, bulk_edit.py, tasks.py, views.py, signals/handlers.py with
  `get_backend().add_or_update/remove/batch_update`
- Add `effective_content` param to `_build_tantivy_doc` / `add_or_update`
  (used by signal handler to re-index root doc with version's OCR text)
- Add `wipe_index()` (renamed from `_wipe_index`) to public API; use from
  `document_index --recreate` flag
- `index_optimize()` replaced with deprecation log message; Tantivy
  manages segment merging automatically
- `index_reindex()` now calls `get_backend().rebuild()` + `reset_backend()`
  with select_related/prefetch_related for efficiency
- `document_index` management command: add `--recreate` flag
- Status view: use `get_backend()` + dir mtime scan instead of Whoosh
  `ix.last_modified()`
- Delete `documents/index.py`, `test_index.py`, `test_delayedquery.py`
- Update all tests: patch `documents.search.get_backend` (lazy imports);
  `DirectoriesMixin` calls `reset_backend()` in setUp/tearDown;
  `TestDocumentConsumptionFinishedSignal` likewise
- `test_api_search.py`: fix order-independent assertions for date-range
  queries; fix `_rewrite_8digit_date` to be field-aware and
  timezone-correct for DateTimeField vs DateField

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 10:43:30 -07:00

182 lines
6.4 KiB
Python

import types
from unittest.mock import patch
import tantivy
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone
from rest_framework import status
from documents.admin import DocumentAdmin
from documents.admin import TagAdmin
from documents.models import Document
from documents.models import Tag
from documents.search import get_backend
from documents.search import reset_backend
from documents.tests.utils import DirectoriesMixin
from paperless.admin import PaperlessUserAdmin
class TestDocumentAdmin(DirectoriesMixin, TestCase):
def get_document_from_index(self, doc):
backend = get_backend()
searcher = backend._index.searcher()
results = searcher.search(
tantivy.Query.range_query(
backend._schema,
"id",
tantivy.FieldType.Unsigned,
doc.pk,
doc.pk,
),
limit=1,
)
if results.hits:
return searcher.doc(results.hits[0][1]).to_dict()
return None
def setUp(self) -> None:
super().setUp()
reset_backend()
self.doc_admin = DocumentAdmin(model=Document, admin_site=AdminSite())
def tearDown(self) -> None:
reset_backend()
super().tearDown()
def test_save_model(self) -> None:
doc = Document.objects.create(title="test")
doc.title = "new title"
self.doc_admin.save_model(None, doc, None, None)
self.assertEqual(Document.objects.get(id=doc.id).title, "new title")
self.assertEqual(self.get_document_from_index(doc)["id"], [doc.id])
def test_delete_model(self) -> None:
doc = Document.objects.create(title="test")
get_backend().add_or_update(doc)
self.assertIsNotNone(self.get_document_from_index(doc))
self.doc_admin.delete_model(None, doc)
self.assertRaises(Document.DoesNotExist, Document.objects.get, id=doc.id)
self.assertIsNone(self.get_document_from_index(doc))
def test_delete_queryset(self) -> None:
docs = []
for i in range(42):
doc = Document.objects.create(
title="Many documents with the same title",
checksum=f"{i:02}",
)
docs.append(doc)
get_backend().add_or_update(doc)
self.assertEqual(Document.objects.count(), 42)
for doc in docs:
self.assertIsNotNone(self.get_document_from_index(doc))
self.doc_admin.delete_queryset(None, Document.objects.all())
self.assertEqual(Document.objects.count(), 0)
for doc in docs:
self.assertIsNone(self.get_document_from_index(doc))
def test_created(self) -> None:
doc = Document.objects.create(
title="test",
created=timezone.make_aware(timezone.datetime(2020, 4, 12)),
)
self.assertEqual(self.doc_admin.created_(doc), "2020-04-12")
class TestTagAdmin(DirectoriesMixin, TestCase):
def setUp(self) -> None:
super().setUp()
self.tag_admin = TagAdmin(model=Tag, admin_site=AdminSite())
@patch("documents.tasks.bulk_update_documents")
def test_parent_tags_get_added(self, mock_bulk_update):
document = Document.objects.create(title="test")
parent = Tag.objects.create(name="parent")
child = Tag.objects.create(name="child")
document.tags.add(child)
child.tn_parent = parent
self.tag_admin.save_model(None, child, None, change=True)
document.refresh_from_db()
self.assertIn(parent, document.tags.all())
class TestPaperlessAdmin(DirectoriesMixin, TestCase):
def setUp(self) -> None:
super().setUp()
self.user_admin = PaperlessUserAdmin(model=User, admin_site=AdminSite())
def test_request_is_passed_to_form(self) -> None:
user = User.objects.create(username="test", is_superuser=False)
non_superuser = User.objects.create(username="requestuser")
request = types.SimpleNamespace(user=non_superuser)
formType = self.user_admin.get_form(request)
form = formType(data={}, instance=user)
self.assertEqual(form.request, request)
def test_only_superuser_can_change_superuser(self) -> None:
superuser = User.objects.create_superuser(username="superuser", password="test")
non_superuser = User.objects.create(username="requestuser")
user = User.objects.create(username="test", is_superuser=False)
data = {
"username": "test",
"is_superuser": True,
}
form = self.user_admin.form(data, instance=user)
form.request = types.SimpleNamespace(user=non_superuser)
self.assertFalse(form.is_valid())
self.assertEqual(
form.errors.get("__all__"),
["Superuser status can only be changed by a superuser"],
)
form = self.user_admin.form(data, instance=user)
form.request = types.SimpleNamespace(user=superuser)
self.assertTrue(form.is_valid())
self.assertEqual({}, form.errors)
def test_superuser_can_only_be_modified_by_superuser(self) -> None:
superuser = User.objects.create_superuser(username="superuser", password="test")
user = User.objects.create(
username="test",
is_superuser=False,
is_staff=True,
)
change_user_perm = Permission.objects.get(codename="change_user")
user.user_permissions.add(change_user_perm)
self.client.force_login(user)
response = self.client.patch(
f"/api/users/{superuser.pk}/",
{"first_name": "Updated"},
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
response.content.decode(),
"Superusers can only be modified by other superusers",
)
self.client.logout()
self.client.force_login(superuser)
response = self.client.patch(
f"/api/users/{superuser.pk}/",
{"first_name": "Updated"},
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
superuser.refresh_from_db()
self.assertEqual(superuser.first_name, "Updated")