diff --git a/src/documents/export/compression.py b/src/documents/export/compression.py index 2572a225f..89b311f1b 100644 --- a/src/documents/export/compression.py +++ b/src/documents/export/compression.py @@ -64,7 +64,7 @@ def compression_available(method: str) -> bool: return _module_importable("lzma") if method == "zstd": return ZSTD is not None and _module_importable("compression.zstd") - return False + return False # pragma: no cover -- method is always one of COMPRESSION_CHOICES def _module_importable(name: str) -> bool: diff --git a/src/documents/tests/export/test_compression.py b/src/documents/tests/export/test_compression.py index 584d6a66a..65ae859b9 100644 --- a/src/documents/tests/export/test_compression.py +++ b/src/documents/tests/export/test_compression.py @@ -2,6 +2,7 @@ import sys import zipfile import pytest +import pytest_mock from documents.export import compression @@ -69,6 +70,25 @@ class TestCompressionMethods: expected: bool = sys.version_info >= (3, 14) assert compression.compression_available("zstd") == expected + def test_unimportable_module_reports_unavailable( + self, + mocker: pytest_mock.MockerFixture, + ) -> None: + """ + GIVEN: + - A compression method whose backing module fails to import + (e.g. a minimal Python build without bz2/lzma compiled in) + WHEN: + - Checked with compression_available() + THEN: + - False is returned rather than the ImportError propagating + """ + mocker.patch( + "documents.export.compression.importlib.import_module", + side_effect=ImportError, + ) + assert not compression.compression_available("bzip2") + class TestLevelError: @pytest.mark.parametrize( diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index 21c83d18a..2ffded5e6 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -1182,6 +1182,35 @@ class TestExportImport( ) self.assertIn("3.14", str(e.exception)) + def test_non_zstd_unavailable_raises_generic_error(self) -> None: + """ + GIVEN: + - A Python runtime missing the module backing a non-zstd method + (e.g. bz2/lzma not compiled in on a minimal build) + WHEN: + - That method is requested via --zip-compression + THEN: + - A CommandError is raised naming the method, not the + zstd-specific "requires 3.14" message + """ + with ( + mock.patch( + "documents.management.commands.document_exporter.compression_available", + return_value=False, + ), + self.assertRaises(CommandError) as e, + ): + call_command( + "document_exporter", + self.target, + "--zip", + "--zip-compression", + "bzip2", + skip_checks=True, + ) + self.assertIn("bzip2", str(e.exception)) + self.assertNotIn("3.14", str(e.exception)) + def test_zip_compression_flag_resolves_to_sink_constant(self) -> None: """ GIVEN: diff --git a/src/documents/tests/test_management_importer.py b/src/documents/tests/test_management_importer.py index 243de9d64..bd74e7a16 100644 --- a/src/documents/tests/test_management_importer.py +++ b/src/documents/tests/test_management_importer.py @@ -554,6 +554,42 @@ class TestCommandImport( ) self.assertIn("compression", str(e.exception)) + def test_import_rejects_unreadable_zstd_with_version_hint(self) -> None: + """ + GIVEN: + - A zip archive with an entry compressed with zstd + WHEN: + - Import is attempted on a Python runtime that can't read zstd + THEN: + - The CommandError names the 3.14+ requirement, not just the + generic "can't read" message + """ + import zipfile + from unittest import mock + + archive = Path(self.dirs.scratch_dir) / "export.zip" + with zipfile.ZipFile(archive, "w") as zf: + zf.writestr("manifest.json", "[]") + + with ( + mock.patch( + "documents.management.commands.document_importer.compress_type_readable", + return_value=False, + ), + mock.patch( + "documents.management.commands.document_importer.unreadable_method_names", + return_value={"zstd"}, + ), + ): + with self.assertRaises(CommandError) as e: + call_command( + "document_importer", + str(archive), + "--no-progress-bar", + skip_checks=True, + ) + self.assertIn("3.14", str(e.exception)) + @pytest.mark.management @pytest.mark.django_db