Chore: Fix bugs in the test suite (#14244)

* Fix: redirect SHARE_LINK_BUNDLE_DIR to the test temp layout instead of the real media directory

* Fix: include f_to in test_filters subTest labels so each of the 8 cases reports distinctly

* Fix: run the post_consume error-log assertion after the raising call and match the actual paperless_mail logger name

* Fix: rename the blank-password workflow test to match its behavior and add a real wrong-password-fails test

* Fix: use the created social account's actual pk and remove an accidental tuple wrapping the mock provider

* Fix: assert against the created documents' actual pks instead of hardcoded 1 and 2

* Fix: assert test_compression actually produces a valid LZMA-compressed zip

* Fix: clear os.environ when patching PAPERLESS_ADMIN_* vars so a host-set value can't leak into the no-user test

* Fix: restore MIDDLEWARE, AUTHENTICATION_BACKENDS and REST_FRAMEWORK auth classes after each remote-user settings test instead of leaking the mutation into later tests

* Fix: use a guaranteed-nonexistent temp path instead of hardcoded /tmp/foo/bar in test_export_target_not_exists
This commit is contained in:
Trenton H
2026-09-23 19:11:58 +00:00
committed by GitHub
parent 969c2ea0e2
commit b457610ffb
10 changed files with 124 additions and 45 deletions
@@ -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:
+11 -14
View File
@@ -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",
},
@@ -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:
"""
+8 -8
View File
@@ -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:
"""
@@ -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",
@@ -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:
+57 -3
View File
@@ -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,
@@ -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:
+11 -5
View File
@@ -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")
+4
View File
@@ -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,
)