7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
265 lines
10 KiB
Python
265 lines
10 KiB
Python
# 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
|