diff --git a/src/paperless/tests/test_adapter.py b/src/paperless/tests/test_adapter.py index fcbd21b12..680abdd77 100644 --- a/src/paperless/tests/test_adapter.py +++ b/src/paperless/tests/test_adapter.py @@ -1,107 +1,100 @@ -from unittest import mock +import logging +import pytest from allauth.account.adapter import get_adapter from allauth.core import context from allauth.socialaccount.adapter import get_adapter as get_social_adapter -from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import Group from django.contrib.auth.models import User from django.forms import ValidationError from django.http import HttpRequest -from django.test import TestCase -from django.test import override_settings from django.urls import reverse +from pytest_django.fixtures import SettingsWrapper +from pytest_mock import MockerFixture from rest_framework.authtoken.models import Token from paperless.adapter import DrfTokenStrategy -class TestCustomAccountAdapter(TestCase): - def test_is_open_for_signup(self) -> None: +@pytest.mark.django_db +class TestCustomAccountAdapter: + def test_is_open_for_signup(self, settings: SettingsWrapper) -> None: adapter = get_adapter() # With no accounts, signups should be allowed - self.assertTrue(adapter.is_open_for_signup(None)) + assert adapter.is_open_for_signup(None) User.objects.create_user("testuser") - # Test when ACCOUNT_ALLOW_SIGNUPS is True settings.ACCOUNT_ALLOW_SIGNUPS = True - self.assertTrue(adapter.is_open_for_signup(None)) + assert adapter.is_open_for_signup(None) - # Test when ACCOUNT_ALLOW_SIGNUPS is False settings.ACCOUNT_ALLOW_SIGNUPS = False - self.assertFalse(adapter.is_open_for_signup(None)) + assert not adapter.is_open_for_signup(None) - def test_is_safe_url(self) -> None: + def test_is_safe_url(self, settings: SettingsWrapper) -> None: request = HttpRequest() - request.get_host = mock.Mock(return_value="example.com") + request.get_host = lambda: "example.com" with context.request_context(request): adapter = get_adapter() - with override_settings(ALLOWED_HOSTS=["*"]): - # True because request host is same - url = "https://example.com" - self.assertTrue(adapter.is_safe_url(url)) - url = "https://evil.com" + settings.ALLOWED_HOSTS = ["*"] + # True because request host is same + assert adapter.is_safe_url("https://example.com") # False despite wildcard because request host is different - self.assertFalse(adapter.is_safe_url(url)) + assert not adapter.is_safe_url("https://evil.com") settings.ALLOWED_HOSTS = ["example.com"] - url = "https://example.com" # True because request host is same - self.assertTrue(adapter.is_safe_url(url)) + assert adapter.is_safe_url("https://example.com") settings.ALLOWED_HOSTS = ["*", "example.com"] - url = "//evil.com" # False because request host is not in allowed hosts - self.assertFalse(adapter.is_safe_url(url)) + assert not adapter.is_safe_url("//evil.com") - @mock.patch("allauth.core.internal.ratelimit.consume", return_value=True) - def test_pre_authenticate(self, mock_consume) -> None: + def test_pre_authenticate( + self, + settings: SettingsWrapper, + mocker: MockerFixture, + ) -> None: + mocker.patch("allauth.core.internal.ratelimit.consume", return_value=True) adapter = get_adapter() request = HttpRequest() - request.get_host = mock.Mock(return_value="example.com") + request.get_host = lambda: "example.com" settings.DISABLE_REGULAR_LOGIN = False adapter.pre_authenticate(request) settings.DISABLE_REGULAR_LOGIN = True - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): adapter.pre_authenticate(request) - def test_get_reset_password_from_key_url(self) -> None: + def test_get_reset_password_from_key_url(self, settings: SettingsWrapper) -> None: request = HttpRequest() - request.get_host = mock.Mock(return_value="foo.org") + request.get_host = lambda: "foo.org" with context.request_context(request): adapter = get_adapter() - # Test when PAPERLESS_URL is None - with override_settings( - PAPERLESS_URL=None, - ACCOUNT_DEFAULT_HTTP_PROTOCOL="https", - ): - expected_url = f"https://foo.org{reverse('account_reset_password_from_key', kwargs={'uidb36': 'UID', 'key': 'KEY'})}" - self.assertEqual( - adapter.get_reset_password_from_key_url("UID-KEY"), - expected_url, - ) + settings.PAPERLESS_URL = None + settings.ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" + expected_url = f"https://foo.org{reverse('account_reset_password_from_key', kwargs={'uidb36': 'UID', 'key': 'KEY'})}" + assert adapter.get_reset_password_from_key_url("UID-KEY") == expected_url - # Test when PAPERLESS_URL is not None - with override_settings(PAPERLESS_URL="https://bar.com"): - expected_url = f"https://bar.com{reverse('account_reset_password_from_key', kwargs={'uidb36': 'UID', 'key': 'KEY'})}" - self.assertEqual( - adapter.get_reset_password_from_key_url("UID-KEY"), - expected_url, - ) + settings.PAPERLESS_URL = "https://bar.com" + expected_url = f"https://bar.com{reverse('account_reset_password_from_key', kwargs={'uidb36': 'UID', 'key': 'KEY'})}" + assert adapter.get_reset_password_from_key_url("UID-KEY") == expected_url - @override_settings(ACCOUNT_DEFAULT_GROUPS=["group1", "group2"]) - def test_save_user_adds_groups(self) -> None: + def test_save_user_adds_groups( + self, + settings: SettingsWrapper, + mocker: MockerFixture, + ) -> None: + settings.ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"] Group.objects.create(name="group1") user = User.objects.create_user("testuser") adapter = get_adapter() - form = mock.Mock( + form = mocker.MagicMock( cleaned_data={ "username": "testuser", "email": "user@example.com", @@ -110,88 +103,81 @@ class TestCustomAccountAdapter(TestCase): user = adapter.save_user(HttpRequest(), user, form, commit=True) - self.assertEqual(user.groups.count(), 1) - self.assertTrue(user.groups.filter(name="group1").exists()) - self.assertFalse(user.groups.filter(name="group2").exists()) + assert user.groups.count() == 1 + assert user.groups.filter(name="group1").exists() + assert not user.groups.filter(name="group2").exists() - def test_fresh_install_save_creates_superuser(self) -> None: + def test_fresh_install_save_creates_superuser(self, mocker: MockerFixture) -> None: adapter = get_adapter() - form = mock.Mock( + form = mocker.MagicMock( cleaned_data={ "username": "testuser", "email": "user@paperless-ngx.com", }, ) user = adapter.save_user(HttpRequest(), User(), form, commit=True) - self.assertTrue(user.is_superuser) + assert user.is_superuser - # Next time, it should not create a superuser - form = mock.Mock( + form = mocker.MagicMock( cleaned_data={ "username": "testuser2", "email": "user2@paperless-ngx.com", }, ) user2 = adapter.save_user(HttpRequest(), User(), form, commit=True) - self.assertFalse(user2.is_superuser) + assert not user2.is_superuser -class TestCustomSocialAccountAdapter(TestCase): - def test_is_open_for_signup(self) -> None: +class TestCustomSocialAccountAdapter: + @pytest.mark.django_db + def test_is_open_for_signup(self, settings: SettingsWrapper) -> None: adapter = get_social_adapter() - # Test when SOCIALACCOUNT_ALLOW_SIGNUPS is True settings.SOCIALACCOUNT_ALLOW_SIGNUPS = True - self.assertTrue(adapter.is_open_for_signup(None, None)) + assert adapter.is_open_for_signup(None, None) - # Test when SOCIALACCOUNT_ALLOW_SIGNUPS is False settings.SOCIALACCOUNT_ALLOW_SIGNUPS = False - self.assertFalse(adapter.is_open_for_signup(None, None)) + assert not adapter.is_open_for_signup(None, None) def test_get_connect_redirect_url(self) -> None: adapter = get_social_adapter() - request = None - socialaccount = None + assert adapter.get_connect_redirect_url(None, None) == reverse("base") - # Test the default URL - expected_url = reverse("base") - self.assertEqual( - adapter.get_connect_redirect_url(request, socialaccount), - expected_url, - ) - - @override_settings(SOCIAL_ACCOUNT_DEFAULT_GROUPS=["group1", "group2"]) - def test_save_user_adds_groups(self) -> None: + @pytest.mark.django_db + def test_save_user_adds_groups( + self, + settings: SettingsWrapper, + mocker: MockerFixture, + ) -> None: + settings.SOCIAL_ACCOUNT_DEFAULT_GROUPS = ["group1", "group2"] Group.objects.create(name="group1") adapter = get_social_adapter() - request = HttpRequest() user = User.objects.create_user("testuser") - sociallogin = mock.Mock( - user=user, - ) + sociallogin = mocker.MagicMock(user=user) - user = adapter.save_user(request, sociallogin, None) + user = adapter.save_user(HttpRequest(), sociallogin, None) - self.assertEqual(user.groups.count(), 1) - self.assertTrue(user.groups.filter(name="group1").exists()) - self.assertFalse(user.groups.filter(name="group2").exists()) + assert user.groups.count() == 1 + assert user.groups.filter(name="group1").exists() + assert not user.groups.filter(name="group2").exists() - def test_error_logged_on_authentication_error(self) -> None: + def test_error_logged_on_authentication_error( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: adapter = get_social_adapter() - request = HttpRequest() - with self.assertLogs("paperless.auth", level="INFO") as log_cm: + with caplog.at_level(logging.INFO, logger="paperless.auth"): adapter.on_authentication_error( - request, + HttpRequest(), provider="test-provider", error="Error", exception="Test authentication error", ) - self.assertTrue( - any("Test authentication error" in message for message in log_cm.output), - ) + assert any("Test authentication error" in msg for msg in caplog.messages) -class TestDrfTokenStrategy(TestCase): +@pytest.mark.django_db +class TestDrfTokenStrategy: def test_create_access_token_creates_new_token(self) -> None: """ GIVEN: @@ -201,7 +187,6 @@ class TestDrfTokenStrategy(TestCase): THEN: - A new token is created and its key is returned """ - user = User.objects.create_user("testuser") request = HttpRequest() request.user = user @@ -209,13 +194,9 @@ class TestDrfTokenStrategy(TestCase): strategy = DrfTokenStrategy() token_key = strategy.create_access_token(request) - # Verify a token was created - self.assertIsNotNone(token_key) - self.assertTrue(Token.objects.filter(user=user).exists()) - - # Verify the returned key matches the created token - token = Token.objects.get(user=user) - self.assertEqual(token_key, token.key) + assert token_key is not None + assert Token.objects.filter(user=user).exists() + assert token_key == Token.objects.get(user=user).key def test_create_access_token_returns_existing_token(self) -> None: """ @@ -226,7 +207,6 @@ class TestDrfTokenStrategy(TestCase): THEN: - The same token key is returned (no new token created) """ - user = User.objects.create_user("testuser") existing_token = Token.objects.create(user=user) @@ -236,11 +216,8 @@ class TestDrfTokenStrategy(TestCase): strategy = DrfTokenStrategy() token_key = strategy.create_access_token(request) - # Verify the existing token key is returned - self.assertEqual(token_key, existing_token.key) - - # Verify only one token exists (no duplicate created) - self.assertEqual(Token.objects.filter(user=user).count(), 1) + assert token_key == existing_token.key + assert Token.objects.filter(user=user).count() == 1 def test_create_access_token_returns_none_for_unauthenticated_user(self) -> None: """ @@ -251,12 +228,11 @@ class TestDrfTokenStrategy(TestCase): THEN: - None is returned and no token is created """ - request = HttpRequest() request.user = AnonymousUser() strategy = DrfTokenStrategy() token_key = strategy.create_access_token(request) - self.assertIsNone(token_key) - self.assertEqual(Token.objects.count(), 0) + assert token_key is None + assert Token.objects.count() == 0 diff --git a/src/paperless/tests/test_checks.py b/src/paperless/tests/test_checks.py index c8596a396..14950e2ca 100644 --- a/src/paperless/tests/test_checks.py +++ b/src/paperless/tests/test_checks.py @@ -1,16 +1,15 @@ import os +from collections.abc import Callable +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 +19,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 +107,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 +181,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 +200,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 +224,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 +264,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( @@ -403,7 +392,10 @@ class TestV3MinimumUpgradeVersionCheck: """Test suite for check_v3_minimum_upgrade_version system check.""" @pytest.fixture - def build_conn_mock(self, mocker: MockerFixture): + def build_conn_mock( + self, + mocker: MockerFixture, + ) -> Callable[[list[str], list[str]], mock.MagicMock]: """Factory fixture that builds a connections['default'] mock. Usage:: @@ -423,7 +415,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_no_migrations_table_fresh_install( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -442,7 +434,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_no_documents_migrations_fresh_install( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -461,7 +453,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_v3_state_with_0001_squashed( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -485,7 +477,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_v3_state_with_0002_squashed_only( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -504,7 +496,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_v2_20_9_state_ready_to_upgrade( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -531,7 +523,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_v2_20_8_raises_error( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -558,7 +550,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_very_old_version_raises_error( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: @@ -585,7 +577,7 @@ class TestV3MinimumUpgradeVersionCheck: def test_error_hint_mentions_v2_20_9( self, mocker: MockerFixture, - build_conn_mock, + build_conn_mock: Callable[[list[str], list[str]], mock.MagicMock], ) -> None: """ GIVEN: 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