# allow: no-sut-import — exercises sqlcipher_compat via importlib reload to cover ImportError paths (dynamic import) """ Comprehensive tests for local_deep_research.database.sqlcipher_compat module. Tests the compatibility layer logic, feature detection, and ImportError behavior when sqlcipher3 is unavailable. """ import importlib import sys import types from unittest import mock import pytest # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- MODULE_PATH = "local_deep_research.database.sqlcipher_compat" def _fresh_import(): """Re-import sqlcipher_compat so module-level code re-executes.""" mod = importlib.import_module(MODULE_PATH) importlib.reload(mod) return mod # --------------------------------------------------------------------------- # 1. Successful import path – sqlcipher3 is available # --------------------------------------------------------------------------- class TestGetSqlcipherModuleSuccess: """When sqlcipher3 can be imported, get_sqlcipher_module should return it.""" def test_returns_sqlcipher3_module(self): """get_sqlcipher_module returns the sqlcipher3 module object.""" fake_sqlcipher3 = types.ModuleType("sqlcipher3") fake_sqlcipher3.dbapi2 = object() with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}): mod = _fresh_import() result = mod.get_sqlcipher_module() assert result is fake_sqlcipher3 def test_returned_module_has_dbapi2(self): """The returned module should carry a dbapi2 attribute.""" fake_sqlcipher3 = types.ModuleType("sqlcipher3") sentinel = object() fake_sqlcipher3.dbapi2 = sentinel with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}): mod = _fresh_import() result = mod.get_sqlcipher_module() assert hasattr(result, "dbapi2") assert result.dbapi2 is sentinel def test_multiple_calls_return_same_module(self): """Repeated calls should consistently return the same module.""" fake_sqlcipher3 = types.ModuleType("sqlcipher3") fake_sqlcipher3.dbapi2 = object() with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}): mod = _fresh_import() r1 = mod.get_sqlcipher_module() r2 = mod.get_sqlcipher_module() assert r1 is r2 def test_returns_module_with_connect_attribute(self): """Validates that a module with a connect() callable works fine.""" fake_sqlcipher3 = types.ModuleType("sqlcipher3") fake_sqlcipher3.dbapi2 = types.ModuleType("sqlcipher3.dbapi2") fake_sqlcipher3.connect = lambda db: None with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}): mod = _fresh_import() result = mod.get_sqlcipher_module() assert callable(result.connect) # --------------------------------------------------------------------------- # 2. ImportError path – sqlcipher3 is NOT available # --------------------------------------------------------------------------- class TestGetSqlcipherModuleImportError: """When sqlcipher3 cannot be imported, get_sqlcipher_module must raise.""" def _block_sqlcipher3(self): """Return a context manager that makes `import sqlcipher3` fail.""" original_import = ( __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__ ) def _guarded_import(name, *args, **kwargs): if name == "sqlcipher3": raise ImportError("No module named 'sqlcipher3'") return original_import(name, *args, **kwargs) return mock.patch("builtins.__import__", side_effect=_guarded_import) def test_raises_import_error(self): """An ImportError must be raised when sqlcipher3 is absent.""" with self._block_sqlcipher3(): # Remove sqlcipher3 from sys.modules if cached with mock.patch.dict(sys.modules, {"sqlcipher3": None}): mod = _fresh_import() with pytest.raises(ImportError): mod.get_sqlcipher_module() def test_error_message_mentions_sqlcipher3(self): """The error message should mention sqlcipher3.""" with self._block_sqlcipher3(): with mock.patch.dict(sys.modules, {"sqlcipher3": None}): mod = _fresh_import() with pytest.raises(ImportError, match="sqlcipher3"): mod.get_sqlcipher_module() def test_error_message_mentions_pdm_install(self): """The error message should mention pdm install.""" with self._block_sqlcipher3(): with mock.patch.dict(sys.modules, {"sqlcipher3": None}): mod = _fresh_import() with pytest.raises(ImportError, match="pdm install"): mod.get_sqlcipher_module() def test_error_message_mentions_system_library(self): """The error message should mention SQLCipher system library.""" with self._block_sqlcipher3(): with mock.patch.dict(sys.modules, {"sqlcipher3": None}): mod = _fresh_import() with pytest.raises( ImportError, match="SQLCipher system library" ): mod.get_sqlcipher_module() # --------------------------------------------------------------------------- # 3. Feature detection – module attributes # --------------------------------------------------------------------------- class TestModuleAttributes: """Verify the public API surface of the compat module.""" def test_get_sqlcipher_module_is_callable(self): """get_sqlcipher_module must be a callable.""" mod = importlib.import_module(MODULE_PATH) assert callable(mod.get_sqlcipher_module) def test_module_has_docstring(self): """The module should have a docstring describing its purpose.""" mod = importlib.import_module(MODULE_PATH) assert mod.__doc__ is not None assert "SQLCipher" in mod.__doc__ def test_function_has_docstring(self): """get_sqlcipher_module should have a docstring.""" mod = importlib.import_module(MODULE_PATH) assert mod.get_sqlcipher_module.__doc__ is not None def test_docstring_documents_return_type(self): """The function docstring should mention Returns.""" mod = importlib.import_module(MODULE_PATH) assert "Returns" in mod.get_sqlcipher_module.__doc__ def test_docstring_documents_raises(self): """The function docstring should mention Raises / ImportError.""" mod = importlib.import_module(MODULE_PATH) assert "ImportError" in mod.get_sqlcipher_module.__doc__ # --------------------------------------------------------------------------- # 4. Platform compat – the module works regardless of which package provides # the sqlcipher3 namespace (sqlcipher3-binary vs sqlcipher3 source build). # --------------------------------------------------------------------------- class TestPlatformCompatibility: """Simulates different package backends behind the sqlcipher3 name.""" def test_binary_package(self): """Simulate sqlcipher3-binary providing the sqlcipher3 namespace.""" fake = types.ModuleType("sqlcipher3") fake._binary = True # marker attribute fake.dbapi2 = types.ModuleType("sqlcipher3.dbapi2") with mock.patch.dict(sys.modules, {"sqlcipher3": fake}): mod = _fresh_import() result = mod.get_sqlcipher_module() assert result._binary is True def test_source_package(self): """Simulate sqlcipher3 built from source.""" fake = types.ModuleType("sqlcipher3") fake._source = True fake.dbapi2 = types.ModuleType("sqlcipher3.dbapi2") with mock.patch.dict(sys.modules, {"sqlcipher3": fake}): mod = _fresh_import() result = mod.get_sqlcipher_module() assert result._source is True def test_module_identity_preserved(self): """The compat layer should not wrap or proxy the module.""" fake = types.ModuleType("sqlcipher3") fake.dbapi2 = object() with mock.patch.dict(sys.modules, {"sqlcipher3": fake}): mod = _fresh_import() result = mod.get_sqlcipher_module() # Must be the exact same object, not a wrapper assert result is fake assert type(result) is types.ModuleType # --------------------------------------------------------------------------- # 5. Edge cases # --------------------------------------------------------------------------- class TestEdgeCases: """Assorted edge-case scenarios.""" def test_import_error_is_not_generic_exception(self): """The raised exception must be ImportError, not a generic Exception.""" with mock.patch.dict(sys.modules, {"sqlcipher3": None}): mod = _fresh_import() with pytest.raises(ImportError) as exc_info: mod.get_sqlcipher_module() assert type(exc_info.value) is ImportError def test_no_side_effects_on_sys_modules(self): """Calling get_sqlcipher_module should not add unexpected entries.""" fake = types.ModuleType("sqlcipher3") fake.dbapi2 = object() before_keys = set(sys.modules.keys()) with mock.patch.dict(sys.modules, {"sqlcipher3": fake}): mod = _fresh_import() mod.get_sqlcipher_module() after_keys = set(sys.modules.keys()) # Only the compat module itself (from reload) may appear; no others unexpected = after_keys - before_keys - {MODULE_PATH} # Filter out __pycache__ or bytecode artifacts unexpected = {k for k in unexpected if "sqlcipher" in k.lower()} assert not unexpected, f"Unexpected modules added: {unexpected}" def test_reloadable(self): """The module should be safely reloadable.""" fake = types.ModuleType("sqlcipher3") fake.dbapi2 = object() with mock.patch.dict(sys.modules, {"sqlcipher3": fake}): mod = _fresh_import() # Reload again importlib.reload(mod) result = mod.get_sqlcipher_module() assert result is fake