mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-25 02:40:32 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user