From e2d25a7a094fd5594f012525e48ff1cf6eb41908 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:08:51 -0800 Subject: [PATCH] Chore: reorganize api tests (#4935) * Move permissions-related API tests * Move bulk-edit-related API tests * Move bulk-download-related API tests * Move uisettings-related API tests * Move remoteversion-related API tests * Move tasks API tests * Move object-related API tests * Move consumption-template-related API tests * Rename pared-down documents API test file Co-Authored-By: Trenton H <797416+stumpylog@users.noreply.github.com> --- src/documents/tests/test_api.py | 4907 ----------------- src/documents/tests/test_api_bulk_download.py | 337 ++ src/documents/tests/test_api_bulk_edit.py | 870 +++ .../tests/test_api_consumption_templates.py | 236 + src/documents/tests/test_api_documents.py | 1992 +++++++ src/documents/tests/test_api_objects.py | 224 + src/documents/tests/test_api_permissions.py | 910 +++ .../tests/test_api_remote_version.py | 123 + src/documents/tests/test_api_tasks.py | 240 + src/documents/tests/test_api_uisettings.py | 65 + 10 files changed, 4997 insertions(+), 4907 deletions(-) delete mode 100644 src/documents/tests/test_api.py create mode 100644 src/documents/tests/test_api_bulk_download.py create mode 100644 src/documents/tests/test_api_bulk_edit.py create mode 100644 src/documents/tests/test_api_consumption_templates.py create mode 100644 src/documents/tests/test_api_documents.py create mode 100644 src/documents/tests/test_api_objects.py create mode 100644 src/documents/tests/test_api_permissions.py create mode 100644 src/documents/tests/test_api_remote_version.py create mode 100644 src/documents/tests/test_api_tasks.py create mode 100644 src/documents/tests/test_api_uisettings.py diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py deleted file mode 100644 index cb1290057..000000000 --- a/src/documents/tests/test_api.py +++ /dev/null @@ -1,4907 +0,0 @@ -import datetime -import io -import json -import os -import shutil -import tempfile -import urllib.request -import uuid -import zipfile -import zoneinfo -from datetime import timedelta -from pathlib import Path -from unittest import mock -from unittest.mock import MagicMock - -import celery -from dateutil import parser -from django.conf import settings -from django.contrib.auth.models import Group -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.test import override_settings -from django.utils import timezone -from guardian.shortcuts import assign_perm -from guardian.shortcuts import get_perms -from guardian.shortcuts import get_users_with_perms -from rest_framework import status -from rest_framework.test import APITestCase - -from documents import bulk_edit -from documents.data_models import DocumentSource -from documents.models import ConsumptionTemplate -from documents.models import Correspondent -from documents.models import CustomField -from documents.models import CustomFieldInstance -from documents.models import Document -from documents.models import DocumentType -from documents.models import MatchingModel -from documents.models import Note -from documents.models import PaperlessTask -from documents.models import SavedView -from documents.models import ShareLink -from documents.models import StoragePath -from documents.models import Tag -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import DocumentConsumeDelayMixin -from paperless import version -from paperless_mail.models import MailAccount -from paperless_mail.models import MailRule - - -class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): - def setUp(self): - super().setUp() - - self.user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=self.user) - - def testDocuments(self): - response = self.client.get("/api/documents/").data - - self.assertEqual(response["count"], 0) - - c = Correspondent.objects.create(name="c", pk=41) - dt = DocumentType.objects.create(name="dt", pk=63) - tag = Tag.objects.create(name="t", pk=85) - - doc = Document.objects.create( - title="WOW", - content="the content", - correspondent=c, - document_type=dt, - checksum="123", - mime_type="application/pdf", - ) - - doc.tags.add(tag) - - response = self.client.get("/api/documents/", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) - - returned_doc = response.data["results"][0] - self.assertEqual(returned_doc["id"], doc.id) - self.assertEqual(returned_doc["title"], doc.title) - self.assertEqual(returned_doc["correspondent"], c.id) - self.assertEqual(returned_doc["document_type"], dt.id) - self.assertListEqual(returned_doc["tags"], [tag.id]) - - c2 = Correspondent.objects.create(name="c2") - - returned_doc["correspondent"] = c2.pk - returned_doc["title"] = "the new title" - - response = self.client.put( - f"/api/documents/{doc.pk}/", - returned_doc, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - doc_after_save = Document.objects.get(id=doc.id) - - self.assertEqual(doc_after_save.correspondent, c2) - self.assertEqual(doc_after_save.title, "the new title") - - self.client.delete(f"/api/documents/{doc_after_save.pk}/") - - self.assertEqual(len(Document.objects.all()), 0) - - def test_document_fields(self): - c = Correspondent.objects.create(name="c", pk=41) - dt = DocumentType.objects.create(name="dt", pk=63) - Tag.objects.create(name="t", pk=85) - storage_path = StoragePath.objects.create(name="sp", pk=77, path="p") - Document.objects.create( - title="WOW", - content="the content", - correspondent=c, - document_type=dt, - checksum="123", - mime_type="application/pdf", - storage_path=storage_path, - ) - - response = self.client.get("/api/documents/", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results_full = response.data["results"] - self.assertIn("content", results_full[0]) - self.assertIn("id", results_full[0]) - - response = self.client.get("/api/documents/?fields=id", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertFalse("content" in results[0]) - self.assertIn("id", results[0]) - self.assertEqual(len(results[0]), 1) - - response = self.client.get("/api/documents/?fields=content", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertIn("content", results[0]) - self.assertFalse("id" in results[0]) - self.assertEqual(len(results[0]), 1) - - response = self.client.get("/api/documents/?fields=id,content", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertIn("content", results[0]) - self.assertIn("id", results[0]) - self.assertEqual(len(results[0]), 2) - - response = self.client.get( - "/api/documents/?fields=id,conteasdnt", - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertFalse("content" in results[0]) - self.assertIn("id", results[0]) - self.assertEqual(len(results[0]), 1) - - response = self.client.get("/api/documents/?fields=", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results_full[0]), len(results[0])) - - response = self.client.get("/api/documents/?fields=dgfhs", format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results[0]), 0) - - def test_document_actions(self): - _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) - - content = b"This is a test" - content_thumbnail = b"thumbnail content" - - with open(filename, "wb") as f: - f.write(content) - - doc = Document.objects.create( - title="none", - filename=os.path.basename(filename), - mime_type="application/pdf", - ) - - with open( - os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"), - "wb", - ) as f: - f.write(content_thumbnail) - - response = self.client.get(f"/api/documents/{doc.pk}/download/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content) - - response = self.client.get(f"/api/documents/{doc.pk}/preview/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content) - - response = self.client.get(f"/api/documents/{doc.pk}/thumb/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content_thumbnail) - - def test_document_actions_with_perms(self): - """ - GIVEN: - - Document with owner and without granted permissions - - User is then granted permissions - WHEN: - - User tries to load preview, thumbnail - THEN: - - Initially, HTTP 403 Forbidden - - With permissions, HTTP 200 OK - """ - _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) - - content = b"This is a test" - content_thumbnail = b"thumbnail content" - - with open(filename, "wb") as f: - f.write(content) - - user1 = User.objects.create_user(username="test1") - user2 = User.objects.create_user(username="test2") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - user2.user_permissions.add(*Permission.objects.filter(codename="view_document")) - - self.client.force_authenticate(user2) - - doc = Document.objects.create( - title="none", - filename=os.path.basename(filename), - mime_type="application/pdf", - owner=user1, - ) - - with open( - os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"), - "wb", - ) as f: - f.write(content_thumbnail) - - response = self.client.get(f"/api/documents/{doc.pk}/download/") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - response = self.client.get(f"/api/documents/{doc.pk}/preview/") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - response = self.client.get(f"/api/documents/{doc.pk}/thumb/") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - assign_perm("view_document", user2, doc) - - response = self.client.get(f"/api/documents/{doc.pk}/download/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.get(f"/api/documents/{doc.pk}/preview/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.get(f"/api/documents/{doc.pk}/thumb/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @override_settings(FILENAME_FORMAT="") - def test_download_with_archive(self): - content = b"This is a test" - content_archive = b"This is the same test but archived" - - doc = Document.objects.create( - title="none", - filename="my_document.pdf", - archive_filename="archived.pdf", - mime_type="application/pdf", - ) - - with open(doc.source_path, "wb") as f: - f.write(content) - - with open(doc.archive_path, "wb") as f: - f.write(content_archive) - - response = self.client.get(f"/api/documents/{doc.pk}/download/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content_archive) - - response = self.client.get( - f"/api/documents/{doc.pk}/download/?original=true", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content) - - response = self.client.get(f"/api/documents/{doc.pk}/preview/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content_archive) - - response = self.client.get( - f"/api/documents/{doc.pk}/preview/?original=true", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content, content) - - def test_document_actions_not_existing_file(self): - doc = Document.objects.create( - title="none", - filename=os.path.basename("asd"), - mime_type="application/pdf", - ) - - response = self.client.get(f"/api/documents/{doc.pk}/download/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - response = self.client.get(f"/api/documents/{doc.pk}/preview/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - response = self.client.get(f"/api/documents/{doc.pk}/thumb/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_document_filters(self): - doc1 = Document.objects.create( - title="none1", - checksum="A", - mime_type="application/pdf", - ) - doc2 = Document.objects.create( - title="none2", - checksum="B", - mime_type="application/pdf", - ) - doc3 = Document.objects.create( - title="none3", - checksum="C", - mime_type="application/pdf", - ) - - tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True) - tag_2 = Tag.objects.create(name="t2") - tag_3 = Tag.objects.create(name="t3") - - cf1 = CustomField.objects.create( - name="stringfield", - data_type=CustomField.FieldDataType.STRING, - ) - cf2 = CustomField.objects.create( - name="numberfield", - data_type=CustomField.FieldDataType.INT, - ) - - doc1.tags.add(tag_inbox) - doc2.tags.add(tag_2) - doc3.tags.add(tag_2) - doc3.tags.add(tag_3) - - cf1_d1 = CustomFieldInstance.objects.create( - document=doc1, - field=cf1, - value_text="foobard1", - ) - CustomFieldInstance.objects.create( - document=doc1, - field=cf2, - value_int=999, - ) - cf1_d3 = CustomFieldInstance.objects.create( - document=doc3, - field=cf1, - value_text="foobard3", - ) - - response = self.client.get("/api/documents/?is_in_inbox=true") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc1.id) - - response = self.client.get("/api/documents/?is_in_inbox=false") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id]) - - response = self.client.get( - f"/api/documents/?tags__id__in={tag_inbox.id},{tag_3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc3.id]) - - response = self.client.get( - f"/api/documents/?tags__id__in={tag_2.id},{tag_3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id]) - - response = self.client.get( - f"/api/documents/?tags__id__all={tag_2.id},{tag_3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc3.id) - - response = self.client.get( - f"/api/documents/?tags__id__all={tag_inbox.id},{tag_3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 0) - - response = self.client.get( - f"/api/documents/?tags__id__all={tag_inbox.id}a{tag_3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 3) - - response = self.client.get(f"/api/documents/?tags__id__none={tag_3.id}") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc2.id]) - - response = self.client.get( - f"/api/documents/?tags__id__none={tag_3.id},{tag_2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc1.id) - - response = self.client.get( - f"/api/documents/?tags__id__none={tag_2.id},{tag_inbox.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 0) - - response = self.client.get( - f"/api/documents/?id__in={doc1.id},{doc2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - - response = self.client.get( - f"/api/documents/?id__range={doc1.id},{doc3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 3) - - response = self.client.get( - f"/api/documents/?id={doc2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - - # custom field name - response = self.client.get( - f"/api/documents/?custom_fields__icontains={cf1.name}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - - # custom field value - response = self.client.get( - f"/api/documents/?custom_fields__icontains={cf1_d1.value}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc1.id) - - response = self.client.get( - f"/api/documents/?custom_fields__icontains={cf1_d3.value}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc3.id) - - def test_document_checksum_filter(self): - Document.objects.create( - title="none1", - checksum="A", - mime_type="application/pdf", - ) - doc2 = Document.objects.create( - title="none2", - checksum="B", - mime_type="application/pdf", - ) - Document.objects.create( - title="none3", - checksum="C", - mime_type="application/pdf", - ) - - response = self.client.get("/api/documents/?checksum__iexact=B") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc2.id) - - response = self.client.get("/api/documents/?checksum__iexact=X") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 0) - - def test_document_original_filename_filter(self): - doc1 = Document.objects.create( - title="none1", - checksum="A", - mime_type="application/pdf", - original_filename="docA.pdf", - ) - doc2 = Document.objects.create( - title="none2", - checksum="B", - mime_type="application/pdf", - original_filename="docB.pdf", - ) - doc3 = Document.objects.create( - title="none3", - checksum="C", - mime_type="application/pdf", - original_filename="docC.pdf", - ) - - response = self.client.get("/api/documents/?original_filename__iexact=DOCa.pdf") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["id"], doc1.id) - - response = self.client.get("/api/documents/?original_filename__iexact=docx.pdf") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 0) - - response = self.client.get("/api/documents/?original_filename__istartswith=dOc") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 3) - self.assertCountEqual( - [results[0]["id"], results[1]["id"], results[2]["id"]], - [doc1.id, doc2.id, doc3.id], - ) - - def test_documents_title_content_filter(self): - doc1 = Document.objects.create( - title="title A", - content="content A", - checksum="A", - mime_type="application/pdf", - ) - doc2 = Document.objects.create( - title="title B", - content="content A", - checksum="B", - mime_type="application/pdf", - ) - doc3 = Document.objects.create( - title="title A", - content="content B", - checksum="C", - mime_type="application/pdf", - ) - doc4 = Document.objects.create( - title="title B", - content="content B", - checksum="D", - mime_type="application/pdf", - ) - - response = self.client.get("/api/documents/?title_content=A") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 3) - self.assertCountEqual( - [results[0]["id"], results[1]["id"], results[2]["id"]], - [doc1.id, doc2.id, doc3.id], - ) - - response = self.client.get("/api/documents/?title_content=B") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 3) - self.assertCountEqual( - [results[0]["id"], results[1]["id"], results[2]["id"]], - [doc2.id, doc3.id, doc4.id], - ) - - response = self.client.get("/api/documents/?title_content=X") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 0) - - def test_document_owner_filters(self): - """ - GIVEN: - - Documents with owners, with and without granted permissions - WHEN: - - User filters by owner - THEN: - - Owner filters work correctly but still respect permissions - """ - u1 = User.objects.create_user("user1") - u2 = User.objects.create_user("user2") - u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) - - u1_doc1 = Document.objects.create( - title="none1", - checksum="A", - mime_type="application/pdf", - owner=u1, - ) - Document.objects.create( - title="none2", - checksum="B", - mime_type="application/pdf", - owner=u2, - ) - u0_doc1 = Document.objects.create( - title="none3", - checksum="C", - mime_type="application/pdf", - ) - u1_doc2 = Document.objects.create( - title="none4", - checksum="D", - mime_type="application/pdf", - owner=u1, - ) - u2_doc2 = Document.objects.create( - title="none5", - checksum="E", - mime_type="application/pdf", - owner=u2, - ) - - self.client.force_authenticate(user=u1) - assign_perm("view_document", u1, u2_doc2) - - # Will not show any u1 docs or u2_doc1 which isn't shared - response = self.client.get(f"/api/documents/?owner__id__none={u1.id}") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - self.assertCountEqual( - [results[0]["id"], results[1]["id"]], - [u0_doc1.id, u2_doc2.id], - ) - - # Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared - response = self.client.get( - f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) - - # Will not show any u1 docs, u2_doc2 which is shared but has owner - response = self.client.get( - f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertCountEqual([results[0]["id"]], [u0_doc1.id]) - - # Will not show any u1 docs or u2_doc1 which is not shared - response = self.client.get(f"/api/documents/?owner__id={u2.id}") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) - - # Will not show u2_doc1 which is not shared - response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}") - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 3) - self.assertCountEqual( - [results[0]["id"], results[1]["id"], results[2]["id"]], - [u1_doc1.id, u1_doc2.id, u2_doc2.id], - ) - - def test_pagination_all(self): - """ - GIVEN: - - A set of 50 documents - WHEN: - - API request for document filtering - THEN: - - Results are paginated (25 items) and response["all"] returns all ids (50 items) - """ - t = Tag.objects.create(name="tag") - docs = [] - for i in range(50): - d = Document.objects.create(checksum=i, content=f"test{i}") - d.tags.add(t) - docs.append(d) - - response = self.client.get( - f"/api/documents/?tags__id__in={t.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 25) - self.assertEqual(len(response.data["all"]), 50) - self.assertCountEqual(response.data["all"], [d.id for d in docs]) - - def test_statistics(self): - doc1 = Document.objects.create( - title="none1", - checksum="A", - mime_type="application/pdf", - content="abc", - ) - Document.objects.create( - title="none2", - checksum="B", - mime_type="application/pdf", - content="123", - ) - Document.objects.create( - title="none3", - checksum="C", - mime_type="text/plain", - content="hello", - ) - - tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True) - Tag.objects.create(name="t2") - Tag.objects.create(name="t3") - Correspondent.objects.create(name="c1") - Correspondent.objects.create(name="c2") - DocumentType.objects.create(name="dt1") - StoragePath.objects.create(name="sp1") - StoragePath.objects.create(name="sp2") - - doc1.tags.add(tag_inbox) - - response = self.client.get("/api/statistics/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["documents_total"], 3) - self.assertEqual(response.data["documents_inbox"], 1) - self.assertEqual(response.data["inbox_tag"], tag_inbox.pk) - self.assertEqual( - response.data["document_file_type_counts"][0]["mime_type_count"], - 2, - ) - self.assertEqual( - response.data["document_file_type_counts"][1]["mime_type_count"], - 1, - ) - self.assertEqual(response.data["character_count"], 11) - self.assertEqual(response.data["tag_count"], 3) - self.assertEqual(response.data["correspondent_count"], 2) - self.assertEqual(response.data["document_type_count"], 1) - self.assertEqual(response.data["storage_path_count"], 2) - - def test_statistics_no_inbox_tag(self): - Document.objects.create(title="none1", checksum="A") - - response = self.client.get("/api/statistics/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["documents_inbox"], None) - self.assertEqual(response.data["inbox_tag"], None) - - def test_upload(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - input_doc, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file.name, "simple.pdf") - self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents) - self.assertIsNone(overrides.title) - self.assertIsNone(overrides.correspondent_id) - self.assertIsNone(overrides.document_type_id) - self.assertIsNone(overrides.tag_ids) - - def test_upload_empty_metadata(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "title": "", "correspondent": "", "document_type": ""}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - input_doc, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file.name, "simple.pdf") - self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents) - self.assertIsNone(overrides.title) - self.assertIsNone(overrides.correspondent_id) - self.assertIsNone(overrides.document_type_id) - self.assertIsNone(overrides.tag_ids) - - def test_upload_invalid_form(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"documenst": f}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.consume_file_mock.assert_not_called() - - def test_upload_invalid_file(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.zip"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.consume_file_mock.assert_not_called() - - def test_upload_with_title(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "title": "my custom title"}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - _, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(overrides.title, "my custom title") - self.assertIsNone(overrides.correspondent_id) - self.assertIsNone(overrides.document_type_id) - self.assertIsNone(overrides.tag_ids) - - def test_upload_with_correspondent(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - c = Correspondent.objects.create(name="test-corres") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "correspondent": c.id}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - _, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(overrides.correspondent_id, c.id) - self.assertIsNone(overrides.title) - self.assertIsNone(overrides.document_type_id) - self.assertIsNone(overrides.tag_ids) - - def test_upload_with_invalid_correspondent(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "correspondent": 3456}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - self.consume_file_mock.assert_not_called() - - def test_upload_with_document_type(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - dt = DocumentType.objects.create(name="invoice") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "document_type": dt.id}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - _, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(overrides.document_type_id, dt.id) - self.assertIsNone(overrides.correspondent_id) - self.assertIsNone(overrides.title) - self.assertIsNone(overrides.tag_ids) - - def test_upload_with_invalid_document_type(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "document_type": 34578}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - self.consume_file_mock.assert_not_called() - - def test_upload_with_tags(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - t1 = Tag.objects.create(name="tag1") - t2 = Tag.objects.create(name="tag2") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "tags": [t2.id, t1.id]}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - _, overrides = self.get_last_consume_delay_call_args() - - self.assertCountEqual(overrides.tag_ids, [t1.id, t2.id]) - self.assertIsNone(overrides.document_type_id) - self.assertIsNone(overrides.correspondent_id) - self.assertIsNone(overrides.title) - - def test_upload_with_invalid_tags(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - t1 = Tag.objects.create(name="tag1") - t2 = Tag.objects.create(name="tag2") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "tags": [t2.id, t1.id, 734563]}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - self.consume_file_mock.assert_not_called() - - def test_upload_with_created(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - created = datetime.datetime( - 2022, - 5, - 12, - 0, - 0, - 0, - 0, - tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"), - ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "created": created}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - _, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(overrides.created, created) - - def test_upload_with_asn(self): - self.consume_file_mock.return_value = celery.result.AsyncResult( - id=str(uuid.uuid4()), - ) - - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: - response = self.client.post( - "/api/documents/post_document/", - {"document": f, "archive_serial_number": 500}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.consume_file_mock.assert_called_once() - - input_doc, overrides = self.get_last_consume_delay_call_args() - - self.assertEqual(input_doc.original_file.name, "simple.pdf") - self.assertEqual(overrides.filename, "simple.pdf") - self.assertIsNone(overrides.correspondent_id) - self.assertIsNone(overrides.document_type_id) - self.assertIsNone(overrides.tag_ids) - self.assertEqual(500, overrides.asn) - - def test_get_metadata(self): - doc = Document.objects.create( - title="test", - filename="file.pdf", - mime_type="image/png", - archive_checksum="A", - archive_filename="archive.pdf", - ) - - source_file = os.path.join( - os.path.dirname(__file__), - "samples", - "documents", - "thumbnails", - "0000001.webp", - ) - archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") - - shutil.copy(source_file, doc.source_path) - shutil.copy(archive_file, doc.archive_path) - - response = self.client.get(f"/api/documents/{doc.pk}/metadata/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - meta = response.data - - self.assertEqual(meta["original_mime_type"], "image/png") - self.assertTrue(meta["has_archive_version"]) - self.assertEqual(len(meta["original_metadata"]), 0) - self.assertGreater(len(meta["archive_metadata"]), 0) - self.assertEqual(meta["media_filename"], "file.pdf") - self.assertEqual(meta["archive_media_filename"], "archive.pdf") - self.assertEqual(meta["original_size"], os.stat(source_file).st_size) - self.assertEqual(meta["archive_size"], os.stat(archive_file).st_size) - - def test_get_metadata_invalid_doc(self): - response = self.client.get("/api/documents/34576/metadata/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_get_metadata_no_archive(self): - doc = Document.objects.create( - title="test", - filename="file.pdf", - mime_type="application/pdf", - ) - - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - doc.source_path, - ) - - response = self.client.get(f"/api/documents/{doc.pk}/metadata/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - meta = response.data - - self.assertEqual(meta["original_mime_type"], "application/pdf") - self.assertFalse(meta["has_archive_version"]) - self.assertGreater(len(meta["original_metadata"]), 0) - self.assertIsNone(meta["archive_metadata"]) - self.assertIsNone(meta["archive_media_filename"]) - - def test_get_metadata_missing_files(self): - doc = Document.objects.create( - title="test", - filename="file.pdf", - mime_type="application/pdf", - archive_filename="file.pdf", - archive_checksum="B", - checksum="A", - ) - - response = self.client.get(f"/api/documents/{doc.pk}/metadata/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - meta = response.data - - self.assertTrue(meta["has_archive_version"]) - self.assertIsNone(meta["original_metadata"]) - self.assertIsNone(meta["original_size"]) - self.assertIsNone(meta["archive_metadata"]) - self.assertIsNone(meta["archive_size"]) - - def test_get_empty_suggestions(self): - doc = Document.objects.create(title="test", mime_type="application/pdf") - - response = self.client.get(f"/api/documents/{doc.pk}/suggestions/") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.data, - { - "correspondents": [], - "tags": [], - "document_types": [], - "storage_paths": [], - "dates": [], - }, - ) - - def test_get_suggestions_invalid_doc(self): - response = self.client.get("/api/documents/34676/suggestions/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - @mock.patch("documents.views.match_storage_paths") - @mock.patch("documents.views.match_document_types") - @mock.patch("documents.views.match_tags") - @mock.patch("documents.views.match_correspondents") - @override_settings(NUMBER_OF_SUGGESTED_DATES=10) - def test_get_suggestions( - self, - match_correspondents, - match_tags, - match_document_types, - match_storage_paths, - ): - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is an invoice from 12.04.2022!", - ) - - match_correspondents.return_value = [Correspondent(id=88), Correspondent(id=2)] - match_tags.return_value = [Tag(id=56), Tag(id=123)] - match_document_types.return_value = [DocumentType(id=23)] - match_storage_paths.return_value = [StoragePath(id=99), StoragePath(id=77)] - - response = self.client.get(f"/api/documents/{doc.pk}/suggestions/") - self.assertEqual( - response.data, - { - "correspondents": [88, 2], - "tags": [56, 123], - "document_types": [23], - "storage_paths": [99, 77], - "dates": ["2022-04-12"], - }, - ) - - @mock.patch("documents.parsers.parse_date_generator") - @override_settings(NUMBER_OF_SUGGESTED_DATES=0) - def test_get_suggestions_dates_disabled( - self, - parse_date_generator, - ): - """ - GIVEN: - - NUMBER_OF_SUGGESTED_DATES = 0 (disables feature) - WHEN: - - API reuqest for document suggestions - THEN: - - Dont check for suggested dates at all - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is an invoice from 12.04.2022!", - ) - - self.client.get(f"/api/documents/{doc.pk}/suggestions/") - self.assertFalse(parse_date_generator.called) - - def test_saved_views(self): - u1 = User.objects.create_superuser("user1") - u2 = User.objects.create_superuser("user2") - - v1 = SavedView.objects.create( - owner=u1, - name="test1", - sort_field="", - show_on_dashboard=False, - show_in_sidebar=False, - ) - SavedView.objects.create( - owner=u2, - name="test2", - sort_field="", - show_on_dashboard=False, - show_in_sidebar=False, - ) - SavedView.objects.create( - owner=u2, - name="test3", - sort_field="", - show_on_dashboard=False, - show_in_sidebar=False, - ) - - response = self.client.get("/api/saved_views/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 0) - - self.assertEqual( - self.client.get(f"/api/saved_views/{v1.id}/").status_code, - status.HTTP_404_NOT_FOUND, - ) - - self.client.force_authenticate(user=u1) - - response = self.client.get("/api/saved_views/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) - - self.assertEqual( - self.client.get(f"/api/saved_views/{v1.id}/").status_code, - status.HTTP_200_OK, - ) - - self.client.force_authenticate(user=u2) - - response = self.client.get("/api/saved_views/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 2) - - self.assertEqual( - self.client.get(f"/api/saved_views/{v1.id}/").status_code, - status.HTTP_404_NOT_FOUND, - ) - - def test_create_update_patch(self): - User.objects.create_user("user1") - - view = { - "name": "test", - "show_on_dashboard": True, - "show_in_sidebar": True, - "sort_field": "created2", - "filter_rules": [{"rule_type": 4, "value": "test"}], - } - - response = self.client.post("/api/saved_views/", view, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - v1 = SavedView.objects.get(name="test") - self.assertEqual(v1.sort_field, "created2") - self.assertEqual(v1.filter_rules.count(), 1) - self.assertEqual(v1.owner, self.user) - - response = self.client.patch( - f"/api/saved_views/{v1.id}/", - {"show_in_sidebar": False}, - format="json", - ) - - v1 = SavedView.objects.get(id=v1.id) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertFalse(v1.show_in_sidebar) - self.assertEqual(v1.filter_rules.count(), 1) - - view["filter_rules"] = [{"rule_type": 12, "value": "secret"}] - - response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - v1 = SavedView.objects.get(id=v1.id) - self.assertEqual(v1.filter_rules.count(), 1) - self.assertEqual(v1.filter_rules.first().value, "secret") - - view["filter_rules"] = [] - - response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - v1 = SavedView.objects.get(id=v1.id) - self.assertEqual(v1.filter_rules.count(), 0) - - def test_get_logs(self): - log_data = "test\ntest2\n" - with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f: - f.write(log_data) - with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: - f.write(log_data) - response = self.client.get("/api/logs/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertCountEqual(response.data, ["mail", "paperless"]) - - def test_get_logs_only_when_exist(self): - log_data = "test\ntest2\n" - with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: - f.write(log_data) - response = self.client.get("/api/logs/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertCountEqual(response.data, ["paperless"]) - - def test_get_invalid_log(self): - response = self.client.get("/api/logs/bogus_log/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - @override_settings(LOGGING_DIR="bogus_dir") - def test_get_nonexistent_log(self): - response = self.client.get("/api/logs/paperless/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_get_log(self): - log_data = "test\ntest2\n" - with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: - f.write(log_data) - response = self.client.get("/api/logs/paperless/") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertListEqual(response.data, ["test", "test2"]) - - def test_invalid_regex_other_algorithm(self): - for endpoint in ["correspondents", "tags", "document_types"]: - response = self.client.post( - f"/api/{endpoint}/", - { - "name": "test", - "matching_algorithm": MatchingModel.MATCH_ANY, - "match": "[", - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint) - - def test_invalid_regex(self): - for endpoint in ["correspondents", "tags", "document_types"]: - response = self.client.post( - f"/api/{endpoint}/", - { - "name": "test", - "matching_algorithm": MatchingModel.MATCH_REGEX, - "match": "[", - }, - format="json", - ) - self.assertEqual( - response.status_code, - status.HTTP_400_BAD_REQUEST, - endpoint, - ) - - def test_valid_regex(self): - for endpoint in ["correspondents", "tags", "document_types"]: - response = self.client.post( - f"/api/{endpoint}/", - { - "name": "test", - "matching_algorithm": MatchingModel.MATCH_REGEX, - "match": "[0-9]", - }, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint) - - def test_regex_no_algorithm(self): - for endpoint in ["correspondents", "tags", "document_types"]: - response = self.client.post( - f"/api/{endpoint}/", - {"name": "test", "match": "[0-9]"}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint) - - def test_tag_color_default(self): - response = self.client.post("/api/tags/", {"name": "tag"}, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#a6cee3") - self.assertEqual( - self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[ - "colour" - ], - 1, - ) - - def test_tag_color(self): - response = self.client.post( - "/api/tags/", - {"name": "tag", "colour": 3}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#b2df8a") - self.assertEqual( - self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[ - "colour" - ], - 3, - ) - - def test_tag_color_invalid(self): - response = self.client.post( - "/api/tags/", - {"name": "tag", "colour": 34}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_tag_color_custom(self): - tag = Tag.objects.create(name="test", color="#abcdef") - self.assertEqual( - self.client.get(f"/api/tags/{tag.id}/", format="json").data["colour"], - 1, - ) - - def test_get_existing_notes(self): - """ - GIVEN: - - A document with a single note - WHEN: - - API reuqest for document notes is made - THEN: - - The associated note is returned - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have notes!", - ) - note = Note.objects.create( - note="This is a note.", - document=doc, - user=self.user, - ) - - response = self.client.get( - f"/api/documents/{doc.pk}/notes/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertEqual(len(resp_data), 1) - - resp_data = resp_data[0] - del resp_data["created"] - - self.assertDictEqual( - resp_data, - { - "id": note.id, - "note": note.note, - "user": { - "id": note.user.id, - "username": note.user.username, - "first_name": note.user.first_name, - "last_name": note.user.last_name, - }, - }, - ) - - def test_create_note(self): - """ - GIVEN: - - Existing document - WHEN: - - API request is made to add a note - THEN: - - note is created and associated with document, modified time is updated - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have notes added", - created=timezone.now() - timedelta(days=1), - ) - # set to yesterday - doc.modified = timezone.now() - timedelta(days=1) - self.assertEqual(doc.modified.day, (timezone.now() - timedelta(days=1)).day) - - resp = self.client.post( - f"/api/documents/{doc.pk}/notes/", - data={"note": "this is a posted note"}, - ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - - response = self.client.get( - f"/api/documents/{doc.pk}/notes/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertEqual(len(resp_data), 1) - - resp_data = resp_data[0] - - self.assertEqual(resp_data["note"], "this is a posted note") - - doc = Document.objects.get(pk=doc.pk) - # modified was updated to today - self.assertEqual(doc.modified.day, timezone.now().day) - - def test_notes_permissions_aware(self): - """ - GIVEN: - - Existing document owned by user2 but with granted view perms for user1 - WHEN: - - API request is made by user1 to add a note or delete - THEN: - - Notes are neither created nor deleted - """ - user1 = User.objects.create_user(username="test1") - user1.user_permissions.add(*Permission.objects.all()) - user1.save() - - user2 = User.objects.create_user(username="test2") - user2.save() - - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have notes added", - ) - doc.owner = user2 - doc.save() - - self.client.force_authenticate(user1) - - resp = self.client.get( - f"/api/documents/{doc.pk}/notes/", - format="json", - ) - self.assertEqual(resp.content, b"Insufficient permissions to view notes") - self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - - assign_perm("view_document", user1, doc) - - resp = self.client.post( - f"/api/documents/{doc.pk}/notes/", - data={"note": "this is a posted note"}, - ) - self.assertEqual(resp.content, b"Insufficient permissions to create notes") - self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - - note = Note.objects.create( - note="This is a note.", - document=doc, - user=user2, - ) - - response = self.client.delete( - f"/api/documents/{doc.pk}/notes/?id={note.pk}", - format="json", - ) - - self.assertEqual(response.content, b"Insufficient permissions to delete notes") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_delete_note(self): - """ - GIVEN: - - Existing document, existing note - WHEN: - - API request is made to delete a note - THEN: - - note is deleted, document modified is updated - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have notes!", - created=timezone.now() - timedelta(days=1), - ) - # set to yesterday - doc.modified = timezone.now() - timedelta(days=1) - self.assertEqual(doc.modified.day, (timezone.now() - timedelta(days=1)).day) - note = Note.objects.create( - note="This is a note.", - document=doc, - user=self.user, - ) - - response = self.client.delete( - f"/api/documents/{doc.pk}/notes/?id={note.pk}", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.assertEqual(len(Note.objects.all()), 0) - doc = Document.objects.get(pk=doc.pk) - # modified was updated to today - self.assertEqual(doc.modified.day, timezone.now().day) - - def test_get_notes_no_doc(self): - """ - GIVEN: - - A request to get notes from a non-existent document - WHEN: - - API request for document notes is made - THEN: - - HTTP status.HTTP_404_NOT_FOUND is returned - """ - response = self.client.get( - "/api/documents/500/notes/", - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_tag_unique_name_and_owner(self): - """ - GIVEN: - - Multiple users - - Tags owned by particular users - WHEN: - - API request for creating items which are unique by name and owner - THEN: - - Unique items are created - - Non-unique items are not allowed - """ - user1 = User.objects.create_user(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="add_tag")) - user1.save() - - user2 = User.objects.create_user(username="test2") - user2.user_permissions.add(*Permission.objects.filter(codename="add_tag")) - user2.save() - - # User 1 creates tag 1 owned by user 1 by default - # No issue - self.client.force_authenticate(user1) - response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # User 2 creates tag 1 owned by user 2 by default - # No issue - self.client.force_authenticate(user2) - response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # User 2 creates tag 2 owned by user 1 - # No issue - self.client.force_authenticate(user2) - response = self.client.post( - "/api/tags/", - {"name": "tag 2", "owner": user1.pk}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # User 1 creates tag 2 owned by user 1 by default - # Not allowed, would create tag2/user1 which already exists - self.client.force_authenticate(user1) - response = self.client.post( - "/api/tags/", - {"name": "tag 2"}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # User 1 creates tag 2 owned by user 1 - # Not allowed, would create tag2/user1 which already exists - response = self.client.post( - "/api/tags/", - {"name": "tag 2", "owner": user1.pk}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_tag_unique_name_and_owner_enforced_on_update(self): - """ - GIVEN: - - Multiple users - - Tags owned by particular users - WHEN: - - API request for to update tag in such as way as makes it non-unqiue - THEN: - - Unique items are created - - Non-unique items are not allowed on update - """ - user1 = User.objects.create_user(username="test1") - user1.user_permissions.add(*Permission.objects.filter(codename="change_tag")) - user1.save() - - user2 = User.objects.create_user(username="test2") - user2.user_permissions.add(*Permission.objects.filter(codename="change_tag")) - user2.save() - - # Create name tag 1 owned by user 1 - # Create name tag 1 owned by user 2 - Tag.objects.create(name="tag 1", owner=user1) - tag2 = Tag.objects.create(name="tag 1", owner=user2) - - # User 2 attempts to change the owner of tag to user 1 - # Not allowed, would change to tag1/user1 which already exists - self.client.force_authenticate(user2) - response = self.client.patch( - f"/api/tags/{tag2.id}/", - {"owner": user1.pk}, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_share_links(self): - """ - GIVEN: - - Existing document - WHEN: - - API request is made to generate a share_link - - API request is made to view share_links on incorrect doc pk - - Invalid method request is made to view share_links doc - THEN: - - Link is created with a slug and associated with document - - 404 - - Error - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have notes added", - ) - # never expires - resp = self.client.post( - "/api/share_links/", - data={ - "document": doc.pk, - }, - ) - self.assertEqual(resp.status_code, status.HTTP_201_CREATED) - - resp = self.client.post( - "/api/share_links/", - data={ - "expiration": (timezone.now() + timedelta(days=7)).isoformat(), - "document": doc.pk, - "file_version": "original", - }, - ) - self.assertEqual(resp.status_code, status.HTTP_201_CREATED) - - response = self.client.get( - f"/api/documents/{doc.pk}/share_links/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertEqual(len(resp_data), 2) - - self.assertGreater(len(resp_data[1]["slug"]), 0) - self.assertIsNone(resp_data[1]["expiration"]) - self.assertEqual( - (parser.isoparse(resp_data[0]["expiration"]) - timezone.now()).days, - 6, - ) - - sl1 = ShareLink.objects.get(slug=resp_data[1]["slug"]) - self.assertEqual(str(sl1), f"Share Link for {doc.title}") - - response = self.client.post( - f"/api/documents/{doc.pk}/share_links/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - - response = self.client.get( - "/api/documents/99/share_links/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_share_links_permissions_aware(self): - """ - GIVEN: - - Existing document owned by user2 but with granted view perms for user1 - WHEN: - - API request is made by user1 to view share links - THEN: - - Links only shown if user has permissions - """ - user1 = User.objects.create_user(username="test1") - user1.user_permissions.add(*Permission.objects.all()) - user1.save() - - user2 = User.objects.create_user(username="test2") - user2.save() - - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document which will have share links added", - ) - doc.owner = user2 - doc.save() - - self.client.force_authenticate(user1) - - resp = self.client.get( - f"/api/documents/{doc.pk}/share_links/", - format="json", - ) - self.assertEqual(resp.content, b"Insufficient permissions to add share link") - self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) - - assign_perm("change_document", user1, doc) - - resp = self.client.get( - f"/api/documents/{doc.pk}/share_links/", - format="json", - ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - - def test_next_asn(self): - """ - GIVEN: - - Existing documents with ASNs, highest owned by user2 - WHEN: - - API request is made by user1 to get next ASN - THEN: - - ASN +1 from user2's doc is returned for user1 - """ - user1 = User.objects.create_user(username="test1") - user1.user_permissions.add(*Permission.objects.all()) - user1.save() - - user2 = User.objects.create_user(username="test2") - user2.save() - - doc1 = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document 1", - checksum="1", - archive_serial_number=998, - ) - doc1.owner = user1 - doc1.save() - - doc2 = Document.objects.create( - title="test2", - mime_type="application/pdf", - content="this is a document 2 with higher ASN", - checksum="2", - archive_serial_number=999, - ) - doc2.owner = user2 - doc2.save() - - self.client.force_authenticate(user1) - - resp = self.client.get( - "/api/documents/next_asn/", - ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - self.assertEqual(resp.content, b"1000") - - -class TestDocumentApiV2(DirectoriesMixin, APITestCase): - def setUp(self): - super().setUp() - - self.user = User.objects.create_superuser(username="temp_admin") - - self.client.force_authenticate(user=self.user) - self.client.defaults["HTTP_ACCEPT"] = "application/json; version=2" - - def test_tag_validate_color(self): - self.assertEqual( - self.client.post( - "/api/tags/", - {"name": "test", "color": "#12fFaA"}, - format="json", - ).status_code, - status.HTTP_201_CREATED, - ) - - self.assertEqual( - self.client.post( - "/api/tags/", - {"name": "test1", "color": "abcdef"}, - format="json", - ).status_code, - status.HTTP_400_BAD_REQUEST, - ) - self.assertEqual( - self.client.post( - "/api/tags/", - {"name": "test2", "color": "#abcdfg"}, - format="json", - ).status_code, - status.HTTP_400_BAD_REQUEST, - ) - self.assertEqual( - self.client.post( - "/api/tags/", - {"name": "test3", "color": "#asd"}, - format="json", - ).status_code, - status.HTTP_400_BAD_REQUEST, - ) - self.assertEqual( - self.client.post( - "/api/tags/", - {"name": "test4", "color": "#12121212"}, - format="json", - ).status_code, - status.HTTP_400_BAD_REQUEST, - ) - - def test_tag_text_color(self): - t = Tag.objects.create(name="tag1", color="#000000") - self.assertEqual( - self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], - "#ffffff", - ) - - t.color = "#ffffff" - t.save() - self.assertEqual( - self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], - "#000000", - ) - - t.color = "asdf" - t.save() - self.assertEqual( - self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], - "#000000", - ) - - t.color = "123" - t.save() - self.assertEqual( - self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], - "#000000", - ) - - -class TestApiUiSettings(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/ui_settings/" - - def setUp(self): - super().setUp() - self.test_user = User.objects.create_superuser(username="test") - self.test_user.first_name = "Test" - self.test_user.last_name = "User" - self.test_user.save() - self.client.force_authenticate(user=self.test_user) - - def test_api_get_ui_settings(self): - response = self.client.get(self.ENDPOINT, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data["user"], - { - "id": self.test_user.id, - "username": self.test_user.username, - "is_superuser": True, - "groups": [], - "first_name": self.test_user.first_name, - "last_name": self.test_user.last_name, - }, - ) - self.assertDictEqual( - response.data["settings"], - { - "update_checking": { - "backend_setting": "default", - }, - }, - ) - - def test_api_set_ui_settings(self): - settings = { - "settings": { - "dark_mode": { - "enabled": True, - }, - }, - } - - response = self.client.post( - self.ENDPOINT, - json.dumps(settings), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - ui_settings = self.test_user.ui_settings - self.assertDictEqual( - ui_settings.settings, - settings["settings"], - ) - - -class TestBulkEdit(DirectoriesMixin, APITestCase): - def setUp(self): - super().setUp() - - user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=user) - - patcher = mock.patch("documents.bulk_edit.bulk_update_documents.delay") - self.async_task = patcher.start() - self.addCleanup(patcher.stop) - self.c1 = Correspondent.objects.create(name="c1") - self.c2 = Correspondent.objects.create(name="c2") - self.dt1 = DocumentType.objects.create(name="dt1") - self.dt2 = DocumentType.objects.create(name="dt2") - self.t1 = Tag.objects.create(name="t1") - self.t2 = Tag.objects.create(name="t2") - self.doc1 = Document.objects.create(checksum="A", title="A") - self.doc2 = Document.objects.create( - checksum="B", - title="B", - correspondent=self.c1, - document_type=self.dt1, - ) - self.doc3 = Document.objects.create( - checksum="C", - title="C", - correspondent=self.c2, - document_type=self.dt2, - ) - self.doc4 = Document.objects.create(checksum="D", title="D") - self.doc5 = Document.objects.create(checksum="E", title="E") - self.doc2.tags.add(self.t1) - self.doc3.tags.add(self.t2) - self.doc4.tags.add(self.t1, self.t2) - self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}") - - def test_set_correspondent(self): - self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1) - bulk_edit.set_correspondent( - [self.doc1.id, self.doc2.id, self.doc3.id], - self.c2.id, - ) - self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3) - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id]) - - def test_unset_correspondent(self): - self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1) - bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None) - self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0) - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id]) - - def test_set_document_type(self): - self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1) - bulk_edit.set_document_type( - [self.doc1.id, self.doc2.id, self.doc3.id], - self.dt2.id, - ) - self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3) - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id]) - - def test_unset_document_type(self): - self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1) - bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None) - self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0) - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id]) - - def test_set_document_storage_path(self): - """ - GIVEN: - - 5 documents without defined storage path - WHEN: - - Bulk edit called to add storage path to 1 document - THEN: - - Single document storage path update - """ - self.assertEqual(Document.objects.filter(storage_path=None).count(), 5) - - bulk_edit.set_storage_path( - [self.doc1.id], - self.sp1.id, - ) - - self.assertEqual(Document.objects.filter(storage_path=None).count(), 4) - - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - - self.assertCountEqual(kwargs["document_ids"], [self.doc1.id]) - - def test_unset_document_storage_path(self): - """ - GIVEN: - - 4 documents without defined storage path - - 1 document with a defined storage - WHEN: - - Bulk edit called to remove storage path from 1 document - THEN: - - Single document storage path removed - """ - self.assertEqual(Document.objects.filter(storage_path=None).count(), 5) - - bulk_edit.set_storage_path( - [self.doc1.id], - self.sp1.id, - ) - - self.assertEqual(Document.objects.filter(storage_path=None).count(), 4) - - bulk_edit.set_storage_path( - [self.doc1.id], - None, - ) - - self.assertEqual(Document.objects.filter(storage_path=None).count(), 5) - - self.async_task.assert_called() - args, kwargs = self.async_task.call_args - - self.assertCountEqual(kwargs["document_ids"], [self.doc1.id]) - - def test_add_tag(self): - self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) - bulk_edit.add_tag( - [self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id], - self.t1.id, - ) - self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4) - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc3.id]) - - def test_remove_tag(self): - self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) - bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id) - self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1) - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - self.assertCountEqual(kwargs["document_ids"], [self.doc4.id]) - - def test_modify_tags(self): - tag_unrelated = Tag.objects.create(name="unrelated") - self.doc2.tags.add(tag_unrelated) - self.doc3.tags.add(tag_unrelated) - bulk_edit.modify_tags( - [self.doc2.id, self.doc3.id], - add_tags=[self.t2.id], - remove_tags=[self.t1.id], - ) - - self.assertCountEqual(list(self.doc2.tags.all()), [self.t2, tag_unrelated]) - self.assertCountEqual(list(self.doc3.tags.all()), [self.t2, tag_unrelated]) - - self.async_task.assert_called_once() - args, kwargs = self.async_task.call_args - # TODO: doc3 should not be affected, but the query for that is rather complicated - self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id]) - - def test_delete(self): - self.assertEqual(Document.objects.count(), 5) - bulk_edit.delete([self.doc1.id, self.doc2.id]) - self.assertEqual(Document.objects.count(), 3) - self.assertCountEqual( - [doc.id for doc in Document.objects.all()], - [self.doc3.id, self.doc4.id, self.doc5.id], - ) - - @mock.patch("documents.serialisers.bulk_edit.set_correspondent") - def test_api_set_correspondent(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_correspondent", - "parameters": {"correspondent": self.c1.id}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertEqual(kwargs["correspondent"], self.c1.id) - - @mock.patch("documents.serialisers.bulk_edit.set_correspondent") - def test_api_unset_correspondent(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_correspondent", - "parameters": {"correspondent": None}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertIsNone(kwargs["correspondent"]) - - @mock.patch("documents.serialisers.bulk_edit.set_document_type") - def test_api_set_type(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_document_type", - "parameters": {"document_type": self.dt1.id}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertEqual(kwargs["document_type"], self.dt1.id) - - @mock.patch("documents.serialisers.bulk_edit.set_document_type") - def test_api_unset_type(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_document_type", - "parameters": {"document_type": None}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertIsNone(kwargs["document_type"]) - - @mock.patch("documents.serialisers.bulk_edit.add_tag") - def test_api_add_tag(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "add_tag", - "parameters": {"tag": self.t1.id}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertEqual(kwargs["tag"], self.t1.id) - - @mock.patch("documents.serialisers.bulk_edit.remove_tag") - def test_api_remove_tag(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "remove_tag", - "parameters": {"tag": self.t1.id}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertEqual(kwargs["tag"], self.t1.id) - - @mock.patch("documents.serialisers.bulk_edit.modify_tags") - def test_api_modify_tags(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id, self.doc3.id], - "method": "modify_tags", - "parameters": { - "add_tags": [self.t1.id], - "remove_tags": [self.t2.id], - }, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertListEqual(args[0], [self.doc1.id, self.doc3.id]) - self.assertEqual(kwargs["add_tags"], [self.t1.id]) - self.assertEqual(kwargs["remove_tags"], [self.t2.id]) - - @mock.patch("documents.serialisers.bulk_edit.modify_tags") - def test_api_modify_tags_not_provided(self, m): - """ - GIVEN: - - API data to modify tags is missing modify_tags field - WHEN: - - API to edit tags is called - THEN: - - API returns HTTP 400 - - modify_tags is not called - """ - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id, self.doc3.id], - "method": "modify_tags", - "parameters": { - "add_tags": [self.t1.id], - }, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - m.assert_not_called() - - @mock.patch("documents.serialisers.bulk_edit.delete") - def test_api_delete(self, m): - m.return_value = "OK" - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - {"documents": [self.doc1.id], "method": "delete", "parameters": {}}, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - self.assertEqual(args[0], [self.doc1.id]) - self.assertEqual(len(kwargs), 0) - - @mock.patch("documents.serialisers.bulk_edit.set_storage_path") - def test_api_set_storage_path(self, m): - """ - GIVEN: - - API data to set the storage path of a document - WHEN: - - API is called - THEN: - - set_storage_path is called with correct document IDs and storage_path ID - """ - m.return_value = "OK" - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_storage_path", - "parameters": {"storage_path": self.sp1.id}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - - self.assertListEqual(args[0], [self.doc1.id]) - self.assertEqual(kwargs["storage_path"], self.sp1.id) - - @mock.patch("documents.serialisers.bulk_edit.set_storage_path") - def test_api_unset_storage_path(self, m): - """ - GIVEN: - - API data to clear/unset the storage path of a document - WHEN: - - API is called - THEN: - - set_storage_path is called with correct document IDs and None storage_path - """ - m.return_value = "OK" - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_storage_path", - "parameters": {"storage_path": None}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - args, kwargs = m.call_args - - self.assertListEqual(args[0], [self.doc1.id]) - self.assertEqual(kwargs["storage_path"], None) - - def test_api_invalid_storage_path(self): - """ - GIVEN: - - API data to set the storage path of a document - - Given storage_path ID isn't valid - WHEN: - - API is called - THEN: - - set_storage_path is called with correct document IDs and storage_path ID - """ - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_storage_path", - "parameters": {"storage_path": self.sp1.id + 10}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.async_task.assert_not_called() - - def test_api_set_storage_path_not_provided(self): - """ - GIVEN: - - API data to set the storage path of a document - - API data is missing storage path ID - WHEN: - - API is called - THEN: - - set_storage_path is called with correct document IDs and storage_path ID - """ - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id], - "method": "set_storage_path", - "parameters": {}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.async_task.assert_not_called() - - def test_api_invalid_doc(self): - self.assertEqual(Document.objects.count(), 5) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps({"documents": [-235], "method": "delete", "parameters": {}}), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Document.objects.count(), 5) - - def test_api_invalid_method(self): - self.assertEqual(Document.objects.count(), 5) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "exterminate", - "parameters": {}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Document.objects.count(), 5) - - def test_api_invalid_correspondent(self): - self.assertEqual(self.doc2.correspondent, self.c1) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "set_correspondent", - "parameters": {"correspondent": 345657}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - doc2 = Document.objects.get(id=self.doc2.id) - self.assertEqual(doc2.correspondent, self.c1) - - def test_api_no_correspondent(self): - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "set_correspondent", - "parameters": {}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_api_invalid_document_type(self): - self.assertEqual(self.doc2.document_type, self.dt1) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "set_document_type", - "parameters": {"document_type": 345657}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - doc2 = Document.objects.get(id=self.doc2.id) - self.assertEqual(doc2.document_type, self.dt1) - - def test_api_no_document_type(self): - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "set_document_type", - "parameters": {}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_api_add_invalid_tag(self): - self.assertEqual(list(self.doc2.tags.all()), [self.t1]) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "add_tag", - "parameters": {"tag": 345657}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - self.assertEqual(list(self.doc2.tags.all()), [self.t1]) - - def test_api_add_tag_no_tag(self): - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - {"documents": [self.doc2.id], "method": "add_tag", "parameters": {}}, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_api_delete_invalid_tag(self): - self.assertEqual(list(self.doc2.tags.all()), [self.t1]) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "remove_tag", - "parameters": {"tag": 345657}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - self.assertEqual(list(self.doc2.tags.all()), [self.t1]) - - def test_api_delete_tag_no_tag(self): - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - {"documents": [self.doc2.id], "method": "remove_tag", "parameters": {}}, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_api_modify_invalid_tags(self): - self.assertEqual(list(self.doc2.tags.all()), [self.t1]) - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "modify_tags", - "parameters": { - "add_tags": [self.t2.id, 1657], - "remove_tags": [1123123], - }, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_api_modify_tags_no_tags(self): - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "modify_tags", - "parameters": {"remove_tags": [1123123]}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id], - "method": "modify_tags", - "parameters": {"add_tags": [self.t2.id, 1657]}, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_api_selection_data_empty(self): - response = self.client.post( - "/api/documents/selection_data/", - json.dumps({"documents": []}), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - for field, Entity in [ - ("selected_correspondents", Correspondent), - ("selected_tags", Tag), - ("selected_document_types", DocumentType), - ]: - self.assertEqual(len(response.data[field]), Entity.objects.count()) - for correspondent in response.data[field]: - self.assertEqual(correspondent["document_count"], 0) - self.assertCountEqual( - map(lambda c: c["id"], response.data[field]), - map(lambda c: c["id"], Entity.objects.values("id")), - ) - - def test_api_selection_data(self): - response = self.client.post( - "/api/documents/selection_data/", - json.dumps( - {"documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id]}, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - self.assertCountEqual( - response.data["selected_correspondents"], - [ - {"id": self.c1.id, "document_count": 1}, - {"id": self.c2.id, "document_count": 0}, - ], - ) - self.assertCountEqual( - response.data["selected_tags"], - [ - {"id": self.t1.id, "document_count": 2}, - {"id": self.t2.id, "document_count": 1}, - ], - ) - self.assertCountEqual( - response.data["selected_document_types"], - [ - {"id": self.c1.id, "document_count": 1}, - {"id": self.c2.id, "document_count": 0}, - ], - ) - - @mock.patch("documents.serialisers.bulk_edit.set_permissions") - def test_set_permissions(self, m): - m.return_value = "OK" - user1 = User.objects.create(username="user1") - user2 = User.objects.create(username="user2") - permissions = { - "view": { - "users": [user1.id, user2.id], - "groups": None, - }, - "change": { - "users": [user1.id], - "groups": None, - }, - } - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id, self.doc3.id], - "method": "set_permissions", - "parameters": {"set_permissions": permissions}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - m.assert_called_once() - args, kwargs = m.call_args - self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id]) - self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2) - - @mock.patch("documents.serialisers.bulk_edit.set_permissions") - def test_insufficient_permissions_ownership(self, m): - """ - GIVEN: - - Documents owned by user other than logged in user - WHEN: - - set_permissions bulk edit API endpoint is called - THEN: - - User is not able to change permissions - """ - m.return_value = "OK" - self.doc1.owner = User.objects.get(username="temp_admin") - self.doc1.save() - user1 = User.objects.create(username="user1") - self.client.force_authenticate(user=user1) - - permissions = { - "owner": user1.id, - } - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id, self.doc2.id, self.doc3.id], - "method": "set_permissions", - "parameters": {"set_permissions": permissions}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - m.assert_not_called() - self.assertEqual(response.content, b"Insufficient permissions") - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc2.id, self.doc3.id], - "method": "set_permissions", - "parameters": {"set_permissions": permissions}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - m.assert_called_once() - - @mock.patch("documents.serialisers.bulk_edit.set_storage_path") - def test_insufficient_permissions_edit(self, m): - """ - GIVEN: - - Documents for which current user only has view permissions - WHEN: - - API is called - THEN: - - set_storage_path is only called if user can edit all docs - """ - m.return_value = "OK" - self.doc1.owner = User.objects.get(username="temp_admin") - self.doc1.save() - user1 = User.objects.create(username="user1") - assign_perm("view_document", user1, self.doc1) - self.client.force_authenticate(user=user1) - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id, self.doc2.id, self.doc3.id], - "method": "set_storage_path", - "parameters": {"storage_path": self.sp1.id}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - m.assert_not_called() - self.assertEqual(response.content, b"Insufficient permissions") - - assign_perm("change_document", user1, self.doc1) - - response = self.client.post( - "/api/documents/bulk_edit/", - json.dumps( - { - "documents": [self.doc1.id, self.doc2.id, self.doc3.id], - "method": "set_storage_path", - "parameters": {"storage_path": self.sp1.id}, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - m.assert_called_once() - - -class TestBulkDownload(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/documents/bulk_download/" - - def setUp(self): - super().setUp() - - user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=user) - - self.doc1 = Document.objects.create(title="unrelated", checksum="A") - self.doc2 = Document.objects.create( - title="document A", - filename="docA.pdf", - mime_type="application/pdf", - checksum="B", - created=timezone.make_aware(datetime.datetime(2021, 1, 1)), - ) - self.doc2b = Document.objects.create( - title="document A", - filename="docA2.pdf", - mime_type="application/pdf", - checksum="D", - created=timezone.make_aware(datetime.datetime(2021, 1, 1)), - ) - self.doc3 = Document.objects.create( - title="document B", - filename="docB.jpg", - mime_type="image/jpeg", - checksum="C", - created=timezone.make_aware(datetime.datetime(2020, 3, 21)), - archive_filename="docB.pdf", - archive_checksum="D", - ) - - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - self.doc2.source_path, - ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.png"), - self.doc2b.source_path, - ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"), - self.doc3.source_path, - ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"), - self.doc3.archive_path, - ) - - def test_download_originals(self): - response = self.client.post( - self.ENDPOINT, - json.dumps( - {"documents": [self.doc2.id, self.doc3.id], "content": "originals"}, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 2) - self.assertIn("2021-01-01 document A.pdf", zipf.namelist()) - self.assertIn("2020-03-21 document B.jpg", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) - - with self.doc3.source_file as f: - self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg")) - - def test_download_default(self): - response = self.client.post( - self.ENDPOINT, - json.dumps({"documents": [self.doc2.id, self.doc3.id]}), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 2) - self.assertIn("2021-01-01 document A.pdf", zipf.namelist()) - self.assertIn("2020-03-21 document B.pdf", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) - - with self.doc3.archive_file as f: - self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf")) - - def test_download_both(self): - response = self.client.post( - self.ENDPOINT, - json.dumps({"documents": [self.doc2.id, self.doc3.id], "content": "both"}), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 3) - self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist()) - self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist()) - self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual( - f.read(), - zipf.read("originals/2021-01-01 document A.pdf"), - ) - - with self.doc3.archive_file as f: - self.assertEqual( - f.read(), - zipf.read("archive/2020-03-21 document B.pdf"), - ) - - with self.doc3.source_file as f: - self.assertEqual( - f.read(), - zipf.read("originals/2020-03-21 document B.jpg"), - ) - - def test_filename_clashes(self): - response = self.client.post( - self.ENDPOINT, - json.dumps({"documents": [self.doc2.id, self.doc2b.id]}), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 2) - - self.assertIn("2021-01-01 document A.pdf", zipf.namelist()) - self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) - - with self.doc2b.source_file as f: - self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf")) - - def test_compression(self): - self.client.post( - self.ENDPOINT, - json.dumps( - {"documents": [self.doc2.id, self.doc2b.id], "compression": "lzma"}, - ), - content_type="application/json", - ) - - @override_settings(FILENAME_FORMAT="{correspondent}/{title}") - def test_formatted_download_originals(self): - """ - GIVEN: - - Defined file naming format - WHEN: - - Bulk download request for original documents - - Bulk download request requests to follow format - THEN: - - Files defined in resulting zipfile are formatted - """ - - c = Correspondent.objects.create(name="test") - c2 = Correspondent.objects.create(name="a space name") - - self.doc2.correspondent = c - self.doc2.title = "This is Doc 2" - self.doc2.save() - - self.doc3.correspondent = c2 - self.doc3.title = "Title 2 - Doc 3" - self.doc3.save() - - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "documents": [self.doc2.id, self.doc3.id], - "content": "originals", - "follow_formatting": True, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 2) - self.assertIn("a space name/Title 2 - Doc 3.jpg", zipf.namelist()) - self.assertIn("test/This is Doc 2.pdf", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual(f.read(), zipf.read("test/This is Doc 2.pdf")) - - with self.doc3.source_file as f: - self.assertEqual( - f.read(), - zipf.read("a space name/Title 2 - Doc 3.jpg"), - ) - - @override_settings(FILENAME_FORMAT="somewhere/{title}") - def test_formatted_download_archive(self): - """ - GIVEN: - - Defined file naming format - WHEN: - - Bulk download request for archive documents - - Bulk download request requests to follow format - THEN: - - Files defined in resulting zipfile are formatted - """ - - self.doc2.title = "This is Doc 2" - self.doc2.save() - - self.doc3.title = "Title 2 - Doc 3" - self.doc3.save() - print(self.doc3.archive_path) - print(self.doc3.archive_filename) - - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "documents": [self.doc2.id, self.doc3.id], - "follow_formatting": True, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 2) - self.assertIn("somewhere/This is Doc 2.pdf", zipf.namelist()) - self.assertIn("somewhere/Title 2 - Doc 3.pdf", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual(f.read(), zipf.read("somewhere/This is Doc 2.pdf")) - - with self.doc3.archive_file as f: - self.assertEqual(f.read(), zipf.read("somewhere/Title 2 - Doc 3.pdf")) - - @override_settings(FILENAME_FORMAT="{document_type}/{title}") - def test_formatted_download_both(self): - """ - GIVEN: - - Defined file naming format - WHEN: - - Bulk download request for original documents and archive documents - - Bulk download request requests to follow format - THEN: - - Files defined in resulting zipfile are formatted - """ - - dc1 = DocumentType.objects.create(name="bill") - dc2 = DocumentType.objects.create(name="statement") - - self.doc2.document_type = dc1 - self.doc2.title = "This is Doc 2" - self.doc2.save() - - self.doc3.document_type = dc2 - self.doc3.title = "Title 2 - Doc 3" - self.doc3.save() - - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "documents": [self.doc2.id, self.doc3.id], - "content": "both", - "follow_formatting": True, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response["Content-Type"], "application/zip") - - with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: - self.assertEqual(len(zipf.filelist), 3) - self.assertIn("originals/bill/This is Doc 2.pdf", zipf.namelist()) - self.assertIn("archive/statement/Title 2 - Doc 3.pdf", zipf.namelist()) - self.assertIn("originals/statement/Title 2 - Doc 3.jpg", zipf.namelist()) - - with self.doc2.source_file as f: - self.assertEqual( - f.read(), - zipf.read("originals/bill/This is Doc 2.pdf"), - ) - - with self.doc3.archive_file as f: - self.assertEqual( - f.read(), - zipf.read("archive/statement/Title 2 - Doc 3.pdf"), - ) - - with self.doc3.source_file as f: - self.assertEqual( - f.read(), - zipf.read("originals/statement/Title 2 - Doc 3.jpg"), - ) - - -class TestApiAuth(DirectoriesMixin, APITestCase): - def test_auth_required(self): - d = Document.objects.create(title="Test") - - self.assertEqual( - self.client.get("/api/documents/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - - self.assertEqual( - self.client.get(f"/api/documents/{d.id}/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get(f"/api/documents/{d.id}/download/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get(f"/api/documents/{d.id}/preview/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get(f"/api/documents/{d.id}/thumb/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - - self.assertEqual( - self.client.get("/api/tags/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get("/api/correspondents/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get("/api/document_types/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - - self.assertEqual( - self.client.get("/api/logs/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get("/api/saved_views/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - - self.assertEqual( - self.client.get("/api/search/autocomplete/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get("/api/documents/bulk_edit/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get("/api/documents/bulk_download/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - self.assertEqual( - self.client.get("/api/documents/selection_data/").status_code, - status.HTTP_401_UNAUTHORIZED, - ) - - def test_api_version_no_auth(self): - response = self.client.get("/api/") - self.assertNotIn("X-Api-Version", response) - self.assertNotIn("X-Version", response) - - def test_api_version_with_auth(self): - user = User.objects.create_superuser(username="test") - self.client.force_authenticate(user) - response = self.client.get("/api/") - self.assertIn("X-Api-Version", response) - self.assertIn("X-Version", response) - - def test_api_insufficient_permissions(self): - user = User.objects.create_user(username="test") - self.client.force_authenticate(user) - - Document.objects.create(title="Test") - - self.assertEqual( - self.client.get("/api/documents/").status_code, - status.HTTP_403_FORBIDDEN, - ) - - self.assertEqual( - self.client.get("/api/tags/").status_code, - status.HTTP_403_FORBIDDEN, - ) - self.assertEqual( - self.client.get("/api/correspondents/").status_code, - status.HTTP_403_FORBIDDEN, - ) - self.assertEqual( - self.client.get("/api/document_types/").status_code, - status.HTTP_403_FORBIDDEN, - ) - - self.assertEqual( - self.client.get("/api/logs/").status_code, - status.HTTP_403_FORBIDDEN, - ) - self.assertEqual( - self.client.get("/api/saved_views/").status_code, - status.HTTP_403_FORBIDDEN, - ) - - def test_api_sufficient_permissions(self): - user = User.objects.create_user(username="test") - user.user_permissions.add(*Permission.objects.all()) - self.client.force_authenticate(user) - - Document.objects.create(title="Test") - - self.assertEqual( - self.client.get("/api/documents/").status_code, - status.HTTP_200_OK, - ) - - self.assertEqual(self.client.get("/api/tags/").status_code, status.HTTP_200_OK) - self.assertEqual( - self.client.get("/api/correspondents/").status_code, - status.HTTP_200_OK, - ) - self.assertEqual( - self.client.get("/api/document_types/").status_code, - status.HTTP_200_OK, - ) - - self.assertEqual(self.client.get("/api/logs/").status_code, status.HTTP_200_OK) - self.assertEqual( - self.client.get("/api/saved_views/").status_code, - status.HTTP_200_OK, - ) - - def test_api_get_object_permissions(self): - user1 = User.objects.create_user(username="test1") - user2 = User.objects.create_user(username="test2") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - self.client.force_authenticate(user1) - - self.assertEqual( - self.client.get("/api/documents/").status_code, - status.HTTP_200_OK, - ) - - d = Document.objects.create(title="Test", content="the content 1", checksum="1") - - # no owner - self.assertEqual( - self.client.get(f"/api/documents/{d.id}/").status_code, - status.HTTP_200_OK, - ) - - d2 = Document.objects.create( - title="Test 2", - content="the content 2", - checksum="2", - owner=user2, - ) - - self.assertEqual( - self.client.get(f"/api/documents/{d2.id}/").status_code, - status.HTTP_404_NOT_FOUND, - ) - - def test_api_default_owner(self): - """ - GIVEN: - - API request to create an object (Tag) - WHEN: - - owner is not set at all - THEN: - - Object created with current user as owner - """ - user1 = User.objects.create_superuser(username="user1") - - self.client.force_authenticate(user1) - - response = self.client.post( - "/api/tags/", - json.dumps( - { - "name": "test1", - "matching_algorithm": MatchingModel.MATCH_AUTO, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - tag1 = Tag.objects.filter(name="test1").first() - self.assertEqual(tag1.owner, user1) - - def test_api_set_no_owner(self): - """ - GIVEN: - - API request to create an object (Tag) - WHEN: - - owner is passed as None - THEN: - - Object created with no owner - """ - user1 = User.objects.create_superuser(username="user1") - - self.client.force_authenticate(user1) - - response = self.client.post( - "/api/tags/", - json.dumps( - { - "name": "test1", - "matching_algorithm": MatchingModel.MATCH_AUTO, - "owner": None, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - tag1 = Tag.objects.filter(name="test1").first() - self.assertEqual(tag1.owner, None) - - def test_api_set_owner_w_permissions(self): - """ - GIVEN: - - API request to create an object (Tag) that supplies set_permissions object - WHEN: - - owner is passed as user id - - view > users is set & view > groups is set - THEN: - - Object permissions are set appropriately - """ - user1 = User.objects.create_superuser(username="user1") - user2 = User.objects.create(username="user2") - group1 = Group.objects.create(name="group1") - - self.client.force_authenticate(user1) - - response = self.client.post( - "/api/tags/", - json.dumps( - { - "name": "test1", - "matching_algorithm": MatchingModel.MATCH_AUTO, - "owner": user1.id, - "set_permissions": { - "view": { - "users": [user2.id], - "groups": [group1.id], - }, - "change": { - "users": None, - "groups": None, - }, - }, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - tag1 = Tag.objects.filter(name="test1").first() - - from guardian.core import ObjectPermissionChecker - - checker = ObjectPermissionChecker(user2) - self.assertEqual(checker.has_perm("view_tag", tag1), True) - self.assertIn("view_tag", get_perms(group1, tag1)) - - def test_api_set_other_owner_w_permissions(self): - """ - GIVEN: - - API request to create an object (Tag) - WHEN: - - a different owner than is logged in is set - - view > groups is set - THEN: - - Object permissions are set appropriately - """ - user1 = User.objects.create_superuser(username="user1") - user2 = User.objects.create(username="user2") - group1 = Group.objects.create(name="group1") - - self.client.force_authenticate(user1) - - response = self.client.post( - "/api/tags/", - json.dumps( - { - "name": "test1", - "matching_algorithm": MatchingModel.MATCH_AUTO, - "owner": user2.id, - "set_permissions": { - "view": { - "users": None, - "groups": [group1.id], - }, - "change": { - "users": None, - "groups": None, - }, - }, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - tag1 = Tag.objects.filter(name="test1").first() - - self.assertEqual(tag1.owner, user2) - self.assertIn("view_tag", get_perms(group1, tag1)) - - def test_api_set_doc_permissions(self): - """ - GIVEN: - - API request to update doc permissions and owner - WHEN: - - owner is set - - view > users is set & view > groups is set - THEN: - - Object permissions are set appropriately - """ - doc = Document.objects.create( - title="test", - mime_type="application/pdf", - content="this is a document", - ) - user1 = User.objects.create_superuser(username="user1") - user2 = User.objects.create(username="user2") - group1 = Group.objects.create(name="group1") - - self.client.force_authenticate(user1) - - response = self.client.patch( - f"/api/documents/{doc.id}/", - json.dumps( - { - "owner": user1.id, - "set_permissions": { - "view": { - "users": [user2.id], - "groups": [group1.id], - }, - "change": { - "users": None, - "groups": None, - }, - }, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - doc = Document.objects.get(pk=doc.id) - - self.assertEqual(doc.owner, user1) - from guardian.core import ObjectPermissionChecker - - checker = ObjectPermissionChecker(user2) - self.assertTrue(checker.has_perm("view_document", doc)) - self.assertIn("view_document", get_perms(group1, doc)) - - def test_dynamic_permissions_fields(self): - user1 = User.objects.create_user(username="user1") - user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) - user2 = User.objects.create_user(username="user2") - - Document.objects.create(title="Test", content="content 1", checksum="1") - doc2 = Document.objects.create( - title="Test2", - content="content 2", - checksum="2", - owner=user2, - ) - doc3 = Document.objects.create( - title="Test3", - content="content 3", - checksum="3", - owner=user2, - ) - - assign_perm("view_document", user1, doc2) - assign_perm("view_document", user1, doc3) - assign_perm("change_document", user1, doc3) - - self.client.force_authenticate(user1) - - response = self.client.get( - "/api/documents/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertNotIn("permissions", resp_data["results"][0]) - self.assertIn("user_can_change", resp_data["results"][0]) - self.assertEqual(resp_data["results"][0]["user_can_change"], True) # doc1 - self.assertEqual(resp_data["results"][1]["user_can_change"], False) # doc2 - self.assertEqual(resp_data["results"][2]["user_can_change"], True) # doc3 - - response = self.client.get( - "/api/documents/?full_perms=true", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - resp_data = response.json() - - self.assertIn("permissions", resp_data["results"][0]) - self.assertNotIn("user_can_change", resp_data["results"][0]) - - -class TestApiRemoteVersion(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/remote_version/" - - def setUp(self): - super().setUp() - - @mock.patch("urllib.request.urlopen") - def test_remote_version_enabled_no_update_prefix(self, urlopen_mock): - cm = MagicMock() - cm.getcode.return_value = status.HTTP_200_OK - cm.read.return_value = json.dumps({"tag_name": "ngx-1.6.0"}).encode() - cm.__enter__.return_value = cm - urlopen_mock.return_value = cm - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data, - { - "version": "1.6.0", - "update_available": False, - }, - ) - - @mock.patch("urllib.request.urlopen") - def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock): - cm = MagicMock() - cm.getcode.return_value = status.HTTP_200_OK - cm.read.return_value = json.dumps( - {"tag_name": version.__full_version_str__}, - ).encode() - cm.__enter__.return_value = cm - urlopen_mock.return_value = cm - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data, - { - "version": version.__full_version_str__, - "update_available": False, - }, - ) - - @mock.patch("urllib.request.urlopen") - def test_remote_version_enabled_update(self, urlopen_mock): - new_version = ( - version.__version__[0], - version.__version__[1], - version.__version__[2] + 1, - ) - new_version_str = ".".join(map(str, new_version)) - - cm = MagicMock() - cm.getcode.return_value = status.HTTP_200_OK - cm.read.return_value = json.dumps( - {"tag_name": new_version_str}, - ).encode() - cm.__enter__.return_value = cm - urlopen_mock.return_value = cm - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data, - { - "version": new_version_str, - "update_available": True, - }, - ) - - @mock.patch("urllib.request.urlopen") - def test_remote_version_bad_json(self, urlopen_mock): - cm = MagicMock() - cm.getcode.return_value = status.HTTP_200_OK - cm.read.return_value = b'{ "blah":' - cm.__enter__.return_value = cm - urlopen_mock.return_value = cm - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data, - { - "version": "0.0.0", - "update_available": False, - }, - ) - - @mock.patch("urllib.request.urlopen") - def test_remote_version_exception(self, urlopen_mock): - cm = MagicMock() - cm.getcode.return_value = status.HTTP_200_OK - cm.read.side_effect = urllib.error.URLError("an error") - cm.__enter__.return_value = cm - urlopen_mock.return_value = cm - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data, - { - "version": "0.0.0", - "update_available": False, - }, - ) - - -class TestApiObjects(DirectoriesMixin, APITestCase): - def setUp(self) -> None: - super().setUp() - - user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=user) - - self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True) - self.tag2 = Tag.objects.create(name="t2") - self.tag3 = Tag.objects.create(name="t3") - self.c1 = Correspondent.objects.create(name="c1") - self.c2 = Correspondent.objects.create(name="c2") - self.c3 = Correspondent.objects.create(name="c3") - self.dt1 = DocumentType.objects.create(name="dt1") - self.dt2 = DocumentType.objects.create(name="dt2") - self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{title}") - self.sp2 = StoragePath.objects.create(name="sp2", path="Something2/{title}") - - def test_object_filters(self): - response = self.client.get( - f"/api/tags/?id={self.tag2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - - response = self.client.get( - f"/api/tags/?id__in={self.tag1.id},{self.tag3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - - response = self.client.get( - f"/api/correspondents/?id={self.c2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - - response = self.client.get( - f"/api/correspondents/?id__in={self.c1.id},{self.c3.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - - response = self.client.get( - f"/api/document_types/?id={self.dt1.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - - response = self.client.get( - f"/api/document_types/?id__in={self.dt1.id},{self.dt2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - - response = self.client.get( - f"/api/storage_paths/?id={self.sp1.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 1) - - response = self.client.get( - f"/api/storage_paths/?id__in={self.sp1.id},{self.sp2.id}", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - results = response.data["results"] - self.assertEqual(len(results), 2) - - -class TestApiStoragePaths(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/storage_paths/" - - def setUp(self) -> None: - super().setUp() - - user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=user) - - self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}") - - def test_api_get_storage_path(self): - """ - GIVEN: - - API request to get all storage paths - WHEN: - - API is called - THEN: - - Existing storage paths are returned - """ - response = self.client.get(self.ENDPOINT, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) - - resp_storage_path = response.data["results"][0] - self.assertEqual(resp_storage_path["id"], self.sp1.id) - self.assertEqual(resp_storage_path["path"], self.sp1.path) - - def test_api_create_storage_path(self): - """ - GIVEN: - - API request to create a storage paths - WHEN: - - API is called - THEN: - - Correct HTTP response - - New storage path is created - """ - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "A storage path", - "path": "Somewhere/{asn}", - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(StoragePath.objects.count(), 2) - - def test_api_create_invalid_storage_path(self): - """ - GIVEN: - - API request to create a storage paths - - Storage path format is incorrect - WHEN: - - API is called - THEN: - - Correct HTTP 400 response - - No storage path is created - """ - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Another storage path", - "path": "Somewhere/{correspdent}", - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(StoragePath.objects.count(), 1) - - def test_api_storage_path_placeholders(self): - """ - GIVEN: - - API request to create a storage path with placeholders - - Storage path is valid - WHEN: - - API is called - THEN: - - Correct HTTP response - - New storage path is created - """ - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Storage path with placeholders", - "path": "{title}/{correspondent}/{document_type}/{created}/{created_year}" - "/{created_year_short}/{created_month}/{created_month_name}" - "/{created_month_name_short}/{created_day}/{added}/{added_year}" - "/{added_year_short}/{added_month}/{added_month_name}" - "/{added_month_name_short}/{added_day}/{asn}/{tags}" - "/{tag_list}/{owner_username}/{original_name}/{doc_pk}/", - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(StoragePath.objects.count(), 2) - - @mock.patch("documents.bulk_edit.bulk_update_documents.delay") - def test_api_update_storage_path(self, bulk_update_mock): - """ - GIVEN: - - API request to get all storage paths - WHEN: - - API is called - THEN: - - Existing storage paths are returned - """ - document = Document.objects.create( - mime_type="application/pdf", - storage_path=self.sp1, - ) - response = self.client.patch( - f"{self.ENDPOINT}{self.sp1.pk}/", - data={ - "path": "somewhere/{created} - {title}", - }, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - bulk_update_mock.assert_called_once() - - args, _ = bulk_update_mock.call_args - - self.assertCountEqual([document.pk], args[0]) - - -class TestTasks(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/tasks/" - ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/" - - def setUp(self): - super().setUp() - - self.user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=self.user) - - def test_get_tasks(self): - """ - GIVEN: - - Attempted celery tasks - WHEN: - - API call is made to get tasks - THEN: - - Attempting and pending tasks are serialized and provided - """ - - task1 = PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_one.pdf", - ) - - task2 = PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_two.pdf", - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 2) - returned_task1 = response.data[1] - returned_task2 = response.data[0] - - self.assertEqual(returned_task1["task_id"], task1.task_id) - self.assertEqual(returned_task1["status"], celery.states.PENDING) - self.assertEqual(returned_task1["task_file_name"], task1.task_file_name) - - self.assertEqual(returned_task2["task_id"], task2.task_id) - self.assertEqual(returned_task2["status"], celery.states.PENDING) - self.assertEqual(returned_task2["task_file_name"], task2.task_file_name) - - def test_get_single_task_status(self): - """ - GIVEN - - Query parameter for a valid task ID - WHEN: - - API call is made to get task status - THEN: - - Single task data is returned - """ - - id1 = str(uuid.uuid4()) - task1 = PaperlessTask.objects.create( - task_id=id1, - task_file_name="task_one.pdf", - ) - - _ = PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_two.pdf", - ) - - response = self.client.get(self.ENDPOINT + f"?task_id={id1}") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - returned_task1 = response.data[0] - - self.assertEqual(returned_task1["task_id"], task1.task_id) - - def test_get_single_task_status_not_valid(self): - """ - GIVEN - - Query parameter for a non-existent task ID - WHEN: - - API call is made to get task status - THEN: - - No task data is returned - """ - PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_one.pdf", - ) - - _ = PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_two.pdf", - ) - - response = self.client.get(self.ENDPOINT + "?task_id=bad-task-id") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 0) - - def test_acknowledge_tasks(self): - """ - GIVEN: - - Attempted celery tasks - WHEN: - - API call is made to get mark task as acknowledged - THEN: - - Task is marked as acknowledged - """ - task = PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_one.pdf", - ) - - response = self.client.get(self.ENDPOINT) - self.assertEqual(len(response.data), 1) - - response = self.client.post( - self.ENDPOINT_ACKNOWLEDGE, - {"tasks": [task.id]}, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.get(self.ENDPOINT) - self.assertEqual(len(response.data), 0) - - def test_task_result_no_error(self): - """ - GIVEN: - - A celery task completed without error - WHEN: - - API call is made to get tasks - THEN: - - The returned data includes the task result - """ - PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_one.pdf", - status=celery.states.SUCCESS, - result="Success. New document id 1 created", - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - returned_data = response.data[0] - - self.assertEqual(returned_data["result"], "Success. New document id 1 created") - self.assertEqual(returned_data["related_document"], "1") - - def test_task_result_with_error(self): - """ - GIVEN: - - A celery task completed with an exception - WHEN: - - API call is made to get tasks - THEN: - - The returned result is the exception info - """ - PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="task_one.pdf", - status=celery.states.FAILURE, - result="test.pdf: Not consuming test.pdf: It is a duplicate.", - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - returned_data = response.data[0] - - self.assertEqual( - returned_data["result"], - "test.pdf: Not consuming test.pdf: It is a duplicate.", - ) - - def test_task_name_webui(self): - """ - GIVEN: - - Attempted celery task - - Task was created through the webui - WHEN: - - API call is made to get tasks - THEN: - - Returned data include the filename - """ - PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="test.pdf", - task_name="documents.tasks.some_task", - status=celery.states.SUCCESS, - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - returned_data = response.data[0] - - self.assertEqual(returned_data["task_file_name"], "test.pdf") - - def test_task_name_consume_folder(self): - """ - GIVEN: - - Attempted celery task - - Task was created through the consume folder - WHEN: - - API call is made to get tasks - THEN: - - Returned data include the filename - """ - PaperlessTask.objects.create( - task_id=str(uuid.uuid4()), - task_file_name="anothertest.pdf", - task_name="documents.tasks.some_task", - status=celery.states.SUCCESS, - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - returned_data = response.data[0] - - self.assertEqual(returned_data["task_file_name"], "anothertest.pdf") - - -class TestApiUser(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/users/" - - def setUp(self): - super().setUp() - - self.user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=self.user) - - def test_get_users(self): - """ - GIVEN: - - Configured users - WHEN: - - API call is made to get users - THEN: - - Configured users are provided - """ - - user1 = User.objects.create( - username="testuser", - password="test", - first_name="Test", - last_name="User", - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 2) - returned_user2 = response.data["results"][1] - - self.assertEqual(returned_user2["username"], user1.username) - self.assertEqual(returned_user2["password"], "**********") - self.assertEqual(returned_user2["first_name"], user1.first_name) - self.assertEqual(returned_user2["last_name"], user1.last_name) - - def test_create_user(self): - """ - WHEN: - - API request is made to add a user account - THEN: - - A new user account is created - """ - - user1 = { - "username": "testuser", - "password": "test", - "first_name": "Test", - "last_name": "User", - } - - response = self.client.post( - self.ENDPOINT, - data=user1, - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - returned_user1 = User.objects.get(username="testuser") - - self.assertEqual(returned_user1.username, user1["username"]) - self.assertEqual(returned_user1.first_name, user1["first_name"]) - self.assertEqual(returned_user1.last_name, user1["last_name"]) - - def test_delete_user(self): - """ - GIVEN: - - Existing user account - WHEN: - - API request is made to delete a user account - THEN: - - Account is deleted - """ - - user1 = User.objects.create( - username="testuser", - password="test", - first_name="Test", - last_name="User", - ) - - nUsers = User.objects.count() - - response = self.client.delete( - f"{self.ENDPOINT}{user1.pk}/", - ) - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - self.assertEqual(User.objects.count(), nUsers - 1) - - def test_update_user(self): - """ - GIVEN: - - Existing user accounts - WHEN: - - API request is made to update user account - THEN: - - The user account is updated, password only updated if not '****' - """ - - user1 = User.objects.create( - username="testuser", - password="test", - first_name="Test", - last_name="User", - ) - - initial_password = user1.password - - response = self.client.patch( - f"{self.ENDPOINT}{user1.pk}/", - data={ - "first_name": "Updated Name 1", - "password": "******", - }, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - returned_user1 = User.objects.get(pk=user1.pk) - self.assertEqual(returned_user1.first_name, "Updated Name 1") - self.assertEqual(returned_user1.password, initial_password) - - response = self.client.patch( - f"{self.ENDPOINT}{user1.pk}/", - data={ - "first_name": "Updated Name 2", - "password": "123xyz", - }, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - returned_user2 = User.objects.get(pk=user1.pk) - self.assertEqual(returned_user2.first_name, "Updated Name 2") - self.assertNotEqual(returned_user2.password, initial_password) - - -class TestApiGroup(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/groups/" - - def setUp(self): - super().setUp() - - self.user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=self.user) - - def test_get_groups(self): - """ - GIVEN: - - Configured groups - WHEN: - - API call is made to get groups - THEN: - - Configured groups are provided - """ - - group1 = Group.objects.create( - name="Test Group", - ) - - response = self.client.get(self.ENDPOINT) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) - returned_group1 = response.data["results"][0] - - self.assertEqual(returned_group1["name"], group1.name) - - def test_create_group(self): - """ - WHEN: - - API request is made to add a group - THEN: - - A new group is created - """ - - group1 = { - "name": "Test Group", - } - - response = self.client.post( - self.ENDPOINT, - data=group1, - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - returned_group1 = Group.objects.get(name="Test Group") - - self.assertEqual(returned_group1.name, group1["name"]) - - def test_delete_group(self): - """ - GIVEN: - - Existing group - WHEN: - - API request is made to delete a group - THEN: - - Group is deleted - """ - - group1 = Group.objects.create( - name="Test Group", - ) - - response = self.client.delete( - f"{self.ENDPOINT}{group1.pk}/", - ) - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - self.assertEqual(len(Group.objects.all()), 0) - - def test_update_group(self): - """ - GIVEN: - - Existing groups - WHEN: - - API request is made to update group - THEN: - - The group is updated - """ - - group1 = Group.objects.create( - name="Test Group", - ) - - response = self.client.patch( - f"{self.ENDPOINT}{group1.pk}/", - data={ - "name": "Updated Name 1", - }, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - returned_group1 = Group.objects.get(pk=group1.pk) - self.assertEqual(returned_group1.name, "Updated Name 1") - - -class TestBulkEditObjectPermissions(APITestCase): - def setUp(self): - super().setUp() - - user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=user) - - self.t1 = Tag.objects.create(name="t1") - self.t2 = Tag.objects.create(name="t2") - self.c1 = Correspondent.objects.create(name="c1") - self.dt1 = DocumentType.objects.create(name="dt1") - self.sp1 = StoragePath.objects.create(name="sp1") - self.user1 = User.objects.create(username="user1") - self.user2 = User.objects.create(username="user2") - self.user3 = User.objects.create(username="user3") - - def test_bulk_object_set_permissions(self): - """ - GIVEN: - - Existing objects - WHEN: - - bulk_edit_object_perms API endpoint is called - THEN: - - Permissions and / or owner are changed - """ - permissions = { - "view": { - "users": [self.user1.id, self.user2.id], - "groups": [], - }, - "change": { - "users": [self.user1.id], - "groups": [], - }, - } - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.t1.id, self.t2.id], - "object_type": "tags", - "permissions": permissions, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn(self.user1, get_users_with_perms(self.t1)) - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.c1.id], - "object_type": "correspondents", - "permissions": permissions, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn(self.user1, get_users_with_perms(self.c1)) - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.dt1.id], - "object_type": "document_types", - "permissions": permissions, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn(self.user1, get_users_with_perms(self.dt1)) - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.sp1.id], - "object_type": "storage_paths", - "permissions": permissions, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn(self.user1, get_users_with_perms(self.sp1)) - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.t1.id, self.t2.id], - "object_type": "tags", - "owner": self.user3.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3) - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.sp1.id], - "object_type": "storage_paths", - "owner": self.user3.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3) - - def test_bulk_edit_object_permissions_insufficient_perms(self): - """ - GIVEN: - - Objects owned by user other than logged in user - WHEN: - - bulk_edit_object_perms API endpoint is called - THEN: - - User is not able to change permissions - """ - self.t1.owner = User.objects.get(username="temp_admin") - self.t1.save() - self.client.force_authenticate(user=self.user1) - - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.t1.id, self.t2.id], - "object_type": "tags", - "owner": self.user1.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.content, b"Insufficient permissions") - - def test_bulk_edit_object_permissions_validation(self): - """ - GIVEN: - - Existing objects - WHEN: - - bulk_edit_object_perms API endpoint is called with invalid params - THEN: - - Validation fails - """ - # not a list - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": self.t1.id, - "object_type": "tags", - "owner": self.user1.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # not a list of ints - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": ["one"], - "object_type": "tags", - "owner": self.user1.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # duplicates - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [self.t1.id, self.t2.id, self.t1.id], - "object_type": "tags", - "owner": self.user1.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # not a valid object type - response = self.client.post( - "/api/bulk_edit_object_perms/", - json.dumps( - { - "objects": [1], - "object_type": "madeup", - "owner": self.user1.id, - }, - ), - content_type="application/json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - -class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase): - ENDPOINT = "/api/consumption_templates/" - - def setUp(self) -> None: - super().setUp() - - user = User.objects.create_superuser(username="temp_admin") - self.client.force_authenticate(user=user) - self.user2 = User.objects.create(username="user2") - self.user3 = User.objects.create(username="user3") - self.group1 = Group.objects.create(name="group1") - - self.c = Correspondent.objects.create(name="Correspondent Name") - self.c2 = Correspondent.objects.create(name="Correspondent Name 2") - self.dt = DocumentType.objects.create(name="DocType Name") - self.t1 = Tag.objects.create(name="t1") - self.t2 = Tag.objects.create(name="t2") - self.t3 = Tag.objects.create(name="t3") - self.sp = StoragePath.objects.create(path="/test/") - self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string") - self.cf2 = CustomField.objects.create( - name="Custom Field 2", - data_type="integer", - ) - - self.ct = ConsumptionTemplate.objects.create( - name="Template 1", - order=0, - sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}", - filter_filename="*simple*", - filter_path="*/samples/*", - assign_title="Doc from {correspondent}", - assign_correspondent=self.c, - assign_document_type=self.dt, - assign_storage_path=self.sp, - assign_owner=self.user2, - ) - self.ct.assign_tags.add(self.t1) - self.ct.assign_tags.add(self.t2) - self.ct.assign_tags.add(self.t3) - self.ct.assign_view_users.add(self.user3.pk) - self.ct.assign_view_groups.add(self.group1.pk) - self.ct.assign_change_users.add(self.user3.pk) - self.ct.assign_change_groups.add(self.group1.pk) - self.ct.assign_custom_fields.add(self.cf1.pk) - self.ct.assign_custom_fields.add(self.cf2.pk) - self.ct.save() - - def test_api_get_consumption_template(self): - """ - GIVEN: - - API request to get all consumption template - WHEN: - - API is called - THEN: - - Existing consumption templates are returned - """ - response = self.client.get(self.ENDPOINT, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) - - resp_consumption_template = response.data["results"][0] - self.assertEqual(resp_consumption_template["id"], self.ct.id) - self.assertEqual( - resp_consumption_template["assign_correspondent"], - self.ct.assign_correspondent.pk, - ) - - def test_api_create_consumption_template(self): - """ - GIVEN: - - API request to create a consumption template - WHEN: - - API is called - THEN: - - Correct HTTP response - - New template is created - """ - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Template 2", - "order": 1, - "sources": [DocumentSource.ApiUpload], - "filter_filename": "*test*", - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(ConsumptionTemplate.objects.count(), 2) - - def test_api_create_invalid_consumption_template(self): - """ - GIVEN: - - API request to create a consumption template - - Neither file name nor path filter are specified - WHEN: - - API is called - THEN: - - Correct HTTP 400 response - - No template is created - """ - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Template 2", - "order": 1, - "sources": [DocumentSource.ApiUpload], - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(ConsumptionTemplate.objects.count(), 1) - - def test_api_create_consumption_template_empty_fields(self): - """ - GIVEN: - - API request to create a consumption template - - Path or filename filter or assign title are empty string - WHEN: - - API is called - THEN: - - Template is created but filter or title assignment is not set if "" - """ - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Template 2", - "order": 1, - "sources": [DocumentSource.ApiUpload], - "filter_filename": "*test*", - "filter_path": "", - "assign_title": "", - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - ct = ConsumptionTemplate.objects.get(name="Template 2") - self.assertEqual(ct.filter_filename, "*test*") - self.assertIsNone(ct.filter_path) - self.assertIsNone(ct.assign_title) - - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Template 3", - "order": 1, - "sources": [DocumentSource.ApiUpload], - "filter_filename": "", - "filter_path": "*/test/*", - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - ct2 = ConsumptionTemplate.objects.get(name="Template 3") - self.assertEqual(ct2.filter_path, "*/test/*") - self.assertIsNone(ct2.filter_filename) - - def test_api_create_consumption_template_with_mailrule(self): - """ - GIVEN: - - API request to create a consumption template with a mail rule but no MailFetch source - WHEN: - - API is called - THEN: - - Correct HTTP response - - New template is created with MailFetch as source - """ - account1 = MailAccount.objects.create( - name="Email1", - username="username1", - password="password1", - imap_server="server.example.com", - imap_port=443, - imap_security=MailAccount.ImapSecurity.SSL, - character_set="UTF-8", - ) - rule1 = MailRule.objects.create( - name="Rule1", - account=account1, - folder="INBOX", - filter_from="from@example.com", - filter_to="someone@somewhere.com", - filter_subject="subject", - filter_body="body", - filter_attachment_filename_include="file.pdf", - maximum_age=30, - action=MailRule.MailAction.MARK_READ, - assign_title_from=MailRule.TitleSource.FROM_SUBJECT, - assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING, - order=0, - attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY, - ) - response = self.client.post( - self.ENDPOINT, - json.dumps( - { - "name": "Template 2", - "order": 1, - "sources": [DocumentSource.ApiUpload], - "filter_mailrule": rule1.pk, - }, - ), - content_type="application/json", - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(ConsumptionTemplate.objects.count(), 2) - ct = ConsumptionTemplate.objects.get(name="Template 2") - self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()]) diff --git a/src/documents/tests/test_api_bulk_download.py b/src/documents/tests/test_api_bulk_download.py new file mode 100644 index 000000000..57912c65c --- /dev/null +++ b/src/documents/tests/test_api_bulk_download.py @@ -0,0 +1,337 @@ +import datetime +import io +import json +import os +import shutil +import zipfile + +from django.contrib.auth.models import User +from django.test import override_settings +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import Correspondent +from documents.models import Document +from documents.models import DocumentType +from documents.tests.utils import DirectoriesMixin + + +class TestBulkDownload(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/documents/bulk_download/" + + def setUp(self): + super().setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=user) + + self.doc1 = Document.objects.create(title="unrelated", checksum="A") + self.doc2 = Document.objects.create( + title="document A", + filename="docA.pdf", + mime_type="application/pdf", + checksum="B", + created=timezone.make_aware(datetime.datetime(2021, 1, 1)), + ) + self.doc2b = Document.objects.create( + title="document A", + filename="docA2.pdf", + mime_type="application/pdf", + checksum="D", + created=timezone.make_aware(datetime.datetime(2021, 1, 1)), + ) + self.doc3 = Document.objects.create( + title="document B", + filename="docB.jpg", + mime_type="image/jpeg", + checksum="C", + created=timezone.make_aware(datetime.datetime(2020, 3, 21)), + archive_filename="docB.pdf", + archive_checksum="D", + ) + + shutil.copy( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + self.doc2.source_path, + ) + shutil.copy( + os.path.join(os.path.dirname(__file__), "samples", "simple.png"), + self.doc2b.source_path, + ) + shutil.copy( + os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"), + self.doc3.source_path, + ) + shutil.copy( + os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"), + self.doc3.archive_path, + ) + + def test_download_originals(self): + response = self.client.post( + self.ENDPOINT, + json.dumps( + {"documents": [self.doc2.id, self.doc3.id], "content": "originals"}, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 2) + self.assertIn("2021-01-01 document A.pdf", zipf.namelist()) + self.assertIn("2020-03-21 document B.jpg", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) + + with self.doc3.source_file as f: + self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg")) + + def test_download_default(self): + response = self.client.post( + self.ENDPOINT, + json.dumps({"documents": [self.doc2.id, self.doc3.id]}), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 2) + self.assertIn("2021-01-01 document A.pdf", zipf.namelist()) + self.assertIn("2020-03-21 document B.pdf", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) + + with self.doc3.archive_file as f: + self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf")) + + def test_download_both(self): + response = self.client.post( + self.ENDPOINT, + json.dumps({"documents": [self.doc2.id, self.doc3.id], "content": "both"}), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 3) + self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist()) + self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist()) + self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual( + f.read(), + zipf.read("originals/2021-01-01 document A.pdf"), + ) + + with self.doc3.archive_file as f: + self.assertEqual( + f.read(), + zipf.read("archive/2020-03-21 document B.pdf"), + ) + + with self.doc3.source_file as f: + self.assertEqual( + f.read(), + zipf.read("originals/2020-03-21 document B.jpg"), + ) + + def test_filename_clashes(self): + response = self.client.post( + self.ENDPOINT, + json.dumps({"documents": [self.doc2.id, self.doc2b.id]}), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 2) + + self.assertIn("2021-01-01 document A.pdf", zipf.namelist()) + self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) + + with self.doc2b.source_file as f: + self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf")) + + def test_compression(self): + self.client.post( + self.ENDPOINT, + json.dumps( + {"documents": [self.doc2.id, self.doc2b.id], "compression": "lzma"}, + ), + content_type="application/json", + ) + + @override_settings(FILENAME_FORMAT="{correspondent}/{title}") + def test_formatted_download_originals(self): + """ + GIVEN: + - Defined file naming format + WHEN: + - Bulk download request for original documents + - Bulk download request requests to follow format + THEN: + - Files in resulting zipfile are formatted + """ + + c = Correspondent.objects.create(name="test") + c2 = Correspondent.objects.create(name="a space name") + + self.doc2.correspondent = c + self.doc2.title = "This is Doc 2" + self.doc2.save() + + self.doc3.correspondent = c2 + self.doc3.title = "Title 2 - Doc 3" + self.doc3.save() + + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "documents": [self.doc2.id, self.doc3.id], + "content": "originals", + "follow_formatting": True, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 2) + self.assertIn("a space name/Title 2 - Doc 3.jpg", zipf.namelist()) + self.assertIn("test/This is Doc 2.pdf", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual(f.read(), zipf.read("test/This is Doc 2.pdf")) + + with self.doc3.source_file as f: + self.assertEqual( + f.read(), + zipf.read("a space name/Title 2 - Doc 3.jpg"), + ) + + @override_settings(FILENAME_FORMAT="somewhere/{title}") + def test_formatted_download_archive(self): + """ + GIVEN: + - Defined file naming format + WHEN: + - Bulk download request for archive documents + - Bulk download request requests to follow format + THEN: + - Files in resulting zipfile are formatted + """ + + self.doc2.title = "This is Doc 2" + self.doc2.save() + + self.doc3.title = "Title 2 - Doc 3" + self.doc3.save() + print(self.doc3.archive_path) + print(self.doc3.archive_filename) + + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "documents": [self.doc2.id, self.doc3.id], + "follow_formatting": True, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 2) + self.assertIn("somewhere/This is Doc 2.pdf", zipf.namelist()) + self.assertIn("somewhere/Title 2 - Doc 3.pdf", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual(f.read(), zipf.read("somewhere/This is Doc 2.pdf")) + + with self.doc3.archive_file as f: + self.assertEqual(f.read(), zipf.read("somewhere/Title 2 - Doc 3.pdf")) + + @override_settings(FILENAME_FORMAT="{document_type}/{title}") + def test_formatted_download_both(self): + """ + GIVEN: + - Defined file naming format + WHEN: + - Bulk download request for original documents and archive documents + - Bulk download request requests to follow format + THEN: + - Files defined in resulting zipfile are formatted + """ + + dc1 = DocumentType.objects.create(name="bill") + dc2 = DocumentType.objects.create(name="statement") + + self.doc2.document_type = dc1 + self.doc2.title = "This is Doc 2" + self.doc2.save() + + self.doc3.document_type = dc2 + self.doc3.title = "Title 2 - Doc 3" + self.doc3.save() + + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "documents": [self.doc2.id, self.doc3.id], + "content": "both", + "follow_formatting": True, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(response.content)) as zipf: + self.assertEqual(len(zipf.filelist), 3) + self.assertIn("originals/bill/This is Doc 2.pdf", zipf.namelist()) + self.assertIn("archive/statement/Title 2 - Doc 3.pdf", zipf.namelist()) + self.assertIn("originals/statement/Title 2 - Doc 3.jpg", zipf.namelist()) + + with self.doc2.source_file as f: + self.assertEqual( + f.read(), + zipf.read("originals/bill/This is Doc 2.pdf"), + ) + + with self.doc3.archive_file as f: + self.assertEqual( + f.read(), + zipf.read("archive/statement/Title 2 - Doc 3.pdf"), + ) + + with self.doc3.source_file as f: + self.assertEqual( + f.read(), + zipf.read("originals/statement/Title 2 - Doc 3.jpg"), + ) diff --git a/src/documents/tests/test_api_bulk_edit.py b/src/documents/tests/test_api_bulk_edit.py new file mode 100644 index 000000000..c2dc69a1e --- /dev/null +++ b/src/documents/tests/test_api_bulk_edit.py @@ -0,0 +1,870 @@ +import json +from unittest import mock + +from django.contrib.auth.models import User +from guardian.shortcuts import assign_perm +from rest_framework import status +from rest_framework.test import APITestCase + +from documents import bulk_edit +from documents.models import Correspondent +from documents.models import Document +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from documents.tests.utils import DirectoriesMixin + + +class TestBulkEdit(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=user) + + patcher = mock.patch("documents.bulk_edit.bulk_update_documents.delay") + self.async_task = patcher.start() + self.addCleanup(patcher.stop) + self.c1 = Correspondent.objects.create(name="c1") + self.c2 = Correspondent.objects.create(name="c2") + self.dt1 = DocumentType.objects.create(name="dt1") + self.dt2 = DocumentType.objects.create(name="dt2") + self.t1 = Tag.objects.create(name="t1") + self.t2 = Tag.objects.create(name="t2") + self.doc1 = Document.objects.create(checksum="A", title="A") + self.doc2 = Document.objects.create( + checksum="B", + title="B", + correspondent=self.c1, + document_type=self.dt1, + ) + self.doc3 = Document.objects.create( + checksum="C", + title="C", + correspondent=self.c2, + document_type=self.dt2, + ) + self.doc4 = Document.objects.create(checksum="D", title="D") + self.doc5 = Document.objects.create(checksum="E", title="E") + self.doc2.tags.add(self.t1) + self.doc3.tags.add(self.t2) + self.doc4.tags.add(self.t1, self.t2) + self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}") + + def test_set_correspondent(self): + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1) + bulk_edit.set_correspondent( + [self.doc1.id, self.doc2.id, self.doc3.id], + self.c2.id, + ) + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 3) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id]) + + def test_unset_correspondent(self): + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 1) + bulk_edit.set_correspondent([self.doc1.id, self.doc2.id, self.doc3.id], None) + self.assertEqual(Document.objects.filter(correspondent=self.c2).count(), 0) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id]) + + def test_set_document_type(self): + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1) + bulk_edit.set_document_type( + [self.doc1.id, self.doc2.id, self.doc3.id], + self.dt2.id, + ) + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 3) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id]) + + def test_unset_document_type(self): + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 1) + bulk_edit.set_document_type([self.doc1.id, self.doc2.id, self.doc3.id], None) + self.assertEqual(Document.objects.filter(document_type=self.dt2).count(), 0) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id]) + + def test_set_document_storage_path(self): + """ + GIVEN: + - 5 documents without defined storage path + WHEN: + - Bulk edit called to add storage path to 1 document + THEN: + - Single document storage path update + """ + self.assertEqual(Document.objects.filter(storage_path=None).count(), 5) + + bulk_edit.set_storage_path( + [self.doc1.id], + self.sp1.id, + ) + + self.assertEqual(Document.objects.filter(storage_path=None).count(), 4) + + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + + self.assertCountEqual(kwargs["document_ids"], [self.doc1.id]) + + def test_unset_document_storage_path(self): + """ + GIVEN: + - 4 documents without defined storage path + - 1 document with a defined storage + WHEN: + - Bulk edit called to remove storage path from 1 document + THEN: + - Single document storage path removed + """ + self.assertEqual(Document.objects.filter(storage_path=None).count(), 5) + + bulk_edit.set_storage_path( + [self.doc1.id], + self.sp1.id, + ) + + self.assertEqual(Document.objects.filter(storage_path=None).count(), 4) + + bulk_edit.set_storage_path( + [self.doc1.id], + None, + ) + + self.assertEqual(Document.objects.filter(storage_path=None).count(), 5) + + self.async_task.assert_called() + args, kwargs = self.async_task.call_args + + self.assertCountEqual(kwargs["document_ids"], [self.doc1.id]) + + def test_add_tag(self): + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) + bulk_edit.add_tag( + [self.doc1.id, self.doc2.id, self.doc3.id, self.doc4.id], + self.t1.id, + ) + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 4) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc3.id]) + + def test_remove_tag(self): + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) + bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id) + self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 1) + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + self.assertCountEqual(kwargs["document_ids"], [self.doc4.id]) + + def test_modify_tags(self): + tag_unrelated = Tag.objects.create(name="unrelated") + self.doc2.tags.add(tag_unrelated) + self.doc3.tags.add(tag_unrelated) + bulk_edit.modify_tags( + [self.doc2.id, self.doc3.id], + add_tags=[self.t2.id], + remove_tags=[self.t1.id], + ) + + self.assertCountEqual(list(self.doc2.tags.all()), [self.t2, tag_unrelated]) + self.assertCountEqual(list(self.doc3.tags.all()), [self.t2, tag_unrelated]) + + self.async_task.assert_called_once() + args, kwargs = self.async_task.call_args + # TODO: doc3 should not be affected, but the query for that is rather complicated + self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id]) + + def test_delete(self): + self.assertEqual(Document.objects.count(), 5) + bulk_edit.delete([self.doc1.id, self.doc2.id]) + self.assertEqual(Document.objects.count(), 3) + self.assertCountEqual( + [doc.id for doc in Document.objects.all()], + [self.doc3.id, self.doc4.id, self.doc5.id], + ) + + @mock.patch("documents.serialisers.bulk_edit.set_correspondent") + def test_api_set_correspondent(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_correspondent", + "parameters": {"correspondent": self.c1.id}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertEqual(kwargs["correspondent"], self.c1.id) + + @mock.patch("documents.serialisers.bulk_edit.set_correspondent") + def test_api_unset_correspondent(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_correspondent", + "parameters": {"correspondent": None}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertIsNone(kwargs["correspondent"]) + + @mock.patch("documents.serialisers.bulk_edit.set_document_type") + def test_api_set_type(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_document_type", + "parameters": {"document_type": self.dt1.id}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertEqual(kwargs["document_type"], self.dt1.id) + + @mock.patch("documents.serialisers.bulk_edit.set_document_type") + def test_api_unset_type(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_document_type", + "parameters": {"document_type": None}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertIsNone(kwargs["document_type"]) + + @mock.patch("documents.serialisers.bulk_edit.add_tag") + def test_api_add_tag(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "add_tag", + "parameters": {"tag": self.t1.id}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertEqual(kwargs["tag"], self.t1.id) + + @mock.patch("documents.serialisers.bulk_edit.remove_tag") + def test_api_remove_tag(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "remove_tag", + "parameters": {"tag": self.t1.id}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertEqual(kwargs["tag"], self.t1.id) + + @mock.patch("documents.serialisers.bulk_edit.modify_tags") + def test_api_modify_tags(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id, self.doc3.id], + "method": "modify_tags", + "parameters": { + "add_tags": [self.t1.id], + "remove_tags": [self.t2.id], + }, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertListEqual(args[0], [self.doc1.id, self.doc3.id]) + self.assertEqual(kwargs["add_tags"], [self.t1.id]) + self.assertEqual(kwargs["remove_tags"], [self.t2.id]) + + @mock.patch("documents.serialisers.bulk_edit.modify_tags") + def test_api_modify_tags_not_provided(self, m): + """ + GIVEN: + - API data to modify tags is missing modify_tags field + WHEN: + - API to edit tags is called + THEN: + - API returns HTTP 400 + - modify_tags is not called + """ + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id, self.doc3.id], + "method": "modify_tags", + "parameters": { + "add_tags": [self.t1.id], + }, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + m.assert_not_called() + + @mock.patch("documents.serialisers.bulk_edit.delete") + def test_api_delete(self, m): + m.return_value = "OK" + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + {"documents": [self.doc1.id], "method": "delete", "parameters": {}}, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + self.assertEqual(args[0], [self.doc1.id]) + self.assertEqual(len(kwargs), 0) + + @mock.patch("documents.serialisers.bulk_edit.set_storage_path") + def test_api_set_storage_path(self, m): + """ + GIVEN: + - API data to set the storage path of a document + WHEN: + - API is called + THEN: + - set_storage_path is called with correct document IDs and storage_path ID + """ + m.return_value = "OK" + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_storage_path", + "parameters": {"storage_path": self.sp1.id}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + + self.assertListEqual(args[0], [self.doc1.id]) + self.assertEqual(kwargs["storage_path"], self.sp1.id) + + @mock.patch("documents.serialisers.bulk_edit.set_storage_path") + def test_api_unset_storage_path(self, m): + """ + GIVEN: + - API data to clear/unset the storage path of a document + WHEN: + - API is called + THEN: + - set_storage_path is called with correct document IDs and None storage_path + """ + m.return_value = "OK" + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_storage_path", + "parameters": {"storage_path": None}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + args, kwargs = m.call_args + + self.assertListEqual(args[0], [self.doc1.id]) + self.assertEqual(kwargs["storage_path"], None) + + def test_api_invalid_storage_path(self): + """ + GIVEN: + - API data to set the storage path of a document + - Given storage_path ID isn't valid + WHEN: + - API is called + THEN: + - set_storage_path is called with correct document IDs and storage_path ID + """ + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_storage_path", + "parameters": {"storage_path": self.sp1.id + 10}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.async_task.assert_not_called() + + def test_api_set_storage_path_not_provided(self): + """ + GIVEN: + - API data to set the storage path of a document + - API data is missing storage path ID + WHEN: + - API is called + THEN: + - set_storage_path is called with correct document IDs and storage_path ID + """ + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id], + "method": "set_storage_path", + "parameters": {}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.async_task.assert_not_called() + + def test_api_invalid_doc(self): + self.assertEqual(Document.objects.count(), 5) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps({"documents": [-235], "method": "delete", "parameters": {}}), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Document.objects.count(), 5) + + def test_api_invalid_method(self): + self.assertEqual(Document.objects.count(), 5) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "exterminate", + "parameters": {}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Document.objects.count(), 5) + + def test_api_invalid_correspondent(self): + self.assertEqual(self.doc2.correspondent, self.c1) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "set_correspondent", + "parameters": {"correspondent": 345657}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + doc2 = Document.objects.get(id=self.doc2.id) + self.assertEqual(doc2.correspondent, self.c1) + + def test_api_no_correspondent(self): + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "set_correspondent", + "parameters": {}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_api_invalid_document_type(self): + self.assertEqual(self.doc2.document_type, self.dt1) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "set_document_type", + "parameters": {"document_type": 345657}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + doc2 = Document.objects.get(id=self.doc2.id) + self.assertEqual(doc2.document_type, self.dt1) + + def test_api_no_document_type(self): + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "set_document_type", + "parameters": {}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_api_add_invalid_tag(self): + self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "add_tag", + "parameters": {"tag": 345657}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + + def test_api_add_tag_no_tag(self): + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + {"documents": [self.doc2.id], "method": "add_tag", "parameters": {}}, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_api_delete_invalid_tag(self): + self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "remove_tag", + "parameters": {"tag": 345657}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + + def test_api_delete_tag_no_tag(self): + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + {"documents": [self.doc2.id], "method": "remove_tag", "parameters": {}}, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_api_modify_invalid_tags(self): + self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "modify_tags", + "parameters": { + "add_tags": [self.t2.id, 1657], + "remove_tags": [1123123], + }, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_api_modify_tags_no_tags(self): + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "modify_tags", + "parameters": {"remove_tags": [1123123]}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "modify_tags", + "parameters": {"add_tags": [self.t2.id, 1657]}, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_api_selection_data_empty(self): + response = self.client.post( + "/api/documents/selection_data/", + json.dumps({"documents": []}), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + for field, Entity in [ + ("selected_correspondents", Correspondent), + ("selected_tags", Tag), + ("selected_document_types", DocumentType), + ]: + self.assertEqual(len(response.data[field]), Entity.objects.count()) + for correspondent in response.data[field]: + self.assertEqual(correspondent["document_count"], 0) + self.assertCountEqual( + map(lambda c: c["id"], response.data[field]), + map(lambda c: c["id"], Entity.objects.values("id")), + ) + + def test_api_selection_data(self): + response = self.client.post( + "/api/documents/selection_data/", + json.dumps( + {"documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id]}, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertCountEqual( + response.data["selected_correspondents"], + [ + {"id": self.c1.id, "document_count": 1}, + {"id": self.c2.id, "document_count": 0}, + ], + ) + self.assertCountEqual( + response.data["selected_tags"], + [ + {"id": self.t1.id, "document_count": 2}, + {"id": self.t2.id, "document_count": 1}, + ], + ) + self.assertCountEqual( + response.data["selected_document_types"], + [ + {"id": self.c1.id, "document_count": 1}, + {"id": self.c2.id, "document_count": 0}, + ], + ) + + @mock.patch("documents.serialisers.bulk_edit.set_permissions") + def test_set_permissions(self, m): + m.return_value = "OK" + user1 = User.objects.create(username="user1") + user2 = User.objects.create(username="user2") + permissions = { + "view": { + "users": [user1.id, user2.id], + "groups": None, + }, + "change": { + "users": [user1.id], + "groups": None, + }, + } + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id, self.doc3.id], + "method": "set_permissions", + "parameters": {"set_permissions": permissions}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + m.assert_called_once() + args, kwargs = m.call_args + self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id]) + self.assertEqual(len(kwargs["set_permissions"]["view"]["users"]), 2) + + @mock.patch("documents.serialisers.bulk_edit.set_permissions") + def test_insufficient_permissions_ownership(self, m): + """ + GIVEN: + - Documents owned by user other than logged in user + WHEN: + - set_permissions bulk edit API endpoint is called + THEN: + - User is not able to change permissions + """ + m.return_value = "OK" + self.doc1.owner = User.objects.get(username="temp_admin") + self.doc1.save() + user1 = User.objects.create(username="user1") + self.client.force_authenticate(user=user1) + + permissions = { + "owner": user1.id, + } + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id, self.doc2.id, self.doc3.id], + "method": "set_permissions", + "parameters": {"set_permissions": permissions}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + m.assert_not_called() + self.assertEqual(response.content, b"Insufficient permissions") + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id, self.doc3.id], + "method": "set_permissions", + "parameters": {"set_permissions": permissions}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + m.assert_called_once() + + @mock.patch("documents.serialisers.bulk_edit.set_storage_path") + def test_insufficient_permissions_edit(self, m): + """ + GIVEN: + - Documents for which current user only has view permissions + WHEN: + - API is called + THEN: + - set_storage_path only called if user can edit all docs + """ + m.return_value = "OK" + self.doc1.owner = User.objects.get(username="temp_admin") + self.doc1.save() + user1 = User.objects.create(username="user1") + assign_perm("view_document", user1, self.doc1) + self.client.force_authenticate(user=user1) + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id, self.doc2.id, self.doc3.id], + "method": "set_storage_path", + "parameters": {"storage_path": self.sp1.id}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + m.assert_not_called() + self.assertEqual(response.content, b"Insufficient permissions") + + assign_perm("change_document", user1, self.doc1) + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc1.id, self.doc2.id, self.doc3.id], + "method": "set_storage_path", + "parameters": {"storage_path": self.sp1.id}, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + m.assert_called_once() diff --git a/src/documents/tests/test_api_consumption_templates.py b/src/documents/tests/test_api_consumption_templates.py new file mode 100644 index 000000000..e32294050 --- /dev/null +++ b/src/documents/tests/test_api_consumption_templates.py @@ -0,0 +1,236 @@ +import json + +from django.contrib.auth.models import Group +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.data_models import DocumentSource +from documents.models import ConsumptionTemplate +from documents.models import Correspondent +from documents.models import CustomField +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from documents.tests.utils import DirectoriesMixin +from paperless_mail.models import MailAccount +from paperless_mail.models import MailRule + + +class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/consumption_templates/" + + def setUp(self) -> None: + super().setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=user) + self.user2 = User.objects.create(username="user2") + self.user3 = User.objects.create(username="user3") + self.group1 = Group.objects.create(name="group1") + + self.c = Correspondent.objects.create(name="Correspondent Name") + self.c2 = Correspondent.objects.create(name="Correspondent Name 2") + self.dt = DocumentType.objects.create(name="DocType Name") + self.t1 = Tag.objects.create(name="t1") + self.t2 = Tag.objects.create(name="t2") + self.t3 = Tag.objects.create(name="t3") + self.sp = StoragePath.objects.create(path="/test/") + self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string") + self.cf2 = CustomField.objects.create( + name="Custom Field 2", + data_type="integer", + ) + + self.ct = ConsumptionTemplate.objects.create( + name="Template 1", + order=0, + sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}", + filter_filename="*simple*", + filter_path="*/samples/*", + assign_title="Doc from {correspondent}", + assign_correspondent=self.c, + assign_document_type=self.dt, + assign_storage_path=self.sp, + assign_owner=self.user2, + ) + self.ct.assign_tags.add(self.t1) + self.ct.assign_tags.add(self.t2) + self.ct.assign_tags.add(self.t3) + self.ct.assign_view_users.add(self.user3.pk) + self.ct.assign_view_groups.add(self.group1.pk) + self.ct.assign_change_users.add(self.user3.pk) + self.ct.assign_change_groups.add(self.group1.pk) + self.ct.assign_custom_fields.add(self.cf1.pk) + self.ct.assign_custom_fields.add(self.cf2.pk) + self.ct.save() + + def test_api_get_consumption_template(self): + """ + GIVEN: + - API request to get all consumption template + WHEN: + - API is called + THEN: + - Existing consumption templates are returned + """ + response = self.client.get(self.ENDPOINT, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + resp_consumption_template = response.data["results"][0] + self.assertEqual(resp_consumption_template["id"], self.ct.id) + self.assertEqual( + resp_consumption_template["assign_correspondent"], + self.ct.assign_correspondent.pk, + ) + + def test_api_create_consumption_template(self): + """ + GIVEN: + - API request to create a consumption template + WHEN: + - API is called + THEN: + - Correct HTTP response + - New template is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Template 2", + "order": 1, + "sources": [DocumentSource.ApiUpload], + "filter_filename": "*test*", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(ConsumptionTemplate.objects.count(), 2) + + def test_api_create_invalid_consumption_template(self): + """ + GIVEN: + - API request to create a consumption template + - Neither file name nor path filter are specified + WHEN: + - API is called + THEN: + - Correct HTTP 400 response + - No template is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Template 2", + "order": 1, + "sources": [DocumentSource.ApiUpload], + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(ConsumptionTemplate.objects.count(), 1) + + def test_api_create_consumption_template_empty_fields(self): + """ + GIVEN: + - API request to create a consumption template + - Path or filename filter or assign title are empty string + WHEN: + - API is called + THEN: + - Template is created but filter or title assignment is not set if "" + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Template 2", + "order": 1, + "sources": [DocumentSource.ApiUpload], + "filter_filename": "*test*", + "filter_path": "", + "assign_title": "", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + ct = ConsumptionTemplate.objects.get(name="Template 2") + self.assertEqual(ct.filter_filename, "*test*") + self.assertIsNone(ct.filter_path) + self.assertIsNone(ct.assign_title) + + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Template 3", + "order": 1, + "sources": [DocumentSource.ApiUpload], + "filter_filename": "", + "filter_path": "*/test/*", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + ct2 = ConsumptionTemplate.objects.get(name="Template 3") + self.assertEqual(ct2.filter_path, "*/test/*") + self.assertIsNone(ct2.filter_filename) + + def test_api_create_consumption_template_with_mailrule(self): + """ + GIVEN: + - API request to create a consumption template with a mail rule but no MailFetch source + WHEN: + - API is called + THEN: + - New template is created with MailFetch as source + """ + account1 = MailAccount.objects.create( + name="Email1", + username="username1", + password="password1", + imap_server="server.example.com", + imap_port=443, + imap_security=MailAccount.ImapSecurity.SSL, + character_set="UTF-8", + ) + rule1 = MailRule.objects.create( + name="Rule1", + account=account1, + folder="INBOX", + filter_from="from@example.com", + filter_to="someone@somewhere.com", + filter_subject="subject", + filter_body="body", + filter_attachment_filename_include="file.pdf", + maximum_age=30, + action=MailRule.MailAction.MARK_READ, + assign_title_from=MailRule.TitleSource.FROM_SUBJECT, + assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING, + order=0, + attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY, + ) + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Template 2", + "order": 1, + "sources": [DocumentSource.ApiUpload], + "filter_mailrule": rule1.pk, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(ConsumptionTemplate.objects.count(), 2) + ct = ConsumptionTemplate.objects.get(name="Template 2") + self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()]) diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py new file mode 100644 index 000000000..779d02134 --- /dev/null +++ b/src/documents/tests/test_api_documents.py @@ -0,0 +1,1992 @@ +import datetime +import os +import shutil +import tempfile +import uuid +import zoneinfo +from datetime import timedelta +from pathlib import Path +from unittest import mock + +import celery +from dateutil import parser +from django.conf import settings +from django.contrib.auth.models import Permission +from django.contrib.auth.models import User +from django.test import override_settings +from django.utils import timezone +from guardian.shortcuts import assign_perm +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import Correspondent +from documents.models import CustomField +from documents.models import CustomFieldInstance +from documents.models import Document +from documents.models import DocumentType +from documents.models import MatchingModel +from documents.models import Note +from documents.models import SavedView +from documents.models import ShareLink +from documents.models import StoragePath +from documents.models import Tag +from documents.tests.utils import DirectoriesMixin +from documents.tests.utils import DocumentConsumeDelayMixin + + +class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=self.user) + + def testDocuments(self): + response = self.client.get("/api/documents/").data + + self.assertEqual(response["count"], 0) + + c = Correspondent.objects.create(name="c", pk=41) + dt = DocumentType.objects.create(name="dt", pk=63) + tag = Tag.objects.create(name="t", pk=85) + + doc = Document.objects.create( + title="WOW", + content="the content", + correspondent=c, + document_type=dt, + checksum="123", + mime_type="application/pdf", + ) + + doc.tags.add(tag) + + response = self.client.get("/api/documents/", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + returned_doc = response.data["results"][0] + self.assertEqual(returned_doc["id"], doc.id) + self.assertEqual(returned_doc["title"], doc.title) + self.assertEqual(returned_doc["correspondent"], c.id) + self.assertEqual(returned_doc["document_type"], dt.id) + self.assertListEqual(returned_doc["tags"], [tag.id]) + + c2 = Correspondent.objects.create(name="c2") + + returned_doc["correspondent"] = c2.pk + returned_doc["title"] = "the new title" + + response = self.client.put( + f"/api/documents/{doc.pk}/", + returned_doc, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + doc_after_save = Document.objects.get(id=doc.id) + + self.assertEqual(doc_after_save.correspondent, c2) + self.assertEqual(doc_after_save.title, "the new title") + + self.client.delete(f"/api/documents/{doc_after_save.pk}/") + + self.assertEqual(len(Document.objects.all()), 0) + + def test_document_fields(self): + c = Correspondent.objects.create(name="c", pk=41) + dt = DocumentType.objects.create(name="dt", pk=63) + Tag.objects.create(name="t", pk=85) + storage_path = StoragePath.objects.create(name="sp", pk=77, path="p") + Document.objects.create( + title="WOW", + content="the content", + correspondent=c, + document_type=dt, + checksum="123", + mime_type="application/pdf", + storage_path=storage_path, + ) + + response = self.client.get("/api/documents/", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results_full = response.data["results"] + self.assertIn("content", results_full[0]) + self.assertIn("id", results_full[0]) + + response = self.client.get("/api/documents/?fields=id", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertFalse("content" in results[0]) + self.assertIn("id", results[0]) + self.assertEqual(len(results[0]), 1) + + response = self.client.get("/api/documents/?fields=content", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertIn("content", results[0]) + self.assertFalse("id" in results[0]) + self.assertEqual(len(results[0]), 1) + + response = self.client.get("/api/documents/?fields=id,content", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertIn("content", results[0]) + self.assertIn("id", results[0]) + self.assertEqual(len(results[0]), 2) + + response = self.client.get( + "/api/documents/?fields=id,conteasdnt", + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertFalse("content" in results[0]) + self.assertIn("id", results[0]) + self.assertEqual(len(results[0]), 1) + + response = self.client.get("/api/documents/?fields=", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results_full[0]), len(results[0])) + + response = self.client.get("/api/documents/?fields=dgfhs", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results[0]), 0) + + def test_document_actions(self): + _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) + + content = b"This is a test" + content_thumbnail = b"thumbnail content" + + with open(filename, "wb") as f: + f.write(content) + + doc = Document.objects.create( + title="none", + filename=os.path.basename(filename), + mime_type="application/pdf", + ) + + with open( + os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"), + "wb", + ) as f: + f.write(content_thumbnail) + + response = self.client.get(f"/api/documents/{doc.pk}/download/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content) + + response = self.client.get(f"/api/documents/{doc.pk}/preview/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content) + + response = self.client.get(f"/api/documents/{doc.pk}/thumb/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content_thumbnail) + + def test_document_actions_with_perms(self): + """ + GIVEN: + - Document with owner and without granted permissions + - User is then granted permissions + WHEN: + - User tries to load preview, thumbnail + THEN: + - Initially, HTTP 403 Forbidden + - With permissions, HTTP 200 OK + """ + _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) + + content = b"This is a test" + content_thumbnail = b"thumbnail content" + + with open(filename, "wb") as f: + f.write(content) + + user1 = User.objects.create_user(username="test1") + user2 = User.objects.create_user(username="test2") + user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + user2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + + self.client.force_authenticate(user2) + + doc = Document.objects.create( + title="none", + filename=os.path.basename(filename), + mime_type="application/pdf", + owner=user1, + ) + + with open( + os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"), + "wb", + ) as f: + f.write(content_thumbnail) + + response = self.client.get(f"/api/documents/{doc.pk}/download/") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.get(f"/api/documents/{doc.pk}/preview/") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.get(f"/api/documents/{doc.pk}/thumb/") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + assign_perm("view_document", user2, doc) + + response = self.client.get(f"/api/documents/{doc.pk}/download/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.get(f"/api/documents/{doc.pk}/preview/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.get(f"/api/documents/{doc.pk}/thumb/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + @override_settings(FILENAME_FORMAT="") + def test_download_with_archive(self): + content = b"This is a test" + content_archive = b"This is the same test but archived" + + doc = Document.objects.create( + title="none", + filename="my_document.pdf", + archive_filename="archived.pdf", + mime_type="application/pdf", + ) + + with open(doc.source_path, "wb") as f: + f.write(content) + + with open(doc.archive_path, "wb") as f: + f.write(content_archive) + + response = self.client.get(f"/api/documents/{doc.pk}/download/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content_archive) + + response = self.client.get( + f"/api/documents/{doc.pk}/download/?original=true", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content) + + response = self.client.get(f"/api/documents/{doc.pk}/preview/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content_archive) + + response = self.client.get( + f"/api/documents/{doc.pk}/preview/?original=true", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.content, content) + + def test_document_actions_not_existing_file(self): + doc = Document.objects.create( + title="none", + filename=os.path.basename("asd"), + mime_type="application/pdf", + ) + + response = self.client.get(f"/api/documents/{doc.pk}/download/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + response = self.client.get(f"/api/documents/{doc.pk}/preview/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + response = self.client.get(f"/api/documents/{doc.pk}/thumb/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_document_filters(self): + doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + ) + doc2 = Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + ) + doc3 = Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + ) + + tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True) + tag_2 = Tag.objects.create(name="t2") + tag_3 = Tag.objects.create(name="t3") + + cf1 = CustomField.objects.create( + name="stringfield", + data_type=CustomField.FieldDataType.STRING, + ) + cf2 = CustomField.objects.create( + name="numberfield", + data_type=CustomField.FieldDataType.INT, + ) + + doc1.tags.add(tag_inbox) + doc2.tags.add(tag_2) + doc3.tags.add(tag_2) + doc3.tags.add(tag_3) + + cf1_d1 = CustomFieldInstance.objects.create( + document=doc1, + field=cf1, + value_text="foobard1", + ) + CustomFieldInstance.objects.create( + document=doc1, + field=cf2, + value_int=999, + ) + cf1_d3 = CustomFieldInstance.objects.create( + document=doc3, + field=cf1, + value_text="foobard3", + ) + + response = self.client.get("/api/documents/?is_in_inbox=true") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc1.id) + + response = self.client.get("/api/documents/?is_in_inbox=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id]) + + response = self.client.get( + f"/api/documents/?tags__id__in={tag_inbox.id},{tag_3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc3.id]) + + response = self.client.get( + f"/api/documents/?tags__id__in={tag_2.id},{tag_3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc2.id, doc3.id]) + + response = self.client.get( + f"/api/documents/?tags__id__all={tag_2.id},{tag_3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc3.id) + + response = self.client.get( + f"/api/documents/?tags__id__all={tag_inbox.id},{tag_3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 0) + + response = self.client.get( + f"/api/documents/?tags__id__all={tag_inbox.id}a{tag_3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + + response = self.client.get(f"/api/documents/?tags__id__none={tag_3.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual([results[0]["id"], results[1]["id"]], [doc1.id, doc2.id]) + + response = self.client.get( + f"/api/documents/?tags__id__none={tag_3.id},{tag_2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc1.id) + + response = self.client.get( + f"/api/documents/?tags__id__none={tag_2.id},{tag_inbox.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 0) + + response = self.client.get( + f"/api/documents/?id__in={doc1.id},{doc2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + + response = self.client.get( + f"/api/documents/?id__range={doc1.id},{doc3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + + response = self.client.get( + f"/api/documents/?id={doc2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + + # custom field name + response = self.client.get( + f"/api/documents/?custom_fields__icontains={cf1.name}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + + # custom field value + response = self.client.get( + f"/api/documents/?custom_fields__icontains={cf1_d1.value}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc1.id) + + response = self.client.get( + f"/api/documents/?custom_fields__icontains={cf1_d3.value}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc3.id) + + def test_document_checksum_filter(self): + Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + ) + doc2 = Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + ) + Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + ) + + response = self.client.get("/api/documents/?checksum__iexact=B") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc2.id) + + response = self.client.get("/api/documents/?checksum__iexact=X") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 0) + + def test_document_original_filename_filter(self): + doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + original_filename="docA.pdf", + ) + doc2 = Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + original_filename="docB.pdf", + ) + doc3 = Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + original_filename="docC.pdf", + ) + + response = self.client.get("/api/documents/?original_filename__iexact=DOCa.pdf") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], doc1.id) + + response = self.client.get("/api/documents/?original_filename__iexact=docx.pdf") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 0) + + response = self.client.get("/api/documents/?original_filename__istartswith=dOc") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertCountEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [doc1.id, doc2.id, doc3.id], + ) + + def test_documents_title_content_filter(self): + doc1 = Document.objects.create( + title="title A", + content="content A", + checksum="A", + mime_type="application/pdf", + ) + doc2 = Document.objects.create( + title="title B", + content="content A", + checksum="B", + mime_type="application/pdf", + ) + doc3 = Document.objects.create( + title="title A", + content="content B", + checksum="C", + mime_type="application/pdf", + ) + doc4 = Document.objects.create( + title="title B", + content="content B", + checksum="D", + mime_type="application/pdf", + ) + + response = self.client.get("/api/documents/?title_content=A") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertCountEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [doc1.id, doc2.id, doc3.id], + ) + + response = self.client.get("/api/documents/?title_content=B") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertCountEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [doc2.id, doc3.id, doc4.id], + ) + + response = self.client.get("/api/documents/?title_content=X") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 0) + + def test_document_owner_filters(self): + """ + GIVEN: + - Documents with owners, with and without granted permissions + WHEN: + - User filters by owner + THEN: + - Owner filters work correctly but still respect permissions + """ + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + + u1_doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + owner=u1, + ) + Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + owner=u2, + ) + u0_doc1 = Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + ) + u1_doc2 = Document.objects.create( + title="none4", + checksum="D", + mime_type="application/pdf", + owner=u1, + ) + u2_doc2 = Document.objects.create( + title="none5", + checksum="E", + mime_type="application/pdf", + owner=u2, + ) + + self.client.force_authenticate(user=u1) + assign_perm("view_document", u1, u2_doc2) + + # Will not show any u1 docs or u2_doc1 which isn't shared + response = self.client.get(f"/api/documents/?owner__id__none={u1.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual( + [results[0]["id"], results[1]["id"]], + [u0_doc1.id, u2_doc2.id], + ) + + # Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared + response = self.client.get( + f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) + + # Will not show any u1 docs, u2_doc2 which is shared but has owner + response = self.client.get( + f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u0_doc1.id]) + + # Will not show any u1 docs or u2_doc1 which is not shared + response = self.client.get(f"/api/documents/?owner__id={u2.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) + + # Will not show u2_doc1 which is not shared + response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertCountEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [u1_doc1.id, u1_doc2.id, u2_doc2.id], + ) + + def test_pagination_all(self): + """ + GIVEN: + - A set of 50 documents + WHEN: + - API request for document filtering + THEN: + - Results are paginated (25 items) and response["all"] returns all ids (50 items) + """ + t = Tag.objects.create(name="tag") + docs = [] + for i in range(50): + d = Document.objects.create(checksum=i, content=f"test{i}") + d.tags.add(t) + docs.append(d) + + response = self.client.get( + f"/api/documents/?tags__id__in={t.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 25) + self.assertEqual(len(response.data["all"]), 50) + self.assertCountEqual(response.data["all"], [d.id for d in docs]) + + def test_statistics(self): + doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + content="abc", + ) + Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + content="123", + ) + Document.objects.create( + title="none3", + checksum="C", + mime_type="text/plain", + content="hello", + ) + + tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True) + Tag.objects.create(name="t2") + Tag.objects.create(name="t3") + Correspondent.objects.create(name="c1") + Correspondent.objects.create(name="c2") + DocumentType.objects.create(name="dt1") + StoragePath.objects.create(name="sp1") + StoragePath.objects.create(name="sp2") + + doc1.tags.add(tag_inbox) + + response = self.client.get("/api/statistics/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["documents_total"], 3) + self.assertEqual(response.data["documents_inbox"], 1) + self.assertEqual(response.data["inbox_tag"], tag_inbox.pk) + self.assertEqual( + response.data["document_file_type_counts"][0]["mime_type_count"], + 2, + ) + self.assertEqual( + response.data["document_file_type_counts"][1]["mime_type_count"], + 1, + ) + self.assertEqual(response.data["character_count"], 11) + self.assertEqual(response.data["tag_count"], 3) + self.assertEqual(response.data["correspondent_count"], 2) + self.assertEqual(response.data["document_type_count"], 1) + self.assertEqual(response.data["storage_path_count"], 2) + + def test_statistics_no_inbox_tag(self): + Document.objects.create(title="none1", checksum="A") + + response = self.client.get("/api/statistics/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["documents_inbox"], None) + self.assertEqual(response.data["inbox_tag"], None) + + def test_upload(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + input_doc, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(input_doc.original_file.name, "simple.pdf") + self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents) + self.assertIsNone(overrides.title) + self.assertIsNone(overrides.correspondent_id) + self.assertIsNone(overrides.document_type_id) + self.assertIsNone(overrides.tag_ids) + + def test_upload_empty_metadata(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "title": "", "correspondent": "", "document_type": ""}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + input_doc, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(input_doc.original_file.name, "simple.pdf") + self.assertIn(Path(settings.SCRATCH_DIR), input_doc.original_file.parents) + self.assertIsNone(overrides.title) + self.assertIsNone(overrides.correspondent_id) + self.assertIsNone(overrides.document_type_id) + self.assertIsNone(overrides.tag_ids) + + def test_upload_invalid_form(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"documenst": f}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.consume_file_mock.assert_not_called() + + def test_upload_invalid_file(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.zip"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.consume_file_mock.assert_not_called() + + def test_upload_with_title(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "title": "my custom title"}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + _, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(overrides.title, "my custom title") + self.assertIsNone(overrides.correspondent_id) + self.assertIsNone(overrides.document_type_id) + self.assertIsNone(overrides.tag_ids) + + def test_upload_with_correspondent(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + c = Correspondent.objects.create(name="test-corres") + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "correspondent": c.id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + _, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(overrides.correspondent_id, c.id) + self.assertIsNone(overrides.title) + self.assertIsNone(overrides.document_type_id) + self.assertIsNone(overrides.tag_ids) + + def test_upload_with_invalid_correspondent(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "correspondent": 3456}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.consume_file_mock.assert_not_called() + + def test_upload_with_document_type(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + dt = DocumentType.objects.create(name="invoice") + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "document_type": dt.id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + _, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(overrides.document_type_id, dt.id) + self.assertIsNone(overrides.correspondent_id) + self.assertIsNone(overrides.title) + self.assertIsNone(overrides.tag_ids) + + def test_upload_with_invalid_document_type(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "document_type": 34578}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.consume_file_mock.assert_not_called() + + def test_upload_with_tags(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + t1 = Tag.objects.create(name="tag1") + t2 = Tag.objects.create(name="tag2") + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "tags": [t2.id, t1.id]}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + _, overrides = self.get_last_consume_delay_call_args() + + self.assertCountEqual(overrides.tag_ids, [t1.id, t2.id]) + self.assertIsNone(overrides.document_type_id) + self.assertIsNone(overrides.correspondent_id) + self.assertIsNone(overrides.title) + + def test_upload_with_invalid_tags(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + t1 = Tag.objects.create(name="tag1") + t2 = Tag.objects.create(name="tag2") + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "tags": [t2.id, t1.id, 734563]}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.consume_file_mock.assert_not_called() + + def test_upload_with_created(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + created = datetime.datetime( + 2022, + 5, + 12, + 0, + 0, + 0, + 0, + tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"), + ) + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "created": created}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + _, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(overrides.created, created) + + def test_upload_with_asn(self): + self.consume_file_mock.return_value = celery.result.AsyncResult( + id=str(uuid.uuid4()), + ) + + with open( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + "rb", + ) as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f, "archive_serial_number": 500}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.consume_file_mock.assert_called_once() + + input_doc, overrides = self.get_last_consume_delay_call_args() + + self.assertEqual(input_doc.original_file.name, "simple.pdf") + self.assertEqual(overrides.filename, "simple.pdf") + self.assertIsNone(overrides.correspondent_id) + self.assertIsNone(overrides.document_type_id) + self.assertIsNone(overrides.tag_ids) + self.assertEqual(500, overrides.asn) + + def test_get_metadata(self): + doc = Document.objects.create( + title="test", + filename="file.pdf", + mime_type="image/png", + archive_checksum="A", + archive_filename="archive.pdf", + ) + + source_file = os.path.join( + os.path.dirname(__file__), + "samples", + "documents", + "thumbnails", + "0000001.webp", + ) + archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") + + shutil.copy(source_file, doc.source_path) + shutil.copy(archive_file, doc.archive_path) + + response = self.client.get(f"/api/documents/{doc.pk}/metadata/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + meta = response.data + + self.assertEqual(meta["original_mime_type"], "image/png") + self.assertTrue(meta["has_archive_version"]) + self.assertEqual(len(meta["original_metadata"]), 0) + self.assertGreater(len(meta["archive_metadata"]), 0) + self.assertEqual(meta["media_filename"], "file.pdf") + self.assertEqual(meta["archive_media_filename"], "archive.pdf") + self.assertEqual(meta["original_size"], os.stat(source_file).st_size) + self.assertEqual(meta["archive_size"], os.stat(archive_file).st_size) + + def test_get_metadata_invalid_doc(self): + response = self.client.get("/api/documents/34576/metadata/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_get_metadata_no_archive(self): + doc = Document.objects.create( + title="test", + filename="file.pdf", + mime_type="application/pdf", + ) + + shutil.copy( + os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), + doc.source_path, + ) + + response = self.client.get(f"/api/documents/{doc.pk}/metadata/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + meta = response.data + + self.assertEqual(meta["original_mime_type"], "application/pdf") + self.assertFalse(meta["has_archive_version"]) + self.assertGreater(len(meta["original_metadata"]), 0) + self.assertIsNone(meta["archive_metadata"]) + self.assertIsNone(meta["archive_media_filename"]) + + def test_get_metadata_missing_files(self): + doc = Document.objects.create( + title="test", + filename="file.pdf", + mime_type="application/pdf", + archive_filename="file.pdf", + archive_checksum="B", + checksum="A", + ) + + response = self.client.get(f"/api/documents/{doc.pk}/metadata/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + meta = response.data + + self.assertTrue(meta["has_archive_version"]) + self.assertIsNone(meta["original_metadata"]) + self.assertIsNone(meta["original_size"]) + self.assertIsNone(meta["archive_metadata"]) + self.assertIsNone(meta["archive_size"]) + + def test_get_empty_suggestions(self): + doc = Document.objects.create(title="test", mime_type="application/pdf") + + response = self.client.get(f"/api/documents/{doc.pk}/suggestions/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data, + { + "correspondents": [], + "tags": [], + "document_types": [], + "storage_paths": [], + "dates": [], + }, + ) + + def test_get_suggestions_invalid_doc(self): + response = self.client.get("/api/documents/34676/suggestions/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + @mock.patch("documents.views.match_storage_paths") + @mock.patch("documents.views.match_document_types") + @mock.patch("documents.views.match_tags") + @mock.patch("documents.views.match_correspondents") + @override_settings(NUMBER_OF_SUGGESTED_DATES=10) + def test_get_suggestions( + self, + match_correspondents, + match_tags, + match_document_types, + match_storage_paths, + ): + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is an invoice from 12.04.2022!", + ) + + match_correspondents.return_value = [Correspondent(id=88), Correspondent(id=2)] + match_tags.return_value = [Tag(id=56), Tag(id=123)] + match_document_types.return_value = [DocumentType(id=23)] + match_storage_paths.return_value = [StoragePath(id=99), StoragePath(id=77)] + + response = self.client.get(f"/api/documents/{doc.pk}/suggestions/") + self.assertEqual( + response.data, + { + "correspondents": [88, 2], + "tags": [56, 123], + "document_types": [23], + "storage_paths": [99, 77], + "dates": ["2022-04-12"], + }, + ) + + @mock.patch("documents.parsers.parse_date_generator") + @override_settings(NUMBER_OF_SUGGESTED_DATES=0) + def test_get_suggestions_dates_disabled( + self, + parse_date_generator, + ): + """ + GIVEN: + - NUMBER_OF_SUGGESTED_DATES = 0 (disables feature) + WHEN: + - API reuqest for document suggestions + THEN: + - Dont check for suggested dates at all + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is an invoice from 12.04.2022!", + ) + + self.client.get(f"/api/documents/{doc.pk}/suggestions/") + self.assertFalse(parse_date_generator.called) + + def test_saved_views(self): + u1 = User.objects.create_superuser("user1") + u2 = User.objects.create_superuser("user2") + + v1 = SavedView.objects.create( + owner=u1, + name="test1", + sort_field="", + show_on_dashboard=False, + show_in_sidebar=False, + ) + SavedView.objects.create( + owner=u2, + name="test2", + sort_field="", + show_on_dashboard=False, + show_in_sidebar=False, + ) + SavedView.objects.create( + owner=u2, + name="test3", + sort_field="", + show_on_dashboard=False, + show_in_sidebar=False, + ) + + response = self.client.get("/api/saved_views/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 0) + + self.assertEqual( + self.client.get(f"/api/saved_views/{v1.id}/").status_code, + status.HTTP_404_NOT_FOUND, + ) + + self.client.force_authenticate(user=u1) + + response = self.client.get("/api/saved_views/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + self.assertEqual( + self.client.get(f"/api/saved_views/{v1.id}/").status_code, + status.HTTP_200_OK, + ) + + self.client.force_authenticate(user=u2) + + response = self.client.get("/api/saved_views/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + + self.assertEqual( + self.client.get(f"/api/saved_views/{v1.id}/").status_code, + status.HTTP_404_NOT_FOUND, + ) + + def test_create_update_patch(self): + User.objects.create_user("user1") + + view = { + "name": "test", + "show_on_dashboard": True, + "show_in_sidebar": True, + "sort_field": "created2", + "filter_rules": [{"rule_type": 4, "value": "test"}], + } + + response = self.client.post("/api/saved_views/", view, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + v1 = SavedView.objects.get(name="test") + self.assertEqual(v1.sort_field, "created2") + self.assertEqual(v1.filter_rules.count(), 1) + self.assertEqual(v1.owner, self.user) + + response = self.client.patch( + f"/api/saved_views/{v1.id}/", + {"show_in_sidebar": False}, + format="json", + ) + + v1 = SavedView.objects.get(id=v1.id) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(v1.show_in_sidebar) + self.assertEqual(v1.filter_rules.count(), 1) + + view["filter_rules"] = [{"rule_type": 12, "value": "secret"}] + + response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + v1 = SavedView.objects.get(id=v1.id) + self.assertEqual(v1.filter_rules.count(), 1) + self.assertEqual(v1.filter_rules.first().value, "secret") + + view["filter_rules"] = [] + + response = self.client.put(f"/api/saved_views/{v1.id}/", view, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + v1 = SavedView.objects.get(id=v1.id) + self.assertEqual(v1.filter_rules.count(), 0) + + def test_get_logs(self): + log_data = "test\ntest2\n" + with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f: + f.write(log_data) + with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: + f.write(log_data) + response = self.client.get("/api/logs/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, ["mail", "paperless"]) + + def test_get_logs_only_when_exist(self): + log_data = "test\ntest2\n" + with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: + f.write(log_data) + response = self.client.get("/api/logs/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertCountEqual(response.data, ["paperless"]) + + def test_get_invalid_log(self): + response = self.client.get("/api/logs/bogus_log/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + @override_settings(LOGGING_DIR="bogus_dir") + def test_get_nonexistent_log(self): + response = self.client.get("/api/logs/paperless/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_get_log(self): + log_data = "test\ntest2\n" + with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: + f.write(log_data) + response = self.client.get("/api/logs/paperless/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual(response.data, ["test", "test2"]) + + def test_invalid_regex_other_algorithm(self): + for endpoint in ["correspondents", "tags", "document_types"]: + response = self.client.post( + f"/api/{endpoint}/", + { + "name": "test", + "matching_algorithm": MatchingModel.MATCH_ANY, + "match": "[", + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint) + + def test_invalid_regex(self): + for endpoint in ["correspondents", "tags", "document_types"]: + response = self.client.post( + f"/api/{endpoint}/", + { + "name": "test", + "matching_algorithm": MatchingModel.MATCH_REGEX, + "match": "[", + }, + format="json", + ) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + endpoint, + ) + + def test_valid_regex(self): + for endpoint in ["correspondents", "tags", "document_types"]: + response = self.client.post( + f"/api/{endpoint}/", + { + "name": "test", + "matching_algorithm": MatchingModel.MATCH_REGEX, + "match": "[0-9]", + }, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint) + + def test_regex_no_algorithm(self): + for endpoint in ["correspondents", "tags", "document_types"]: + response = self.client.post( + f"/api/{endpoint}/", + {"name": "test", "match": "[0-9]"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, endpoint) + + def test_tag_color_default(self): + response = self.client.post("/api/tags/", {"name": "tag"}, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#a6cee3") + self.assertEqual( + self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[ + "colour" + ], + 1, + ) + + def test_tag_color(self): + response = self.client.post( + "/api/tags/", + {"name": "tag", "colour": 3}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Tag.objects.get(id=response.data["id"]).color, "#b2df8a") + self.assertEqual( + self.client.get(f"/api/tags/{response.data['id']}/", format="json").data[ + "colour" + ], + 3, + ) + + def test_tag_color_invalid(self): + response = self.client.post( + "/api/tags/", + {"name": "tag", "colour": 34}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_tag_color_custom(self): + tag = Tag.objects.create(name="test", color="#abcdef") + self.assertEqual( + self.client.get(f"/api/tags/{tag.id}/", format="json").data["colour"], + 1, + ) + + def test_get_existing_notes(self): + """ + GIVEN: + - A document with a single note + WHEN: + - API reuqest for document notes is made + THEN: + - The associated note is returned + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will have notes!", + ) + note = Note.objects.create( + note="This is a note.", + document=doc, + user=self.user, + ) + + response = self.client.get( + f"/api/documents/{doc.pk}/notes/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertEqual(len(resp_data), 1) + + resp_data = resp_data[0] + del resp_data["created"] + + self.assertDictEqual( + resp_data, + { + "id": note.id, + "note": note.note, + "user": { + "id": note.user.id, + "username": note.user.username, + "first_name": note.user.first_name, + "last_name": note.user.last_name, + }, + }, + ) + + def test_create_note(self): + """ + GIVEN: + - Existing document + WHEN: + - API request is made to add a note + THEN: + - note is created and associated with document, modified time is updated + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will have notes added", + created=timezone.now() - timedelta(days=1), + ) + # set to yesterday + doc.modified = timezone.now() - timedelta(days=1) + self.assertEqual(doc.modified.day, (timezone.now() - timedelta(days=1)).day) + + resp = self.client.post( + f"/api/documents/{doc.pk}/notes/", + data={"note": "this is a posted note"}, + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + response = self.client.get( + f"/api/documents/{doc.pk}/notes/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertEqual(len(resp_data), 1) + + resp_data = resp_data[0] + + self.assertEqual(resp_data["note"], "this is a posted note") + + doc = Document.objects.get(pk=doc.pk) + # modified was updated to today + self.assertEqual(doc.modified.day, timezone.now().day) + + def test_notes_permissions_aware(self): + """ + GIVEN: + - Existing document owned by user2 but with granted view perms for user1 + WHEN: + - API request is made by user1 to add a note or delete + THEN: + - Notes are neither created nor deleted + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.all()) + user1.save() + + user2 = User.objects.create_user(username="test2") + user2.save() + + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will have notes added", + ) + doc.owner = user2 + doc.save() + + self.client.force_authenticate(user1) + + resp = self.client.get( + f"/api/documents/{doc.pk}/notes/", + format="json", + ) + self.assertEqual(resp.content, b"Insufficient permissions to view notes") + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + + assign_perm("view_document", user1, doc) + + resp = self.client.post( + f"/api/documents/{doc.pk}/notes/", + data={"note": "this is a posted note"}, + ) + self.assertEqual(resp.content, b"Insufficient permissions to create notes") + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + + note = Note.objects.create( + note="This is a note.", + document=doc, + user=user2, + ) + + response = self.client.delete( + f"/api/documents/{doc.pk}/notes/?id={note.pk}", + format="json", + ) + + self.assertEqual(response.content, b"Insufficient permissions to delete notes") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_note(self): + """ + GIVEN: + - Existing document, existing note + WHEN: + - API request is made to delete a note + THEN: + - note is deleted, document modified is updated + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will have notes!", + created=timezone.now() - timedelta(days=1), + ) + # set to yesterday + doc.modified = timezone.now() - timedelta(days=1) + self.assertEqual(doc.modified.day, (timezone.now() - timedelta(days=1)).day) + note = Note.objects.create( + note="This is a note.", + document=doc, + user=self.user, + ) + + response = self.client.delete( + f"/api/documents/{doc.pk}/notes/?id={note.pk}", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(len(Note.objects.all()), 0) + doc = Document.objects.get(pk=doc.pk) + # modified was updated to today + self.assertEqual(doc.modified.day, timezone.now().day) + + def test_get_notes_no_doc(self): + """ + GIVEN: + - A request to get notes from a non-existent document + WHEN: + - API request for document notes is made + THEN: + - HTTP status.HTTP_404_NOT_FOUND is returned + """ + response = self.client.get( + "/api/documents/500/notes/", + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_tag_unique_name_and_owner(self): + """ + GIVEN: + - Multiple users + - Tags owned by particular users + WHEN: + - API request for creating items which are unique by name and owner + THEN: + - Unique items are created + - Non-unique items are not allowed + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.filter(codename="add_tag")) + user1.save() + + user2 = User.objects.create_user(username="test2") + user2.user_permissions.add(*Permission.objects.filter(codename="add_tag")) + user2.save() + + # User 1 creates tag 1 owned by user 1 by default + # No issue + self.client.force_authenticate(user1) + response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # User 2 creates tag 1 owned by user 2 by default + # No issue + self.client.force_authenticate(user2) + response = self.client.post("/api/tags/", {"name": "tag 1"}, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # User 2 creates tag 2 owned by user 1 + # No issue + self.client.force_authenticate(user2) + response = self.client.post( + "/api/tags/", + {"name": "tag 2", "owner": user1.pk}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # User 1 creates tag 2 owned by user 1 by default + # Not allowed, would create tag2/user1 which already exists + self.client.force_authenticate(user1) + response = self.client.post( + "/api/tags/", + {"name": "tag 2"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # User 1 creates tag 2 owned by user 1 + # Not allowed, would create tag2/user1 which already exists + response = self.client.post( + "/api/tags/", + {"name": "tag 2", "owner": user1.pk}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_tag_unique_name_and_owner_enforced_on_update(self): + """ + GIVEN: + - Multiple users + - Tags owned by particular users + WHEN: + - API request for to update tag in such as way as makes it non-unqiue + THEN: + - Unique items are created + - Non-unique items are not allowed on update + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.filter(codename="change_tag")) + user1.save() + + user2 = User.objects.create_user(username="test2") + user2.user_permissions.add(*Permission.objects.filter(codename="change_tag")) + user2.save() + + # Create name tag 1 owned by user 1 + # Create name tag 1 owned by user 2 + Tag.objects.create(name="tag 1", owner=user1) + tag2 = Tag.objects.create(name="tag 1", owner=user2) + + # User 2 attempts to change the owner of tag to user 1 + # Not allowed, would change to tag1/user1 which already exists + self.client.force_authenticate(user2) + response = self.client.patch( + f"/api/tags/{tag2.id}/", + {"owner": user1.pk}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_share_links(self): + """ + GIVEN: + - Existing document + WHEN: + - API request is made to generate a share_link + - API request is made to view share_links on incorrect doc pk + - Invalid method request is made to view share_links doc + THEN: + - Link is created with a slug and associated with document + - 404 + - Error + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will have notes added", + ) + # never expires + resp = self.client.post( + "/api/share_links/", + data={ + "document": doc.pk, + }, + ) + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + + resp = self.client.post( + "/api/share_links/", + data={ + "expiration": (timezone.now() + timedelta(days=7)).isoformat(), + "document": doc.pk, + "file_version": "original", + }, + ) + self.assertEqual(resp.status_code, status.HTTP_201_CREATED) + + response = self.client.get( + f"/api/documents/{doc.pk}/share_links/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertEqual(len(resp_data), 2) + + self.assertGreater(len(resp_data[1]["slug"]), 0) + self.assertIsNone(resp_data[1]["expiration"]) + self.assertEqual( + (parser.isoparse(resp_data[0]["expiration"]) - timezone.now()).days, + 6, + ) + + sl1 = ShareLink.objects.get(slug=resp_data[1]["slug"]) + self.assertEqual(str(sl1), f"Share Link for {doc.title}") + + response = self.client.post( + f"/api/documents/{doc.pk}/share_links/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + + response = self.client.get( + "/api/documents/99/share_links/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_share_links_permissions_aware(self): + """ + GIVEN: + - Existing document owned by user2 but with granted view perms for user1 + WHEN: + - API request is made by user1 to view share links + THEN: + - Links only shown if user has permissions + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.all()) + user1.save() + + user2 = User.objects.create_user(username="test2") + user2.save() + + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will have share links added", + ) + doc.owner = user2 + doc.save() + + self.client.force_authenticate(user1) + + resp = self.client.get( + f"/api/documents/{doc.pk}/share_links/", + format="json", + ) + self.assertEqual(resp.content, b"Insufficient permissions to add share link") + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + + assign_perm("change_document", user1, doc) + + resp = self.client.get( + f"/api/documents/{doc.pk}/share_links/", + format="json", + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + def test_next_asn(self): + """ + GIVEN: + - Existing documents with ASNs, highest owned by user2 + WHEN: + - API request is made by user1 to get next ASN + THEN: + - ASN +1 from user2's doc is returned for user1 + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.all()) + user1.save() + + user2 = User.objects.create_user(username="test2") + user2.save() + + doc1 = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document 1", + checksum="1", + archive_serial_number=998, + ) + doc1.owner = user1 + doc1.save() + + doc2 = Document.objects.create( + title="test2", + mime_type="application/pdf", + content="this is a document 2 with higher ASN", + checksum="2", + archive_serial_number=999, + ) + doc2.owner = user2 + doc2.save() + + self.client.force_authenticate(user1) + + resp = self.client.get( + "/api/documents/next_asn/", + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + self.assertEqual(resp.content, b"1000") + + +class TestDocumentApiV2(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser(username="temp_admin") + + self.client.force_authenticate(user=self.user) + self.client.defaults["HTTP_ACCEPT"] = "application/json; version=2" + + def test_tag_validate_color(self): + self.assertEqual( + self.client.post( + "/api/tags/", + {"name": "test", "color": "#12fFaA"}, + format="json", + ).status_code, + status.HTTP_201_CREATED, + ) + + self.assertEqual( + self.client.post( + "/api/tags/", + {"name": "test1", "color": "abcdef"}, + format="json", + ).status_code, + status.HTTP_400_BAD_REQUEST, + ) + self.assertEqual( + self.client.post( + "/api/tags/", + {"name": "test2", "color": "#abcdfg"}, + format="json", + ).status_code, + status.HTTP_400_BAD_REQUEST, + ) + self.assertEqual( + self.client.post( + "/api/tags/", + {"name": "test3", "color": "#asd"}, + format="json", + ).status_code, + status.HTTP_400_BAD_REQUEST, + ) + self.assertEqual( + self.client.post( + "/api/tags/", + {"name": "test4", "color": "#12121212"}, + format="json", + ).status_code, + status.HTTP_400_BAD_REQUEST, + ) + + def test_tag_text_color(self): + t = Tag.objects.create(name="tag1", color="#000000") + self.assertEqual( + self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], + "#ffffff", + ) + + t.color = "#ffffff" + t.save() + self.assertEqual( + self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], + "#000000", + ) + + t.color = "asdf" + t.save() + self.assertEqual( + self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], + "#000000", + ) + + t.color = "123" + t.save() + self.assertEqual( + self.client.get(f"/api/tags/{t.id}/", format="json").data["text_color"], + "#000000", + ) diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py new file mode 100644 index 000000000..e894cae90 --- /dev/null +++ b/src/documents/tests/test_api_objects.py @@ -0,0 +1,224 @@ +import json +from unittest import mock + +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import Correspondent +from documents.models import Document +from documents.models import DocumentType +from documents.models import StoragePath +from documents.models import Tag +from documents.tests.utils import DirectoriesMixin + + +class TestApiObjects(DirectoriesMixin, APITestCase): + def setUp(self) -> None: + super().setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=user) + + self.tag1 = Tag.objects.create(name="t1", is_inbox_tag=True) + self.tag2 = Tag.objects.create(name="t2") + self.tag3 = Tag.objects.create(name="t3") + self.c1 = Correspondent.objects.create(name="c1") + self.c2 = Correspondent.objects.create(name="c2") + self.c3 = Correspondent.objects.create(name="c3") + self.dt1 = DocumentType.objects.create(name="dt1") + self.dt2 = DocumentType.objects.create(name="dt2") + self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{title}") + self.sp2 = StoragePath.objects.create(name="sp2", path="Something2/{title}") + + def test_object_filters(self): + response = self.client.get( + f"/api/tags/?id={self.tag2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + + response = self.client.get( + f"/api/tags/?id__in={self.tag1.id},{self.tag3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + + response = self.client.get( + f"/api/correspondents/?id={self.c2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + + response = self.client.get( + f"/api/correspondents/?id__in={self.c1.id},{self.c3.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + + response = self.client.get( + f"/api/document_types/?id={self.dt1.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + + response = self.client.get( + f"/api/document_types/?id__in={self.dt1.id},{self.dt2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + + response = self.client.get( + f"/api/storage_paths/?id={self.sp1.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + + response = self.client.get( + f"/api/storage_paths/?id__in={self.sp1.id},{self.sp2.id}", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + + +class TestApiStoragePaths(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/storage_paths/" + + def setUp(self) -> None: + super().setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=user) + + self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}") + + def test_api_get_storage_path(self): + """ + GIVEN: + - API request to get all storage paths + WHEN: + - API is called + THEN: + - Existing storage paths are returned + """ + response = self.client.get(self.ENDPOINT, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + resp_storage_path = response.data["results"][0] + self.assertEqual(resp_storage_path["id"], self.sp1.id) + self.assertEqual(resp_storage_path["path"], self.sp1.path) + + def test_api_create_storage_path(self): + """ + GIVEN: + - API request to create a storage paths + WHEN: + - API is called + THEN: + - Correct HTTP response + - New storage path is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "A storage path", + "path": "Somewhere/{asn}", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(StoragePath.objects.count(), 2) + + def test_api_create_invalid_storage_path(self): + """ + GIVEN: + - API request to create a storage paths + - Storage path format is incorrect + WHEN: + - API is called + THEN: + - Correct HTTP 400 response + - No storage path is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Another storage path", + "path": "Somewhere/{correspdent}", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(StoragePath.objects.count(), 1) + + def test_api_storage_path_placeholders(self): + """ + GIVEN: + - API request to create a storage path with placeholders + - Storage path is valid + WHEN: + - API is called + THEN: + - Correct HTTP response + - New storage path is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Storage path with placeholders", + "path": "{title}/{correspondent}/{document_type}/{created}/{created_year}" + "/{created_year_short}/{created_month}/{created_month_name}" + "/{created_month_name_short}/{created_day}/{added}/{added_year}" + "/{added_year_short}/{added_month}/{added_month_name}" + "/{added_month_name_short}/{added_day}/{asn}/{tags}" + "/{tag_list}/{owner_username}/{original_name}/{doc_pk}/", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(StoragePath.objects.count(), 2) + + @mock.patch("documents.bulk_edit.bulk_update_documents.delay") + def test_api_update_storage_path(self, bulk_update_mock): + """ + GIVEN: + - API request to get all storage paths + WHEN: + - API is called + THEN: + - Existing storage paths are returned + """ + document = Document.objects.create( + mime_type="application/pdf", + storage_path=self.sp1, + ) + response = self.client.patch( + f"{self.ENDPOINT}{self.sp1.pk}/", + data={ + "path": "somewhere/{created} - {title}", + }, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + bulk_update_mock.assert_called_once() + + args, _ = bulk_update_mock.call_args + + self.assertCountEqual([document.pk], args[0]) diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py new file mode 100644 index 000000000..1b6bd19df --- /dev/null +++ b/src/documents/tests/test_api_permissions.py @@ -0,0 +1,910 @@ +import json + +from django.contrib.auth.models import Group +from django.contrib.auth.models import Permission +from django.contrib.auth.models import User +from guardian.shortcuts import assign_perm +from guardian.shortcuts import get_perms +from guardian.shortcuts import get_users_with_perms +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import Correspondent +from documents.models import Document +from documents.models import DocumentType +from documents.models import MatchingModel +from documents.models import StoragePath +from documents.models import Tag +from documents.tests.utils import DirectoriesMixin + + +class TestApiAuth(DirectoriesMixin, APITestCase): + def test_auth_required(self): + d = Document.objects.create(title="Test") + + self.assertEqual( + self.client.get("/api/documents/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + + self.assertEqual( + self.client.get(f"/api/documents/{d.id}/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get(f"/api/documents/{d.id}/download/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get(f"/api/documents/{d.id}/preview/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get(f"/api/documents/{d.id}/thumb/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + + self.assertEqual( + self.client.get("/api/tags/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get("/api/correspondents/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get("/api/document_types/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + + self.assertEqual( + self.client.get("/api/logs/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get("/api/saved_views/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + + self.assertEqual( + self.client.get("/api/search/autocomplete/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get("/api/documents/bulk_edit/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get("/api/documents/bulk_download/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + self.assertEqual( + self.client.get("/api/documents/selection_data/").status_code, + status.HTTP_401_UNAUTHORIZED, + ) + + def test_api_version_no_auth(self): + response = self.client.get("/api/") + self.assertNotIn("X-Api-Version", response) + self.assertNotIn("X-Version", response) + + def test_api_version_with_auth(self): + user = User.objects.create_superuser(username="test") + self.client.force_authenticate(user) + response = self.client.get("/api/") + self.assertIn("X-Api-Version", response) + self.assertIn("X-Version", response) + + def test_api_insufficient_permissions(self): + user = User.objects.create_user(username="test") + self.client.force_authenticate(user) + + Document.objects.create(title="Test") + + self.assertEqual( + self.client.get("/api/documents/").status_code, + status.HTTP_403_FORBIDDEN, + ) + + self.assertEqual( + self.client.get("/api/tags/").status_code, + status.HTTP_403_FORBIDDEN, + ) + self.assertEqual( + self.client.get("/api/correspondents/").status_code, + status.HTTP_403_FORBIDDEN, + ) + self.assertEqual( + self.client.get("/api/document_types/").status_code, + status.HTTP_403_FORBIDDEN, + ) + + self.assertEqual( + self.client.get("/api/logs/").status_code, + status.HTTP_403_FORBIDDEN, + ) + self.assertEqual( + self.client.get("/api/saved_views/").status_code, + status.HTTP_403_FORBIDDEN, + ) + + def test_api_sufficient_permissions(self): + user = User.objects.create_user(username="test") + user.user_permissions.add(*Permission.objects.all()) + self.client.force_authenticate(user) + + Document.objects.create(title="Test") + + self.assertEqual( + self.client.get("/api/documents/").status_code, + status.HTTP_200_OK, + ) + + self.assertEqual(self.client.get("/api/tags/").status_code, status.HTTP_200_OK) + self.assertEqual( + self.client.get("/api/correspondents/").status_code, + status.HTTP_200_OK, + ) + self.assertEqual( + self.client.get("/api/document_types/").status_code, + status.HTTP_200_OK, + ) + + self.assertEqual(self.client.get("/api/logs/").status_code, status.HTTP_200_OK) + self.assertEqual( + self.client.get("/api/saved_views/").status_code, + status.HTTP_200_OK, + ) + + def test_api_get_object_permissions(self): + user1 = User.objects.create_user(username="test1") + user2 = User.objects.create_user(username="test2") + user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + self.client.force_authenticate(user1) + + self.assertEqual( + self.client.get("/api/documents/").status_code, + status.HTTP_200_OK, + ) + + d = Document.objects.create(title="Test", content="the content 1", checksum="1") + + # no owner + self.assertEqual( + self.client.get(f"/api/documents/{d.id}/").status_code, + status.HTTP_200_OK, + ) + + d2 = Document.objects.create( + title="Test 2", + content="the content 2", + checksum="2", + owner=user2, + ) + + self.assertEqual( + self.client.get(f"/api/documents/{d2.id}/").status_code, + status.HTTP_404_NOT_FOUND, + ) + + def test_api_default_owner(self): + """ + GIVEN: + - API request to create an object (Tag) + WHEN: + - owner is not set at all + THEN: + - Object created with current user as owner + """ + user1 = User.objects.create_superuser(username="user1") + + self.client.force_authenticate(user1) + + response = self.client.post( + "/api/tags/", + json.dumps( + { + "name": "test1", + "matching_algorithm": MatchingModel.MATCH_AUTO, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + tag1 = Tag.objects.filter(name="test1").first() + self.assertEqual(tag1.owner, user1) + + def test_api_set_no_owner(self): + """ + GIVEN: + - API request to create an object (Tag) + WHEN: + - owner is passed as None + THEN: + - Object created with no owner + """ + user1 = User.objects.create_superuser(username="user1") + + self.client.force_authenticate(user1) + + response = self.client.post( + "/api/tags/", + json.dumps( + { + "name": "test1", + "matching_algorithm": MatchingModel.MATCH_AUTO, + "owner": None, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + tag1 = Tag.objects.filter(name="test1").first() + self.assertEqual(tag1.owner, None) + + def test_api_set_owner_w_permissions(self): + """ + GIVEN: + - API request to create an object (Tag) that supplies set_permissions object + WHEN: + - owner is passed as user id + - view > users is set & view > groups is set + THEN: + - Object permissions are set appropriately + """ + user1 = User.objects.create_superuser(username="user1") + user2 = User.objects.create(username="user2") + group1 = Group.objects.create(name="group1") + + self.client.force_authenticate(user1) + + response = self.client.post( + "/api/tags/", + json.dumps( + { + "name": "test1", + "matching_algorithm": MatchingModel.MATCH_AUTO, + "owner": user1.id, + "set_permissions": { + "view": { + "users": [user2.id], + "groups": [group1.id], + }, + "change": { + "users": None, + "groups": None, + }, + }, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + tag1 = Tag.objects.filter(name="test1").first() + + from guardian.core import ObjectPermissionChecker + + checker = ObjectPermissionChecker(user2) + self.assertEqual(checker.has_perm("view_tag", tag1), True) + self.assertIn("view_tag", get_perms(group1, tag1)) + + def test_api_set_other_owner_w_permissions(self): + """ + GIVEN: + - API request to create an object (Tag) + WHEN: + - a different owner than is logged in is set + - view > groups is set + THEN: + - Object permissions are set appropriately + """ + user1 = User.objects.create_superuser(username="user1") + user2 = User.objects.create(username="user2") + group1 = Group.objects.create(name="group1") + + self.client.force_authenticate(user1) + + response = self.client.post( + "/api/tags/", + json.dumps( + { + "name": "test1", + "matching_algorithm": MatchingModel.MATCH_AUTO, + "owner": user2.id, + "set_permissions": { + "view": { + "users": None, + "groups": [group1.id], + }, + "change": { + "users": None, + "groups": None, + }, + }, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + tag1 = Tag.objects.filter(name="test1").first() + + self.assertEqual(tag1.owner, user2) + self.assertIn("view_tag", get_perms(group1, tag1)) + + def test_api_set_doc_permissions(self): + """ + GIVEN: + - API request to update doc permissions and owner + WHEN: + - owner is set + - view > users is set & view > groups is set + THEN: + - Object permissions are set appropriately + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document", + ) + user1 = User.objects.create_superuser(username="user1") + user2 = User.objects.create(username="user2") + group1 = Group.objects.create(name="group1") + + self.client.force_authenticate(user1) + + response = self.client.patch( + f"/api/documents/{doc.id}/", + json.dumps( + { + "owner": user1.id, + "set_permissions": { + "view": { + "users": [user2.id], + "groups": [group1.id], + }, + "change": { + "users": None, + "groups": None, + }, + }, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + doc = Document.objects.get(pk=doc.id) + + self.assertEqual(doc.owner, user1) + from guardian.core import ObjectPermissionChecker + + checker = ObjectPermissionChecker(user2) + self.assertTrue(checker.has_perm("view_document", doc)) + self.assertIn("view_document", get_perms(group1, doc)) + + def test_dynamic_permissions_fields(self): + user1 = User.objects.create_user(username="user1") + user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + user2 = User.objects.create_user(username="user2") + + Document.objects.create(title="Test", content="content 1", checksum="1") + doc2 = Document.objects.create( + title="Test2", + content="content 2", + checksum="2", + owner=user2, + ) + doc3 = Document.objects.create( + title="Test3", + content="content 3", + checksum="3", + owner=user2, + ) + + assign_perm("view_document", user1, doc2) + assign_perm("view_document", user1, doc3) + assign_perm("change_document", user1, doc3) + + self.client.force_authenticate(user1) + + response = self.client.get( + "/api/documents/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertNotIn("permissions", resp_data["results"][0]) + self.assertIn("user_can_change", resp_data["results"][0]) + self.assertEqual(resp_data["results"][0]["user_can_change"], True) # doc1 + self.assertEqual(resp_data["results"][1]["user_can_change"], False) # doc2 + self.assertEqual(resp_data["results"][2]["user_can_change"], True) # doc3 + + response = self.client.get( + "/api/documents/?full_perms=true", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + resp_data = response.json() + + self.assertIn("permissions", resp_data["results"][0]) + self.assertNotIn("user_can_change", resp_data["results"][0]) + + +class TestApiUser(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/users/" + + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=self.user) + + def test_get_users(self): + """ + GIVEN: + - Configured users + WHEN: + - API call is made to get users + THEN: + - Configured users are provided + """ + + user1 = User.objects.create( + username="testuser", + password="test", + first_name="Test", + last_name="User", + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + returned_user2 = response.data["results"][1] + + self.assertEqual(returned_user2["username"], user1.username) + self.assertEqual(returned_user2["password"], "**********") + self.assertEqual(returned_user2["first_name"], user1.first_name) + self.assertEqual(returned_user2["last_name"], user1.last_name) + + def test_create_user(self): + """ + WHEN: + - API request is made to add a user account + THEN: + - A new user account is created + """ + + user1 = { + "username": "testuser", + "password": "test", + "first_name": "Test", + "last_name": "User", + } + + response = self.client.post( + self.ENDPOINT, + data=user1, + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + returned_user1 = User.objects.get(username="testuser") + + self.assertEqual(returned_user1.username, user1["username"]) + self.assertEqual(returned_user1.first_name, user1["first_name"]) + self.assertEqual(returned_user1.last_name, user1["last_name"]) + + def test_delete_user(self): + """ + GIVEN: + - Existing user account + WHEN: + - API request is made to delete a user account + THEN: + - Account is deleted + """ + + user1 = User.objects.create( + username="testuser", + password="test", + first_name="Test", + last_name="User", + ) + + nUsers = User.objects.count() + + response = self.client.delete( + f"{self.ENDPOINT}{user1.pk}/", + ) + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.assertEqual(User.objects.count(), nUsers - 1) + + def test_update_user(self): + """ + GIVEN: + - Existing user accounts + WHEN: + - API request is made to update user account + THEN: + - The user account is updated, password only updated if not '****' + """ + + user1 = User.objects.create( + username="testuser", + password="test", + first_name="Test", + last_name="User", + ) + + initial_password = user1.password + + response = self.client.patch( + f"{self.ENDPOINT}{user1.pk}/", + data={ + "first_name": "Updated Name 1", + "password": "******", + }, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + returned_user1 = User.objects.get(pk=user1.pk) + self.assertEqual(returned_user1.first_name, "Updated Name 1") + self.assertEqual(returned_user1.password, initial_password) + + response = self.client.patch( + f"{self.ENDPOINT}{user1.pk}/", + data={ + "first_name": "Updated Name 2", + "password": "123xyz", + }, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + returned_user2 = User.objects.get(pk=user1.pk) + self.assertEqual(returned_user2.first_name, "Updated Name 2") + self.assertNotEqual(returned_user2.password, initial_password) + + +class TestApiGroup(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/groups/" + + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=self.user) + + def test_get_groups(self): + """ + GIVEN: + - Configured groups + WHEN: + - API call is made to get groups + THEN: + - Configured groups are provided + """ + + group1 = Group.objects.create( + name="Test Group", + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + returned_group1 = response.data["results"][0] + + self.assertEqual(returned_group1["name"], group1.name) + + def test_create_group(self): + """ + WHEN: + - API request is made to add a group + THEN: + - A new group is created + """ + + group1 = { + "name": "Test Group", + } + + response = self.client.post( + self.ENDPOINT, + data=group1, + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + returned_group1 = Group.objects.get(name="Test Group") + + self.assertEqual(returned_group1.name, group1["name"]) + + def test_delete_group(self): + """ + GIVEN: + - Existing group + WHEN: + - API request is made to delete a group + THEN: + - Group is deleted + """ + + group1 = Group.objects.create( + name="Test Group", + ) + + response = self.client.delete( + f"{self.ENDPOINT}{group1.pk}/", + ) + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.assertEqual(len(Group.objects.all()), 0) + + def test_update_group(self): + """ + GIVEN: + - Existing groups + WHEN: + - API request is made to update group + THEN: + - The group is updated + """ + + group1 = Group.objects.create( + name="Test Group", + ) + + response = self.client.patch( + f"{self.ENDPOINT}{group1.pk}/", + data={ + "name": "Updated Name 1", + }, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + returned_group1 = Group.objects.get(pk=group1.pk) + self.assertEqual(returned_group1.name, "Updated Name 1") + + +class TestBulkEditObjectPermissions(APITestCase): + def setUp(self): + super().setUp() + + user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=user) + + self.t1 = Tag.objects.create(name="t1") + self.t2 = Tag.objects.create(name="t2") + self.c1 = Correspondent.objects.create(name="c1") + self.dt1 = DocumentType.objects.create(name="dt1") + self.sp1 = StoragePath.objects.create(name="sp1") + self.user1 = User.objects.create(username="user1") + self.user2 = User.objects.create(username="user2") + self.user3 = User.objects.create(username="user3") + + def test_bulk_object_set_permissions(self): + """ + GIVEN: + - Existing objects + WHEN: + - bulk_edit_object_perms API endpoint is called + THEN: + - Permissions and / or owner are changed + """ + permissions = { + "view": { + "users": [self.user1.id, self.user2.id], + "groups": [], + }, + "change": { + "users": [self.user1.id], + "groups": [], + }, + } + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.t1.id, self.t2.id], + "object_type": "tags", + "permissions": permissions, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(self.user1, get_users_with_perms(self.t1)) + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.c1.id], + "object_type": "correspondents", + "permissions": permissions, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(self.user1, get_users_with_perms(self.c1)) + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.dt1.id], + "object_type": "document_types", + "permissions": permissions, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(self.user1, get_users_with_perms(self.dt1)) + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.sp1.id], + "object_type": "storage_paths", + "permissions": permissions, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(self.user1, get_users_with_perms(self.sp1)) + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.t1.id, self.t2.id], + "object_type": "tags", + "owner": self.user3.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Tag.objects.get(pk=self.t2.id).owner, self.user3) + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.sp1.id], + "object_type": "storage_paths", + "owner": self.user3.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(StoragePath.objects.get(pk=self.sp1.id).owner, self.user3) + + def test_bulk_edit_object_permissions_insufficient_perms(self): + """ + GIVEN: + - Objects owned by user other than logged in user + WHEN: + - bulk_edit_object_perms API endpoint is called + THEN: + - User is not able to change permissions + """ + self.t1.owner = User.objects.get(username="temp_admin") + self.t1.save() + self.client.force_authenticate(user=self.user1) + + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.t1.id, self.t2.id], + "object_type": "tags", + "owner": self.user1.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content, b"Insufficient permissions") + + def test_bulk_edit_object_permissions_validation(self): + """ + GIVEN: + - Existing objects + WHEN: + - bulk_edit_object_perms API endpoint is called with invalid params + THEN: + - Validation fails + """ + # not a list + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": self.t1.id, + "object_type": "tags", + "owner": self.user1.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # not a list of ints + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": ["one"], + "object_type": "tags", + "owner": self.user1.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # duplicates + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [self.t1.id, self.t2.id, self.t1.id], + "object_type": "tags", + "owner": self.user1.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # not a valid object type + response = self.client.post( + "/api/bulk_edit_object_perms/", + json.dumps( + { + "objects": [1], + "object_type": "madeup", + "owner": self.user1.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/src/documents/tests/test_api_remote_version.py b/src/documents/tests/test_api_remote_version.py new file mode 100644 index 000000000..00d3e0775 --- /dev/null +++ b/src/documents/tests/test_api_remote_version.py @@ -0,0 +1,123 @@ +import json +import urllib.request +from unittest import mock +from unittest.mock import MagicMock + +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.tests.utils import DirectoriesMixin +from paperless import version + + +class TestApiRemoteVersion(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/remote_version/" + + def setUp(self): + super().setUp() + + @mock.patch("urllib.request.urlopen") + def test_remote_version_enabled_no_update_prefix(self, urlopen_mock): + cm = MagicMock() + cm.getcode.return_value = status.HTTP_200_OK + cm.read.return_value = json.dumps({"tag_name": "ngx-1.6.0"}).encode() + cm.__enter__.return_value = cm + urlopen_mock.return_value = cm + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.data, + { + "version": "1.6.0", + "update_available": False, + }, + ) + + @mock.patch("urllib.request.urlopen") + def test_remote_version_enabled_no_update_no_prefix(self, urlopen_mock): + cm = MagicMock() + cm.getcode.return_value = status.HTTP_200_OK + cm.read.return_value = json.dumps( + {"tag_name": version.__full_version_str__}, + ).encode() + cm.__enter__.return_value = cm + urlopen_mock.return_value = cm + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.data, + { + "version": version.__full_version_str__, + "update_available": False, + }, + ) + + @mock.patch("urllib.request.urlopen") + def test_remote_version_enabled_update(self, urlopen_mock): + new_version = ( + version.__version__[0], + version.__version__[1], + version.__version__[2] + 1, + ) + new_version_str = ".".join(map(str, new_version)) + + cm = MagicMock() + cm.getcode.return_value = status.HTTP_200_OK + cm.read.return_value = json.dumps( + {"tag_name": new_version_str}, + ).encode() + cm.__enter__.return_value = cm + urlopen_mock.return_value = cm + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.data, + { + "version": new_version_str, + "update_available": True, + }, + ) + + @mock.patch("urllib.request.urlopen") + def test_remote_version_bad_json(self, urlopen_mock): + cm = MagicMock() + cm.getcode.return_value = status.HTTP_200_OK + cm.read.return_value = b'{ "blah":' + cm.__enter__.return_value = cm + urlopen_mock.return_value = cm + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.data, + { + "version": "0.0.0", + "update_available": False, + }, + ) + + @mock.patch("urllib.request.urlopen") + def test_remote_version_exception(self, urlopen_mock): + cm = MagicMock() + cm.getcode.return_value = status.HTTP_200_OK + cm.read.side_effect = urllib.error.URLError("an error") + cm.__enter__.return_value = cm + urlopen_mock.return_value = cm + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.data, + { + "version": "0.0.0", + "update_available": False, + }, + ) diff --git a/src/documents/tests/test_api_tasks.py b/src/documents/tests/test_api_tasks.py new file mode 100644 index 000000000..52ffb09fe --- /dev/null +++ b/src/documents/tests/test_api_tasks.py @@ -0,0 +1,240 @@ +import uuid + +import celery +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import PaperlessTask +from documents.tests.utils import DirectoriesMixin + + +class TestTasks(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/tasks/" + ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/" + + def setUp(self): + super().setUp() + + self.user = User.objects.create_superuser(username="temp_admin") + self.client.force_authenticate(user=self.user) + + def test_get_tasks(self): + """ + GIVEN: + - Attempted celery tasks + WHEN: + - API call is made to get tasks + THEN: + - Attempting and pending tasks are serialized and provided + """ + + task1 = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_one.pdf", + ) + + task2 = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_two.pdf", + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + returned_task1 = response.data[1] + returned_task2 = response.data[0] + + self.assertEqual(returned_task1["task_id"], task1.task_id) + self.assertEqual(returned_task1["status"], celery.states.PENDING) + self.assertEqual(returned_task1["task_file_name"], task1.task_file_name) + + self.assertEqual(returned_task2["task_id"], task2.task_id) + self.assertEqual(returned_task2["status"], celery.states.PENDING) + self.assertEqual(returned_task2["task_file_name"], task2.task_file_name) + + def test_get_single_task_status(self): + """ + GIVEN + - Query parameter for a valid task ID + WHEN: + - API call is made to get task status + THEN: + - Single task data is returned + """ + + id1 = str(uuid.uuid4()) + task1 = PaperlessTask.objects.create( + task_id=id1, + task_file_name="task_one.pdf", + ) + + _ = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_two.pdf", + ) + + response = self.client.get(self.ENDPOINT + f"?task_id={id1}") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + returned_task1 = response.data[0] + + self.assertEqual(returned_task1["task_id"], task1.task_id) + + def test_get_single_task_status_not_valid(self): + """ + GIVEN + - Query parameter for a non-existent task ID + WHEN: + - API call is made to get task status + THEN: + - No task data is returned + """ + PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_one.pdf", + ) + + _ = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_two.pdf", + ) + + response = self.client.get(self.ENDPOINT + "?task_id=bad-task-id") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 0) + + def test_acknowledge_tasks(self): + """ + GIVEN: + - Attempted celery tasks + WHEN: + - API call is made to get mark task as acknowledged + THEN: + - Task is marked as acknowledged + """ + task = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_one.pdf", + ) + + response = self.client.get(self.ENDPOINT) + self.assertEqual(len(response.data), 1) + + response = self.client.post( + self.ENDPOINT_ACKNOWLEDGE, + {"tasks": [task.id]}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.get(self.ENDPOINT) + self.assertEqual(len(response.data), 0) + + def test_task_result_no_error(self): + """ + GIVEN: + - A celery task completed without error + WHEN: + - API call is made to get tasks + THEN: + - The returned data includes the task result + """ + PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_one.pdf", + status=celery.states.SUCCESS, + result="Success. New document id 1 created", + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + returned_data = response.data[0] + + self.assertEqual(returned_data["result"], "Success. New document id 1 created") + self.assertEqual(returned_data["related_document"], "1") + + def test_task_result_with_error(self): + """ + GIVEN: + - A celery task completed with an exception + WHEN: + - API call is made to get tasks + THEN: + - The returned result is the exception info + """ + PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_one.pdf", + status=celery.states.FAILURE, + result="test.pdf: Not consuming test.pdf: It is a duplicate.", + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + returned_data = response.data[0] + + self.assertEqual( + returned_data["result"], + "test.pdf: Not consuming test.pdf: It is a duplicate.", + ) + + def test_task_name_webui(self): + """ + GIVEN: + - Attempted celery task + - Task was created through the webui + WHEN: + - API call is made to get tasks + THEN: + - Returned data include the filename + """ + PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="test.pdf", + task_name="documents.tasks.some_task", + status=celery.states.SUCCESS, + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + returned_data = response.data[0] + + self.assertEqual(returned_data["task_file_name"], "test.pdf") + + def test_task_name_consume_folder(self): + """ + GIVEN: + - Attempted celery task + - Task was created through the consume folder + WHEN: + - API call is made to get tasks + THEN: + - Returned data include the filename + """ + PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="anothertest.pdf", + task_name="documents.tasks.some_task", + status=celery.states.SUCCESS, + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + returned_data = response.data[0] + + self.assertEqual(returned_data["task_file_name"], "anothertest.pdf") diff --git a/src/documents/tests/test_api_uisettings.py b/src/documents/tests/test_api_uisettings.py new file mode 100644 index 000000000..da9f2914d --- /dev/null +++ b/src/documents/tests/test_api_uisettings.py @@ -0,0 +1,65 @@ +import json + +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.tests.utils import DirectoriesMixin + + +class TestApiUiSettings(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/ui_settings/" + + def setUp(self): + super().setUp() + self.test_user = User.objects.create_superuser(username="test") + self.test_user.first_name = "Test" + self.test_user.last_name = "User" + self.test_user.save() + self.client.force_authenticate(user=self.test_user) + + def test_api_get_ui_settings(self): + response = self.client.get(self.ENDPOINT, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual( + response.data["user"], + { + "id": self.test_user.id, + "username": self.test_user.username, + "is_superuser": True, + "groups": [], + "first_name": self.test_user.first_name, + "last_name": self.test_user.last_name, + }, + ) + self.assertDictEqual( + response.data["settings"], + { + "update_checking": { + "backend_setting": "default", + }, + }, + ) + + def test_api_set_ui_settings(self): + settings = { + "settings": { + "dark_mode": { + "enabled": True, + }, + }, + } + + response = self.client.post( + self.ENDPOINT, + json.dumps(settings), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + ui_settings = self.test_user.ui_settings + self.assertDictEqual( + ui_settings.settings, + settings["settings"], + )