diff --git a/src/documents/tests/test_api_bulk_download.py b/src/documents/tests/test_api_bulk_download.py index e0c29d3cd..b031a5105 100644 --- a/src/documents/tests/test_api_bulk_download.py +++ b/src/documents/tests/test_api_bulk_download.py @@ -166,7 +166,15 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase): ), content_type="application/json", ) - response.close() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + + with zipfile.ZipFile(io.BytesIO(read_streaming_response(response))) as zipf: + self.assertEqual(zipf.infolist()[0].compress_type, zipfile.ZIP_LZMA) + + with self.doc2.source_file as f: + self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf")) @override_settings(FILENAME_FORMAT="{correspondent}/{title}") def test_formatted_download_originals(self) -> None: diff --git a/src/documents/tests/test_api_profile.py b/src/documents/tests/test_api_profile.py index 24ddda5da..31588dd56 100644 --- a/src/documents/tests/test_api_profile.py +++ b/src/documents/tests/test_api_profile.py @@ -64,16 +64,15 @@ class TestApiProfile(DirectoriesMixin, APITestCase): ) self.client.force_authenticate(user=self.user) - def setupSocialAccount(self) -> None: + def setupSocialAccount(self) -> SocialAccount: SocialApp.objects.create( name="Keycloak", provider="openid_connect", provider_id="keycloak-test", ) - self.user.socialaccount_set.add( - SocialAccount(uid="123456789", provider="keycloak-test"), - bulk=False, - ) + social_account = SocialAccount(uid="123456789", provider="keycloak-test") + self.user.socialaccount_set.add(social_account, bulk=False) + return social_account def test_get_profile(self) -> None: """ @@ -111,19 +110,17 @@ class TestApiProfile(DirectoriesMixin, APITestCase): THEN: - Profile is returned with social accounts """ - self.setupSocialAccount() + social_account = self.setupSocialAccount() - openid_provider = ( - MockOpenIDConnectProvider( - app=SocialApp.objects.get(provider_id="keycloak-test"), - ), + openid_provider = MockOpenIDConnectProvider( + app=SocialApp.objects.get(provider_id="keycloak-test"), ) mock_list_providers.return_value = [ openid_provider, ] mock_get_provider_account.return_value = MockOpenIDConnectProviderAccount( mock_social_account_dict={ - "name": openid_provider[0].name, + "name": openid_provider.name, }, ) @@ -135,7 +132,7 @@ class TestApiProfile(DirectoriesMixin, APITestCase): response.data["social_accounts"], [ { - "id": 1, + "id": social_account.pk, "provider": "keycloak-test", "name": "Keycloak", }, @@ -152,7 +149,7 @@ class TestApiProfile(DirectoriesMixin, APITestCase): THEN: - Profile is returned with "Unknown App" as name """ - self.setupSocialAccount() + social_account = self.setupSocialAccount() # Remove the social app SocialApp.objects.get(provider_id="keycloak-test").delete() @@ -165,7 +162,7 @@ class TestApiProfile(DirectoriesMixin, APITestCase): response.data["social_accounts"], [ { - "id": 1, + "id": social_account.pk, "provider": "keycloak-test", "name": "Unknown App", }, diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index 428960025..137744569 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -677,12 +677,13 @@ class TestExportImport( THEN: - Error is raised """ - args = ["document_exporter", "/tmp/foo/bar"] + with tempfile.TemporaryDirectory() as tmp_dir: + args = ["document_exporter", str(Path(tmp_dir) / "does-not-exist")] - with self.assertRaises(CommandError) as e: - call_command(*args, skip_checks=True) + with self.assertRaises(CommandError) as e: + call_command(*args, skip_checks=True) - self.assertEqual("That path doesn't exist", str(e.exception)) + self.assertEqual("That path doesn't exist", str(e.exception)) def test_export_target_exists_but_is_file(self) -> None: """ diff --git a/src/documents/tests/test_management_fuzzy.py b/src/documents/tests/test_management_fuzzy.py index 1921ee795..fdfe459c7 100644 --- a/src/documents/tests/test_management_fuzzy.py +++ b/src/documents/tests/test_management_fuzzy.py @@ -123,14 +123,14 @@ class TestFuzzyMatchCommand(TestCase): - Output contains clickable links to the documents instead of titles """ # Content similarity is 86.667 - Document.objects.create( + doc1 = Document.objects.create( checksum="BEEFCAFE", title="A", content="first document scanned by bob", mime_type="application/pdf", filename="test.pdf", ) - Document.objects.create( + doc2 = Document.objects.create( checksum="DEADBEAF", title="A", content="first document scanned by alice", @@ -145,8 +145,8 @@ class TestFuzzyMatchCommand(TestCase): "http://localhost:8000", ) self.assertIn("Found 1 matching pair(s)", stdout) - self.assertIn("http://localhost:8000/documents/1/details", stdout) - self.assertIn("http://localhost:8000/documents/2/details", stdout) + self.assertIn(f"http://localhost:8000/documents/{doc1.pk}/details", stdout) + self.assertIn(f"http://localhost:8000/documents/{doc2.pk}/details", stdout) def test_with_3_matches(self) -> None: """ @@ -198,14 +198,14 @@ class TestFuzzyMatchCommand(TestCase): - Documents 1 and 2 remain """ # Content similarity is 86.667 - Document.objects.create( + doc1 = Document.objects.create( checksum="BEEFCAFE", title="A", content="first document scanned by bob", mime_type="application/pdf", filename="test.pdf", ) - Document.objects.create( + doc2 = Document.objects.create( checksum="DEADBEAF", title="A", content="second document scanned by alice", @@ -235,8 +235,8 @@ class TestFuzzyMatchCommand(TestCase): self.assertIn("Deleting 1 document(s)", stdout) self.assertEqual(Document.objects.count(), 2) - self.assertIsNotNone(Document.objects.get(pk=1)) - self.assertIsNotNone(Document.objects.get(pk=2)) + self.assertIsNotNone(Document.objects.get(pk=doc1.pk)) + self.assertIsNotNone(Document.objects.get(pk=doc2.pk)) def test_document_deletion_cancelled(self) -> None: """ diff --git a/src/documents/tests/test_management_superuser.py b/src/documents/tests/test_management_superuser.py index 620f46aef..2c6f99abe 100644 --- a/src/documents/tests/test_management_superuser.py +++ b/src/documents/tests/test_management_superuser.py @@ -14,7 +14,7 @@ from paperless_testing.dirs import DirectoriesMixin class TestManageSuperUser(DirectoriesMixin, TestCase): def call_command(self, environ): out = StringIO() - with mock.patch.dict(os.environ, environ): + with mock.patch.dict(os.environ, environ, clear=True): call_command( "manage_superuser", "--no-color", diff --git a/src/documents/tests/test_share_link_bundles.py b/src/documents/tests/test_share_link_bundles.py index f451e58ba..55cc61bde 100644 --- a/src/documents/tests/test_share_link_bundles.py +++ b/src/documents/tests/test_share_link_bundles.py @@ -339,15 +339,6 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase): ) self.document.archive_checksum = "" self.document.save() - self.addCleanup( - setattr, - settings, - "SHARE_LINK_BUNDLE_DIR", - settings.SHARE_LINK_BUNDLE_DIR, - ) - settings.SHARE_LINK_BUNDLE_DIR = ( - Path(settings.MEDIA_ROOT) / "documents" / "share_link_bundles" - ) def _write_document_file(self, *, archive: bool, content: bytes) -> Path: if archive: diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index ccfeca588..3bd9e0775 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -4459,18 +4459,18 @@ class TestWorkflows( ) @mock.patch("documents.bulk_edit.remove_password") - def test_password_removal_action_fails_without_correct_password( + def test_password_removal_action_skips_blank_and_whitespace_passwords( self, mock_remove_password, ) -> None: """ GIVEN: - Workflow password removal action - - No correct password provided + - Only blank and whitespace-only passwords configured WHEN: - Document updated triggering the workflow THEN: - - Password removal is attempted for all passwords and fails + - Password removal is not attempted """ doc = Document.objects.create( title="Protected", @@ -4491,6 +4491,60 @@ class TestWorkflows( mock_remove_password.assert_not_called() + @mock.patch("documents.bulk_edit.remove_password") + def test_password_removal_action_fails_without_correct_password( + self, + mock_remove_password, + ) -> None: + """ + GIVEN: + - Workflow password removal action + - No configured password is correct + WHEN: + - Document updated triggering the workflow + THEN: + - Password removal is attempted for every configured password and fails + """ + doc = Document.objects.create( + title="Protected", + checksum="pw-checksum-3", + ) + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, + ) + action = WorkflowAction.objects.create( + type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL, + passwords=["wrong", "also-wrong"], + ) + workflow = Workflow.objects.create(name="Password workflow wrong passwords") + workflow.triggers.add(trigger) + workflow.actions.add(action) + + mock_remove_password.side_effect = ValueError("wrong password") + + with self.assertLogs("paperless.workflows.actions", level="ERROR"): + run_workflows(trigger.type, doc) + + assert mock_remove_password.call_count == 2 + mock_remove_password.assert_has_calls( + [ + mock.call( + [doc.id], + password="wrong", + update_document=True, + user=doc.owner, + source_paths_by_id=None, + ), + mock.call( + [doc.id], + password="also-wrong", + update_document=True, + user=doc.owner, + source_paths_by_id=None, + ), + ], + ) + @mock.patch("documents.bulk_edit.remove_password") def test_password_removal_action_skips_without_passwords( self, diff --git a/src/paperless/tests/settings/test_remote_user.py b/src/paperless/tests/settings/test_remote_user.py index c9d62844c..f4bd59958 100644 --- a/src/paperless/tests/settings/test_remote_user.py +++ b/src/paperless/tests/settings/test_remote_user.py @@ -17,6 +17,24 @@ class TestRemoteUser(DirectoriesMixin, APITestCase): self.user = UserFactory(username="temp_admin", superuser=True) + # _parse_remote_user_settings() mutates these shared lists in place, + # so undo that after the test instead of leaking remote-user auth + # into every test that runs afterward. + original_middleware = list(settings.MIDDLEWARE) + original_auth_backends = list(settings.AUTHENTICATION_BACKENDS) + original_auth_classes = list( + settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"], + ) + + def _restore_remote_user_settings() -> None: + settings.MIDDLEWARE[:] = original_middleware + settings.AUTHENTICATION_BACKENDS[:] = original_auth_backends + settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"][:] = ( + original_auth_classes + ) + + self.addCleanup(_restore_remote_user_settings) + def test_remote_user(self) -> None: """ GIVEN: diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index c249a9d62..2278df11d 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -1565,7 +1565,12 @@ class TestMail( ("electronic", None, "invoices@mycompany.com", None, 1), (None, "amazon", "me@myselfandi.com", None, 1), ]: - with self.subTest(f_body=f_body, f_from=f_from, f_subject=f_subject): + with self.subTest( + f_body=f_body, + f_from=f_from, + f_to=f_to, + f_subject=f_subject, + ): MailRule.objects.all().delete() _ = MailRule.objects.create( name="testrule3", @@ -1806,7 +1811,7 @@ class TestPostConsumeAction(TestCase): with ( self.assertRaises(errors.ImapToolsError), - self.assertLogs("paperless.mail", level="ERROR") as cm, + self.assertLogs("paperless_mail", level="ERROR") as cm, ): apply_mail_action( result=[], @@ -1815,9 +1820,10 @@ class TestPostConsumeAction(TestCase): message_subject=self.message_subject, message_date=self.message_date, ) - error_str = cm.output[0] - expected_str = "Error while processing mail action during post_consume" - self.assertIn(expected_str, error_str) + + error_str = cm.output[0] + expected_str = "Error while processing mail action during post_consume" + self.assertIn(expected_str, error_str) processed_mail = ProcessedMail.objects.get(uid=self.message_uid) self.assertEqual(processed_mail.status, "FAILED") diff --git a/src/paperless_testing/dirs.py b/src/paperless_testing/dirs.py index 794708ad6..81582ef5c 100644 --- a/src/paperless_testing/dirs.py +++ b/src/paperless_testing/dirs.py @@ -37,6 +37,7 @@ class PaperlessDirs: logging_dir: Path model_file: Path media_lock: Path + share_link_bundle_dir: Path class DirSettings(TypedDict): @@ -54,6 +55,7 @@ class DirSettings(TypedDict): STATIC_ROOT: Path MODEL_FILE: Path MEDIA_LOCK: Path + SHARE_LINK_BUNDLE_DIR: Path def build_paperless_dirs(root: Path) -> PaperlessDirs: @@ -75,6 +77,7 @@ def build_paperless_dirs(root: Path) -> PaperlessDirs: logging_dir=data_dir / "log", model_file=data_dir / "classification_model.pickle", media_lock=media_dir / "media.lock", + share_link_bundle_dir=documents_dir / "share_link_bundles", ) for directory in ( @@ -109,6 +112,7 @@ def dirs_settings(dirs: PaperlessDirs) -> DirSettings: STATIC_ROOT=dirs.static_dir, MODEL_FILE=dirs.model_file, MEDIA_LOCK=dirs.media_lock, + SHARE_LINK_BUNDLE_DIR=dirs.share_link_bundle_dir, )