diff --git a/src/paperless/tests/test_checks.py b/src/paperless/tests/test_checks.py index c8596a396..6e2a48dac 100644 --- a/src/paperless/tests/test_checks.py +++ b/src/paperless/tests/test_checks.py @@ -1,16 +1,13 @@ import os +from dataclasses import dataclass from pathlib import Path -from unittest import mock import pytest from django.core.checks import Error from django.core.checks import Warning -from django.test import TestCase -from django.test import override_settings +from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture -from documents.tests.utils import DirectoriesMixin -from documents.tests.utils import FileSystemAssertsMixin from paperless.checks import audit_log_check from paperless.checks import binaries_check from paperless.checks import check_deprecated_db_settings @@ -20,54 +17,84 @@ from paperless.checks import paths_check from paperless.checks import settings_values_check -class TestChecks(DirectoriesMixin, TestCase): - def test_binaries(self) -> None: - self.assertEqual(binaries_check(None), []) +@dataclass(frozen=True, slots=True) +class PaperlessTestDirs: + data_dir: Path + media_dir: Path + consumption_dir: Path - @override_settings(CONVERT_BINARY="uuuhh") - def test_binaries_fail(self) -> None: - self.assertEqual(len(binaries_check(None)), 1) - def test_paths_check(self) -> None: - self.assertEqual(paths_check(None), []) +# TODO: consolidate with documents/tests/conftest.py PaperlessDirs/paperless_dirs +# once the paperless and documents test suites are ready to share fixtures. +@pytest.fixture() +def directories(tmp_path: Path, settings: SettingsWrapper) -> PaperlessTestDirs: + data_dir = tmp_path / "data" + media_dir = tmp_path / "media" + consumption_dir = tmp_path / "consumption" - @override_settings( - MEDIA_ROOT=Path("uuh"), - DATA_DIR=Path("whatever"), - CONSUMPTION_DIR=Path("idontcare"), + for d in (data_dir, media_dir, consumption_dir): + d.mkdir() + + settings.DATA_DIR = data_dir + settings.MEDIA_ROOT = media_dir + settings.CONSUMPTION_DIR = consumption_dir + + return PaperlessTestDirs( + data_dir=data_dir, + media_dir=media_dir, + consumption_dir=consumption_dir, ) - def test_paths_check_dont_exist(self) -> None: - msgs = paths_check(None) - self.assertEqual(len(msgs), 3, str(msgs)) - for msg in msgs: - self.assertTrue(msg.msg.endswith("is set but doesn't exist.")) - def test_paths_check_no_access(self) -> None: - Path(self.dirs.data_dir).chmod(0o000) - Path(self.dirs.media_dir).chmod(0o000) - Path(self.dirs.consumption_dir).chmod(0o000) +class TestChecks: + def test_binaries(self) -> None: + assert binaries_check(None) == [] - self.addCleanup(os.chmod, self.dirs.data_dir, 0o777) - self.addCleanup(os.chmod, self.dirs.media_dir, 0o777) - self.addCleanup(os.chmod, self.dirs.consumption_dir, 0o777) + def test_binaries_fail(self, settings: SettingsWrapper) -> None: + settings.CONVERT_BINARY = "uuuhh" + assert len(binaries_check(None)) == 1 + + @pytest.mark.usefixtures("directories") + def test_paths_check(self) -> None: + assert paths_check(None) == [] + + def test_paths_check_dont_exist(self, settings: SettingsWrapper) -> None: + settings.MEDIA_ROOT = Path("uuh") + settings.DATA_DIR = Path("whatever") + settings.CONSUMPTION_DIR = Path("idontcare") msgs = paths_check(None) - self.assertEqual(len(msgs), 3) + assert len(msgs) == 3, str(msgs) for msg in msgs: - self.assertTrue(msg.msg.endswith("is not writeable")) + assert msg.msg.endswith("is set but doesn't exist.") - @override_settings(DEBUG=False) - def test_debug_disabled(self) -> None: - self.assertEqual(debug_mode_check(None), []) + def test_paths_check_no_access(self, directories: PaperlessTestDirs) -> None: + directories.data_dir.chmod(0o000) + directories.media_dir.chmod(0o000) + directories.consumption_dir.chmod(0o000) - @override_settings(DEBUG=True) - def test_debug_enabled(self) -> None: - self.assertEqual(len(debug_mode_check(None)), 1) + try: + msgs = paths_check(None) + finally: + directories.data_dir.chmod(0o777) + directories.media_dir.chmod(0o777) + directories.consumption_dir.chmod(0o777) + + assert len(msgs) == 3 + for msg in msgs: + assert msg.msg.endswith("is not writeable") + + def test_debug_disabled(self, settings: SettingsWrapper) -> None: + settings.DEBUG = False + assert debug_mode_check(None) == [] + + def test_debug_enabled(self, settings: SettingsWrapper) -> None: + settings.DEBUG = True + assert len(debug_mode_check(None)) == 1 -class TestSettingsChecksAgainstDefaults(DirectoriesMixin, TestCase): +class TestSettingsChecksAgainstDefaults: def test_all_valid(self) -> None: """ GIVEN: @@ -78,104 +105,71 @@ class TestSettingsChecksAgainstDefaults(DirectoriesMixin, TestCase): - No system check errors reported """ msgs = settings_values_check(None) - self.assertEqual(len(msgs), 0) + assert len(msgs) == 0 -class TestOcrSettingsChecks(DirectoriesMixin, TestCase): - @override_settings(OCR_OUTPUT_TYPE="notapdf") - def test_invalid_output_type(self) -> None: +class TestOcrSettingsChecks: + @pytest.mark.parametrize( + ("setting", "value", "expected_msg"), + [ + pytest.param( + "OCR_OUTPUT_TYPE", + "notapdf", + 'OCR output type "notapdf"', + id="invalid-output-type", + ), + pytest.param( + "OCR_MODE", + "makeitso", + 'OCR output mode "makeitso"', + id="invalid-mode", + ), + pytest.param( + "OCR_MODE", + "skip_noarchive", + "deprecated", + id="deprecated-mode", + ), + pytest.param( + "OCR_SKIP_ARCHIVE_FILE", + "invalid", + 'OCR_SKIP_ARCHIVE_FILE setting "invalid"', + id="invalid-skip-archive-file", + ), + pytest.param( + "OCR_CLEAN", + "cleanme", + 'OCR clean mode "cleanme"', + id="invalid-clean", + ), + ], + ) + def test_invalid_setting_produces_one_error( + self, + settings: SettingsWrapper, + setting: str, + value: str, + expected_msg: str, + ) -> None: """ GIVEN: - Default settings - - OCR output type is invalid + - One OCR setting is set to an invalid value WHEN: - Settings are validated THEN: - - system check error reported for OCR output type + - Exactly one system check error is reported containing the expected message """ + setattr(settings, setting, value) + msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - msg = msgs[0] - - self.assertIn('OCR output type "notapdf"', msg.msg) - - @override_settings(OCR_MODE="makeitso") - def test_invalid_ocr_type(self) -> None: - """ - GIVEN: - - Default settings - - OCR type is invalid - WHEN: - - Settings are validated - THEN: - - system check error reported for OCR type - """ - msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - - msg = msgs[0] - - self.assertIn('OCR output mode "makeitso"', msg.msg) - - @override_settings(OCR_MODE="skip_noarchive") - def test_deprecated_ocr_type(self) -> None: - """ - GIVEN: - - Default settings - - OCR type is deprecated - WHEN: - - Settings are validated - THEN: - - deprecation warning reported for OCR type - """ - msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - - msg = msgs[0] - - self.assertIn("deprecated", msg.msg) - - @override_settings(OCR_SKIP_ARCHIVE_FILE="invalid") - def test_invalid_ocr_skip_archive_file(self) -> None: - """ - GIVEN: - - Default settings - - OCR_SKIP_ARCHIVE_FILE is invalid - WHEN: - - Settings are validated - THEN: - - system check error reported for OCR_SKIP_ARCHIVE_FILE - """ - msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - - msg = msgs[0] - - self.assertIn('OCR_SKIP_ARCHIVE_FILE setting "invalid"', msg.msg) - - @override_settings(OCR_CLEAN="cleanme") - def test_invalid_ocr_clean(self) -> None: - """ - GIVEN: - - Default settings - - OCR cleaning type is invalid - WHEN: - - Settings are validated - THEN: - - system check error reported for OCR cleaning type - """ - msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - - msg = msgs[0] - - self.assertIn('OCR clean mode "cleanme"', msg.msg) + assert len(msgs) == 1 + assert expected_msg in msgs[0].msg -class TestTimezoneSettingsChecks(DirectoriesMixin, TestCase): - @override_settings(TIME_ZONE="TheMoon\\MyCrater") - def test_invalid_timezone(self) -> None: +class TestTimezoneSettingsChecks: + def test_invalid_timezone(self, settings: SettingsWrapper) -> None: """ GIVEN: - Default settings @@ -185,17 +179,16 @@ class TestTimezoneSettingsChecks(DirectoriesMixin, TestCase): THEN: - system check error reported for timezone """ + settings.TIME_ZONE = "TheMoon\\MyCrater" + msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - msg = msgs[0] - - self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg) + assert len(msgs) == 1 + assert 'Timezone "TheMoon\\MyCrater"' in msgs[0].msg -class TestEmailCertSettingsChecks(DirectoriesMixin, FileSystemAssertsMixin, TestCase): - @override_settings(EMAIL_CERTIFICATE_FILE=Path("/tmp/not_actually_here.pem")) - def test_not_valid_file(self) -> None: +class TestEmailCertSettingsChecks: + def test_not_valid_file(self, settings: SettingsWrapper) -> None: """ GIVEN: - Default settings @@ -205,19 +198,22 @@ class TestEmailCertSettingsChecks(DirectoriesMixin, FileSystemAssertsMixin, Test THEN: - system check error reported for email certificate """ - self.assertIsNotFile("/tmp/not_actually_here.pem") + cert_path = Path("/tmp/not_actually_here.pem") + assert not cert_path.is_file() + settings.EMAIL_CERTIFICATE_FILE = cert_path msgs = settings_values_check(None) - self.assertEqual(len(msgs), 1) - - msg = msgs[0] - - self.assertIn("Email cert /tmp/not_actually_here.pem is not a file", msg.msg) + assert len(msgs) == 1 + assert "Email cert /tmp/not_actually_here.pem is not a file" in msgs[0].msg -class TestAuditLogChecks(TestCase): - def test_was_enabled_once(self) -> None: +class TestAuditLogChecks: + def test_was_enabled_once( + self, + settings: SettingsWrapper, + mocker: MockerFixture, + ) -> None: """ GIVEN: - Audit log is not enabled @@ -226,23 +222,18 @@ class TestAuditLogChecks(TestCase): THEN: - system check error reported for disabling audit log """ - introspect_mock = mock.MagicMock() + settings.AUDIT_LOG_ENABLED = False + introspect_mock = mocker.MagicMock() introspect_mock.introspection.table_names.return_value = ["auditlog_logentry"] - with override_settings(AUDIT_LOG_ENABLED=False): - with mock.patch.dict( - "paperless.checks.connections", - {"default": introspect_mock}, - ): - msgs = audit_log_check(None) + mocker.patch.dict( + "paperless.checks.connections", + {"default": introspect_mock}, + ) - self.assertEqual(len(msgs), 1) + msgs = audit_log_check(None) - msg = msgs[0] - - self.assertIn( - ("auditlog table was found but audit log is disabled."), - msg.msg, - ) + assert len(msgs) == 1 + assert "auditlog table was found but audit log is disabled." in msgs[0].msg DEPRECATED_VARS: dict[str, str] = { @@ -271,20 +262,16 @@ class TestDeprecatedDbSettings: @pytest.mark.parametrize( ("env_var", "db_option_key"), [ - ("PAPERLESS_DB_TIMEOUT", "timeout"), - ("PAPERLESS_DB_POOLSIZE", "pool.min_size / pool.max_size"), - ("PAPERLESS_DBSSLMODE", "sslmode"), - ("PAPERLESS_DBSSLROOTCERT", "sslrootcert"), - ("PAPERLESS_DBSSLCERT", "sslcert"), - ("PAPERLESS_DBSSLKEY", "sslkey"), - ], - ids=[ - "db-timeout", - "db-poolsize", - "ssl-mode", - "ssl-rootcert", - "ssl-cert", - "ssl-key", + pytest.param("PAPERLESS_DB_TIMEOUT", "timeout", id="db-timeout"), + pytest.param( + "PAPERLESS_DB_POOLSIZE", + "pool.min_size / pool.max_size", + id="db-poolsize", + ), + pytest.param("PAPERLESS_DBSSLMODE", "sslmode", id="ssl-mode"), + pytest.param("PAPERLESS_DBSSLROOTCERT", "sslrootcert", id="ssl-rootcert"), + pytest.param("PAPERLESS_DBSSLCERT", "sslcert", id="ssl-cert"), + pytest.param("PAPERLESS_DBSSLKEY", "sslkey", id="ssl-key"), ], ) def test_single_deprecated_var_produces_one_warning( diff --git a/src/paperless/tests/test_utils.py b/src/paperless/tests/test_utils.py index 215498c29..152913867 100644 --- a/src/paperless/tests/test_utils.py +++ b/src/paperless/tests/test_utils.py @@ -9,35 +9,50 @@ from paperless.utils import ocr_to_dateparser_languages @pytest.mark.parametrize( ("ocr_language", "expected"), [ - # One language - ("eng", ["en"]), - # Multiple languages - ("fra+ita+lao", ["fr", "it", "lo"]), - # Languages that don't have a two-letter equivalent - ("fil", ["fil"]), - # Languages with a script part supported by dateparser - ("aze_cyrl+srp_latn", ["az-Cyrl", "sr-Latn"]), - # Languages with a script part not supported by dateparser - # In this case, default to the language without script - ("deu_frak", ["de"]), - # Traditional and simplified chinese don't have the same name in dateparser, - # so they're converted to the general chinese language - ("chi_tra+chi_sim", ["zh"]), - # If a language is not supported by dateparser, fallback to the supported ones - ("eng+unsupported_language+por", ["en", "pt"]), - # If no language is supported, fallback to default - ("unsupported1+unsupported2", []), - # Duplicate languages, should not duplicate in result - ("eng+eng", ["en"]), - # Language with script, but script is not mapped - ("ita_unknownscript", ["it"]), + pytest.param("eng", ["en"], id="single-language"), + pytest.param("fra+ita+lao", ["fr", "it", "lo"], id="multiple-languages"), + pytest.param("fil", ["fil"], id="no-two-letter-equivalent"), + pytest.param( + "aze_cyrl+srp_latn", + ["az-Cyrl", "sr-Latn"], + id="script-supported-by-dateparser", + ), + pytest.param( + "deu_frak", + ["de"], + id="script-not-supported-falls-back-to-language", + ), + pytest.param( + "chi_tra+chi_sim", + ["zh"], + id="chinese-variants-collapse-to-general", + ), + pytest.param( + "eng+unsupported_language+por", + ["en", "pt"], + id="unsupported-language-skipped", + ), + pytest.param( + "unsupported1+unsupported2", + [], + id="all-unsupported-returns-empty", + ), + pytest.param("eng+eng", ["en"], id="duplicates-deduplicated"), + pytest.param( + "ita_unknownscript", + ["it"], + id="unknown-script-falls-back-to-language", + ), ], ) -def test_ocr_to_dateparser_languages(ocr_language, expected): +def test_ocr_to_dateparser_languages(ocr_language: str, expected: list[str]) -> None: assert sorted(ocr_to_dateparser_languages(ocr_language)) == sorted(expected) -def test_ocr_to_dateparser_languages_exception(monkeypatch, caplog): +def test_ocr_to_dateparser_languages_exception( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: # Patch LocaleDataLoader.get_locale_map to raise an exception class DummyLoader: def get_locale_map(self, locales=None): diff --git a/src/paperless/tests/test_views.py b/src/paperless/tests/test_views.py index ab8941538..35e0fd910 100644 --- a/src/paperless/tests/test_views.py +++ b/src/paperless/tests/test_views.py @@ -1,24 +1,31 @@ -import tempfile from pathlib import Path -from django.test import override_settings +from django.test import Client +from pytest_django.fixtures import SettingsWrapper -def test_favicon_view(client): - with tempfile.TemporaryDirectory() as tmpdir: - static_dir = Path(tmpdir) - favicon_path = static_dir / "paperless" / "img" / "favicon.ico" - favicon_path.parent.mkdir(parents=True, exist_ok=True) - favicon_path.write_bytes(b"FAKE ICON DATA") +def test_favicon_view( + client: Client, + tmp_path: Path, + settings: SettingsWrapper, +) -> None: + favicon_path = tmp_path / "paperless" / "img" / "favicon.ico" + favicon_path.parent.mkdir(parents=True) + favicon_path.write_bytes(b"FAKE ICON DATA") - with override_settings(STATIC_ROOT=static_dir): - response = client.get("/favicon.ico") - assert response.status_code == 200 - assert response["Content-Type"] == "image/x-icon" - assert b"".join(response.streaming_content) == b"FAKE ICON DATA" + settings.STATIC_ROOT = tmp_path + + response = client.get("/favicon.ico") + assert response.status_code == 200 + assert response["Content-Type"] == "image/x-icon" + assert b"".join(response.streaming_content) == b"FAKE ICON DATA" -def test_favicon_view_missing_file(client): - with override_settings(STATIC_ROOT=Path(tempfile.mkdtemp())): - response = client.get("/favicon.ico") - assert response.status_code == 404 +def test_favicon_view_missing_file( + client: Client, + tmp_path: Path, + settings: SettingsWrapper, +) -> None: + settings.STATIC_ROOT = tmp_path + response = client.get("/favicon.ico") + assert response.status_code == 404