From d5ec76295455105fc5a9d21a77d6a16af06b9ddc Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 26 Nov 2020 18:55:05 +0100 Subject: [PATCH 1/6] couple changes to the consumer. --- src/documents/management/commands/document_consumer.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 4bfd78e8f..c25d0cfa9 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -95,14 +95,8 @@ class Command(BaseCommand): def handle(self, *args, **options): directory = options["directory"] - logging.getLogger(__name__).info( - f"Starting document consumer at {directory}") - for entry in os.scandir(directory): - if entry.is_file(): - async_task("documents.tasks.consume_file", - entry.path, - task_name=os.path.basename(entry.path)[:100]) + _consume(entry.path) if options["oneshot"]: return From 4bf0d834a0ad39f2cdfe188545bd65401f128f0f Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 26 Nov 2020 22:17:14 +0100 Subject: [PATCH 2/6] improved test cases. Python 3.6 compatibility. --- Pipfile | 3 ++ Pipfile.lock | 30 +++++++++++++- src/documents/tests/test_api.py | 30 ++++---------- src/documents/tests/test_classifier.py | 1 - src/documents/tests/test_consumer.py | 26 +++--------- .../tests/test_management_consumer.py | 37 ++++++++++------- src/documents/tests/test_matchables.py | 9 ++++ src/documents/tests/utils.py | 41 +++++++++++++++++++ 8 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 src/documents/tests/utils.py diff --git a/Pipfile b/Pipfile index a6169a2ba..105efd0ad 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,9 @@ url = "https://www.piwheels.org/simple" verify_ssl = true name = "piwheels" +[requires] +python_version = "3.6" + [packages] dateparser = "~=0.7.6" django = "~=3.1.3" diff --git a/Pipfile.lock b/Pipfile.lock index b10c414ed..918609845 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,10 +1,12 @@ { "_meta": { "hash": { - "sha256": "e9792119f687757dd388e73827ddd4216910327d5b65a8b950d4b202679c36eb" + "sha256": "d6432a18280c092c108e998f00bcd377c0c55ef18f26cb0b8eb64f9618b9f383" }, "pipfile-spec": 6, - "requires": {}, + "requires": { + "python_version": "3.6" + }, "sources": [ { "name": "pypi", @@ -701,6 +703,22 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, + "importlib-metadata": { + "hashes": [ + "sha256:030f3b1bdb823ecbe4a9659e14cc861ce5af403fe99863bae173ec5fe00ab132", + "sha256:caeee3603f5dcf567864d1be9b839b0bcfdf1383e3e7be33ce2dead8144ff19c" + ], + "markers": "python_version < '3.8'", + "version": "==2.1.0" + }, + "importlib-resources": { + "hashes": [ + "sha256:7b51f0106c8ec564b1bef3d9c588bc694ce2b92125bbb6278f4f2f5b54ec3592", + "sha256:a3d34a8464ce1d5d7c92b0ea4e921e696d86f2aa212e684451cb1482c8d84ed5" + ], + "markers": "python_version < '3.7'", + "version": "==3.3.0" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -1012,6 +1030,14 @@ ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.1" + }, + "zipp": { + "hashes": [ + "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", + "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" + ], + "markers": "python_version < '3.8'", + "version": "==3.4.0" } } } diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index c7e31e280..d9a2aac26 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -1,41 +1,24 @@ import os -import shutil import tempfile from unittest import mock from django.contrib.auth.models import User -from django.test import override_settings from pathvalidate import ValidationError from rest_framework.test import APITestCase from documents.models import Document, Correspondent, DocumentType, Tag +from documents.tests.utils import setup_directories, remove_dirs class DocumentApiTest(APITestCase): def setUp(self): - self.scratch_dir = tempfile.mkdtemp() - self.media_dir = tempfile.mkdtemp() - self.originals_dir = os.path.join(self.media_dir, "documents", "originals") - self.thumbnail_dir = os.path.join(self.media_dir, "documents", "thumbnails") - - os.makedirs(self.originals_dir, exist_ok=True) - os.makedirs(self.thumbnail_dir, exist_ok=True) - - override_settings( - SCRATCH_DIR=self.scratch_dir, - MEDIA_ROOT=self.media_dir, - ORIGINALS_DIR=self.originals_dir, - THUMBNAIL_DIR=self.thumbnail_dir - ).enable() + self.dirs = setup_directories() + self.addCleanup(remove_dirs, self.dirs) user = User.objects.create_superuser(username="temp_admin") self.client.force_login(user=user) - def tearDown(self): - shutil.rmtree(self.scratch_dir, ignore_errors=True) - shutil.rmtree(self.media_dir, ignore_errors=True) - def testDocuments(self): response = self.client.get("/api/documents/").data @@ -88,7 +71,7 @@ class DocumentApiTest(APITestCase): def test_document_actions(self): - _, filename = tempfile.mkstemp(dir=self.originals_dir) + _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) content = b"This is a test" content_thumbnail = b"thumbnail content" @@ -98,7 +81,7 @@ class DocumentApiTest(APITestCase): doc = Document.objects.create(title="none", filename=os.path.basename(filename), mime_type="application/pdf") - with open(os.path.join(self.thumbnail_dir, "{:07d}.png".format(doc.pk)), "wb") as f: + with open(os.path.join(self.dirs.thumbnail_dir, "{:07d}.png".format(doc.pk)), "wb") as f: f.write(content_thumbnail) response = self.client.get('/api/documents/{}/download/'.format(doc.pk)) @@ -227,7 +210,8 @@ class DocumentApiTest(APITestCase): m.assert_called_once() - self.assertEqual(m.call_args.kwargs['override_filename'], "simple.pdf") + args, kwargs = m.call_args + self.assertEqual(kwargs['override_filename'], "simple.pdf") @mock.patch("documents.forms.async_task") def test_upload_invalid_form(self, m): diff --git a/src/documents/tests/test_classifier.py b/src/documents/tests/test_classifier.py index 0f421bb32..e5e7d8639 100644 --- a/src/documents/tests/test_classifier.py +++ b/src/documents/tests/test_classifier.py @@ -11,7 +11,6 @@ from documents.models import Correspondent, Document, Tag, DocumentType class TestClassifier(TestCase): def setUp(self): - self.classifier = DocumentClassifier() def generate_test_data(self): diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index 6dab98d02..323f5051f 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -1,12 +1,12 @@ import os import re -import shutil import tempfile from unittest import mock from unittest.mock import MagicMock from django.test import TestCase, override_settings +from .utils import setup_directories, remove_dirs from ..consumer import Consumer, ConsumerError from ..models import FileInfo, Tag, Correspondent, DocumentType, Document from ..parsers import DocumentParser, ParseError @@ -411,23 +411,14 @@ def fake_magic_from_file(file, mime=False): class TestConsumer(TestCase): def make_dummy_parser(self, path, logging_group): - return DummyParser(path, logging_group, self.scratch_dir) + return DummyParser(path, logging_group, self.dirs.scratch_dir) def make_faulty_parser(self, path, logging_group): - return FaultyParser(path, logging_group, self.scratch_dir) + return FaultyParser(path, logging_group, self.dirs.scratch_dir) def setUp(self): - self.scratch_dir = tempfile.mkdtemp() - self.media_dir = tempfile.mkdtemp() - self.consumption_dir = tempfile.mkdtemp() - - override_settings( - SCRATCH_DIR=self.scratch_dir, - MEDIA_ROOT=self.media_dir, - ORIGINALS_DIR=os.path.join(self.media_dir, "documents", "originals"), - THUMBNAIL_DIR=os.path.join(self.media_dir, "documents", "thumbnails"), - CONSUMPTION_DIR=self.consumption_dir - ).enable() + self.dirs = setup_directories() + self.addCleanup(remove_dirs, self.dirs) patcher = mock.patch("documents.parsers.document_consumer_declaration.send") m = patcher.start() @@ -441,13 +432,8 @@ class TestConsumer(TestCase): self.consumer = Consumer() - def tearDown(self): - shutil.rmtree(self.scratch_dir, ignore_errors=True) - shutil.rmtree(self.media_dir, ignore_errors=True) - shutil.rmtree(self.consumption_dir, ignore_errors=True) - def get_test_file(self): - fd, f = tempfile.mkstemp(suffix=".pdf", dir=self.scratch_dir) + fd, f = tempfile.mkstemp(suffix=".pdf", dir=self.dirs.scratch_dir) return f def testNormalOperation(self): diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index bfb7520ee..33938d450 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -1,7 +1,6 @@ import filecmp import os import shutil -import tempfile from threading import Thread from time import sleep from unittest import mock @@ -11,6 +10,7 @@ from django.test import TestCase, override_settings from documents.consumer import ConsumerError from documents.management.commands import document_consumer +from documents.tests.utils import setup_directories, remove_dirs class ConsumerThread(Thread): @@ -41,9 +41,8 @@ class TestConsumer(TestCase): self.task_mock = patcher.start() self.addCleanup(patcher.stop) - self.consume_dir = tempfile.mkdtemp() - - override_settings(CONSUMPTION_DIR=self.consume_dir).enable() + self.dirs = setup_directories() + self.addCleanup(remove_dirs, self.dirs) def t_start(self): self.t = ConsumerThread() @@ -94,25 +93,29 @@ class TestConsumer(TestCase): def test_consume_file(self): self.t_start() - f = os.path.join(self.consume_dir, "my_file.pdf") + f = os.path.join(self.dirs.consumption_dir, "my_file.pdf") shutil.copy(self.sample_file, f) self.wait_for_task_mock_call() self.task_mock.assert_called_once() - self.assertEqual(self.task_mock.call_args.args[1], f) + + args, kwargs = self.task_mock.call_args + self.assertEqual(args[1], f) @override_settings(CONSUMER_POLLING=1) def test_consume_file_polling(self): self.test_consume_file() def test_consume_existing_file(self): - f = os.path.join(self.consume_dir, "my_file.pdf") + f = os.path.join(self.dirs.consumption_dir, "my_file.pdf") shutil.copy(self.sample_file, f) self.t_start() self.task_mock.assert_called_once() - self.assertEqual(self.task_mock.call_args.args[1], f) + + args, kwargs = self.task_mock.call_args + self.assertEqual(args[1], f) @override_settings(CONSUMER_POLLING=1) def test_consume_existing_file_polling(self): @@ -125,7 +128,7 @@ class TestConsumer(TestCase): self.t_start() - fname = os.path.join(self.consume_dir, "my_file.pdf") + fname = os.path.join(self.dirs.consumption_dir, "my_file.pdf") self.slow_write_file(fname) @@ -135,7 +138,8 @@ class TestConsumer(TestCase): self.task_mock.assert_called_once() - self.assertEqual(self.task_mock.call_args.args[1], fname) + args, kwargs = self.task_mock.call_args + self.assertEqual(args[1], fname) @override_settings(CONSUMER_POLLING=1) def test_slow_write_pdf_polling(self): @@ -148,8 +152,8 @@ class TestConsumer(TestCase): self.t_start() - fname = os.path.join(self.consume_dir, "my_file.~df") - fname2 = os.path.join(self.consume_dir, "my_file.pdf") + fname = os.path.join(self.dirs.consumption_dir, "my_file.~df") + fname2 = os.path.join(self.dirs.consumption_dir, "my_file.pdf") self.slow_write_file(fname) shutil.move(fname, fname2) @@ -157,7 +161,9 @@ class TestConsumer(TestCase): self.wait_for_task_mock_call() self.task_mock.assert_called_once() - self.assertEqual(self.task_mock.call_args.args[1], fname2) + + args, kwargs = self.task_mock.call_args + self.assertEqual(args[1], fname2) error_logger.assert_not_called() @@ -172,13 +178,14 @@ class TestConsumer(TestCase): self.t_start() - fname = os.path.join(self.consume_dir, "my_file.pdf") + fname = os.path.join(self.dirs.consumption_dir, "my_file.pdf") self.slow_write_file(fname, incomplete=True) self.wait_for_task_mock_call() self.task_mock.assert_called_once() - self.assertEqual(self.task_mock.call_args.args[1], fname) + args, kwargs = self.task_mock.call_args + self.assertEqual(args[1], fname) # assert that we have an error logged with this invalid file. error_logger.assert_called_once() diff --git a/src/documents/tests/test_matchables.py b/src/documents/tests/test_matchables.py index 24e285ae7..4e4a3e7dc 100644 --- a/src/documents/tests/test_matchables.py +++ b/src/documents/tests/test_matchables.py @@ -1,3 +1,5 @@ +import shutil +import tempfile from random import randint from django.contrib.admin.models import LogEntry @@ -215,6 +217,13 @@ class TestDocumentConsumptionFinishedSignal(TestCase): self.doc_contains = Document.objects.create( content="I contain the keyword.", mime_type="application/pdf") + self.index_dir = tempfile.mkdtemp() + # TODO: we should not need the index here. + override_settings(INDEX_DIR=self.index_dir).enable() + + def tearDown(self) -> None: + shutil.rmtree(self.index_dir, ignore_errors=True) + def test_tag_applied_any(self): t1 = Tag.objects.create( name="test", match="keyword", matching_algorithm=Tag.MATCH_ANY) diff --git a/src/documents/tests/utils.py b/src/documents/tests/utils.py new file mode 100644 index 000000000..7b0938ee3 --- /dev/null +++ b/src/documents/tests/utils.py @@ -0,0 +1,41 @@ +import os +import shutil +import tempfile +from collections import namedtuple + +from django.test import override_settings + + +def setup_directories(): + + dirs = namedtuple("Dirs", ()) + + dirs.data_dir = tempfile.mkdtemp() + dirs.scratch_dir = tempfile.mkdtemp() + dirs.media_dir = tempfile.mkdtemp() + dirs.consumption_dir = tempfile.mkdtemp() + dirs.index_dir = os.path.join(dirs.data_dir, "documents", "originals") + dirs.originals_dir = os.path.join(dirs.media_dir, "documents", "originals") + dirs.thumbnail_dir = os.path.join(dirs.media_dir, "documents", "thumbnails") + os.makedirs(dirs.index_dir) + os.makedirs(dirs.originals_dir) + os.makedirs(dirs.thumbnail_dir) + + override_settings( + DATA_DIR=dirs.data_dir, + SCRATCH_DIR=dirs.scratch_dir, + MEDIA_ROOT=dirs.media_dir, + ORIGINALS_DIR=dirs.originals_dir, + THUMBNAIL_DIR=dirs.thumbnail_dir, + CONSUMPTION_DIR=dirs.consumption_dir, + INDEX_DIR=dirs.index_dir + ).enable() + + return dirs + + +def remove_dirs(dirs): + shutil.rmtree(dirs.media_dir, ignore_errors=True) + shutil.rmtree(dirs.data_dir, ignore_errors=True) + shutil.rmtree(dirs.scratch_dir, ignore_errors=True) + shutil.rmtree(dirs.consumption_dir, ignore_errors=True) From b589b7a5dcc8307a743c5db50cf5bd09d8abc2b2 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 26 Nov 2020 22:18:30 +0100 Subject: [PATCH 3/6] The index is now recreated in case loading fails. --- src/documents/index.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/documents/index.py b/src/documents/index.py index cf312cbcc..a6c3abba8 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -64,15 +64,18 @@ def get_schema(): def open_index(recreate=False): - if exists_in(settings.INDEX_DIR) and not recreate: - return open_dir(settings.INDEX_DIR) - else: - # TODO: this is not thread safe. If 2 instances try to create the index - # at the same time, this fails. This currently prevents parallel - # tests. - if not os.path.isdir(settings.INDEX_DIR): - os.makedirs(settings.INDEX_DIR, exist_ok=True) - return create_in(settings.INDEX_DIR, get_schema()) + # TODO: this is not thread safe. If 2 instances try to create the index + # at the same time, this fails. This currently prevents parallel + # tests. + try: + if exists_in(settings.INDEX_DIR) and not recreate: + return open_dir(settings.INDEX_DIR) + except Exception as e: + logger.error(f"Error while opening the index: {e}, recreating.") + + if not os.path.isdir(settings.INDEX_DIR): + os.makedirs(settings.INDEX_DIR, exist_ok=True) + return create_in(settings.INDEX_DIR, get_schema()) def update_document(writer, doc): From 6454df57bf4937bece5e53035a20f0376f51d75f Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 26 Nov 2020 23:09:17 +0100 Subject: [PATCH 4/6] removed some obsolete exporter code. --- .../management/commands/document_exporter.py | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index f86462119..971481ff8 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -22,13 +22,6 @@ class Command(Renderable, BaseCommand): def add_arguments(self, parser): parser.add_argument("target") - parser.add_argument( - "--legacy", - action="store_true", - help="Don't try to export all of the document data, just dump the " - "original document files out in a format that makes " - "re-consuming them easy." - ) def __init__(self, *args, **kwargs): BaseCommand.__init__(self, *args, **kwargs) @@ -44,10 +37,7 @@ class Command(Renderable, BaseCommand): if not os.access(self.target, os.W_OK): raise CommandError("That path doesn't appear to be writable") - if options["legacy"]: - self.dump_legacy() - else: - self.dump() + self.dump() def dump(self): @@ -102,33 +92,3 @@ class Command(Renderable, BaseCommand): with open(os.path.join(self.target, "manifest.json"), "w") as f: json.dump(manifest, f, indent=2) - - def dump_legacy(self): - - for document in Document.objects.all(): - - target = os.path.join( - self.target, self._get_legacy_file_name(document)) - - print("Exporting: {}".format(target)) - - with open(target, "wb") as f: - f.write(GnuPG.decrypted(document.source_file)) - t = int(time.mktime(document.created.timetuple())) - os.utime(target, times=(t, t)) - - @staticmethod - def _get_legacy_file_name(doc): - - if not doc.correspondent and not doc.title: - return os.path.basename(doc.source_path) - - created = doc.created.strftime("%Y%m%d%H%M%SZ") - tags = ",".join([t.slug for t in doc.tags.all()]) - - if tags: - return "{} - {} - {} - {}{}".format( - created, doc.correspondent, doc.title, tags, doc.file_type) - - return "{} - {} - {}{}".format( - created, doc.correspondent, doc.title, doc.file_type) From db0f7649d1784cfbe1c56701c117880170d15206 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 26 Nov 2020 23:56:57 +0100 Subject: [PATCH 5/6] more tests. --- .../management/commands/decrypt_documents.py | 2 +- .../tests/samples/originals/0000001.pdf | Bin 0 -> 22926 bytes .../tests/samples/originals/0000002.pdf.gpg | Bin 0 -> 18961 bytes src/documents/tests/samples/thumb/0000001.png | Bin 0 -> 7913 bytes .../tests/samples/thumb/0000002.png.gpg | Bin 0 -> 7141 bytes .../tests/test_management_decrypt.py | 56 ++++++++++++++++++ .../tests/test_management_exporter.py | 53 +++++++++++++++++ src/setup.cfg | 2 +- 8 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/documents/tests/samples/originals/0000001.pdf create mode 100644 src/documents/tests/samples/originals/0000002.pdf.gpg create mode 100644 src/documents/tests/samples/thumb/0000001.png create mode 100644 src/documents/tests/samples/thumb/0000002.png.gpg create mode 100644 src/documents/tests/test_management_decrypt.py create mode 100644 src/documents/tests/test_management_exporter.py diff --git a/src/documents/management/commands/decrypt_documents.py b/src/documents/management/commands/decrypt_documents.py index f9b4edcdc..2287bfa72 100644 --- a/src/documents/management/commands/decrypt_documents.py +++ b/src/documents/management/commands/decrypt_documents.py @@ -74,7 +74,7 @@ class Command(BaseCommand): f"Abort: encrypted file {document.source_path} does not " f"end with .gpg") - document.filename = os.path.splitext(document.source_path)[0] + document.filename = os.path.splitext(document.filename)[0] with open(document.source_path, "wb") as f: f.write(raw_document) diff --git a/src/documents/tests/samples/originals/0000001.pdf b/src/documents/tests/samples/originals/0000001.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e450de48269ce43785b8344c63e233a1794abae6 GIT binary patch literal 22926 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 literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/originals/0000002.pdf.gpg b/src/documents/tests/samples/originals/0000002.pdf.gpg new file mode 100644 index 0000000000000000000000000000000000000000..0322a8039f0a1e401142c7fb9d5dc08c60ad105c GIT binary patch literal 18961 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 Ruvbl>s;I6|ENAlKe*j2SP+b53 literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/thumb/0000002.png.gpg b/src/documents/tests/samples/thumb/0000002.png.gpg new file mode 100644 index 0000000000000000000000000000000000000000..8a61a91265589106b192d25f557987f1a574faef GIT binary patch literal 7141 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 b)0CCg#qy=b28v$ju#MT_mQiT|m*D(i(OLi# literal 0 HcmV?d00001 diff --git a/src/documents/tests/test_management_decrypt.py b/src/documents/tests/test_management_decrypt.py new file mode 100644 index 000000000..326276389 --- /dev/null +++ b/src/documents/tests/test_management_decrypt.py @@ -0,0 +1,56 @@ +import hashlib +import json +import os +import shutil +import tempfile +from unittest import mock + +from django.core.management import call_command +from django.test import TestCase, override_settings + +from documents.management.commands import document_exporter +from documents.models import Document, Tag, DocumentType, Correspondent + + +class TestDecryptDocuments(TestCase): + + @override_settings( + ORIGINALS_DIR=os.path.join(os.path.dirname(__file__), "samples", "originals"), + THUMBNAIL_DIR=os.path.join(os.path.dirname(__file__), "samples", "thumb"), + PASSPHRASE="test" + ) + @mock.patch("documents.management.commands.decrypt_documents.input") + def test_decrypt(self, m): + + media_dir = tempfile.mkdtemp() + originals_dir = os.path.join(media_dir, "documents", "originals") + thumb_dir = os.path.join(media_dir, "documents", "thumbnails") + os.makedirs(originals_dir, exist_ok=True) + os.makedirs(thumb_dir, exist_ok=True) + + override_settings( + ORIGINALS_DIR=originals_dir, + THUMBNAIL_DIR=thumb_dir, + PASSPHRASE="test" + ).enable() + + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "originals", "0000002.pdf.gpg"), os.path.join(originals_dir, "0000002.pdf.gpg")) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "thumb", "0000002.png.gpg"), os.path.join(thumb_dir, "0000002.png.gpg")) + + Document.objects.create(checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", id=2, mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) + + call_command('decrypt_documents') + + doc = Document.objects.get(id=2) + + self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) + self.assertEqual(doc.filename, "0000002.pdf") + self.assertTrue(os.path.isfile(os.path.join(originals_dir, "0000002.pdf"))) + self.assertTrue(os.path.isfile(doc.source_path)) + self.assertTrue(os.path.isfile(os.path.join(thumb_dir, "0000002.png"))) + self.assertTrue(os.path.isfile(doc.thumbnail_path)) + + with doc.source_file as f: + checksum = hashlib.md5(f.read()).hexdigest() + self.assertEqual(checksum, doc.checksum) + diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py new file mode 100644 index 000000000..c8d1490d2 --- /dev/null +++ b/src/documents/tests/test_management_exporter.py @@ -0,0 +1,53 @@ +import hashlib +import json +import os +import tempfile + +from django.core.management import call_command +from django.test import TestCase, override_settings + +from documents.management.commands import document_exporter +from documents.models import Document, Tag, DocumentType, Correspondent + + +class TestExporter(TestCase): + + @override_settings( + ORIGINALS_DIR=os.path.join(os.path.dirname(__file__), "samples", "originals"), + THUMBNAIL_DIR=os.path.join(os.path.dirname(__file__), "samples", "thumb"), + PASSPHRASE="test" + ) + def test_exporter(self): + file = os.path.join(os.path.dirname(__file__), "samples", "originals", "0000001.pdf") + + with open(file, "rb") as f: + checksum = hashlib.md5(f.read()).hexdigest() + + Document.objects.create(checksum=checksum, title="wow", filename="0000001.pdf", id=1, mime_type="application/pdf") + Document.objects.create(checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", id=2, mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) + Tag.objects.create(name="t") + DocumentType.objects.create(name="dt") + Correspondent.objects.create(name="c") + + target = tempfile.mkdtemp() + + call_command('document_exporter', target) + + with open(os.path.join(target, "manifest.json")) as f: + manifest = json.load(f) + + self.assertEqual(len(manifest), 5) + + for element in manifest: + if element['model'] == 'documents.document': + fname = os.path.join(target, element[document_exporter.EXPORTER_FILE_NAME]) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.exists(os.path.join(target, element[document_exporter.EXPORTER_THUMBNAIL_NAME]))) + + with open(fname, "rb") as f: + checksum = hashlib.md5(f.read()).hexdigest() + self.assertEqual(checksum, element['fields']['checksum']) + + Document.objects.create(checksum="AAAAAAAAAAAAAAAAA", title="wow", filename="0000004.pdf", id=3, mime_type="application/pdf") + + self.assertRaises(FileNotFoundError, call_command, 'document_exporter', target) diff --git a/src/setup.cfg b/src/setup.cfg index 4b0a216f5..b540f9efe 100644 --- a/src/setup.cfg +++ b/src/setup.cfg @@ -3,7 +3,7 @@ exclude = migrations, paperless/settings.py, .tox, */tests/* [tool:pytest] DJANGO_SETTINGS_MODULE=paperless.settings -addopts = --pythonwarnings=all +addopts = --pythonwarnings=all --cov --cov-report=html env = PAPERLESS_SECRET=paperless PAPERLESS_EMAIL_SECRET=paperless From a4bd2d687ed193311bd1b40ee413d11efdfd2378 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Fri, 27 Nov 2020 00:05:29 +0100 Subject: [PATCH 6/6] add empty test case. --- src/documents/tests/test_document_retagger.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/documents/tests/test_document_retagger.py diff --git a/src/documents/tests/test_document_retagger.py b/src/documents/tests/test_document_retagger.py new file mode 100644 index 000000000..6fe40d7e9 --- /dev/null +++ b/src/documents/tests/test_document_retagger.py @@ -0,0 +1,7 @@ +from django.test import TestCase + + +class TestRetagger(TestCase): + + def test_overwrite(self): + pass