7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
436 lines
16 KiB
Python
436 lines
16 KiB
Python
"""
|
|
Tests for SQLCipher missing scenario.
|
|
|
|
Verifies that users get helpful error messages when SQLCipher is not installed
|
|
and that the LDR_BOOTSTRAP_ALLOW_UNENCRYPTED workaround works.
|
|
"""
|
|
|
|
import os
|
|
import pytest
|
|
from unittest.mock import patch
|
|
|
|
from tests.test_utils import add_src_to_path
|
|
|
|
add_src_to_path()
|
|
|
|
|
|
def _clear_allow_unencrypted_env():
|
|
"""Clear both canonical and deprecated env vars."""
|
|
os.environ.pop("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", None)
|
|
os.environ.pop("LDR_ALLOW_UNENCRYPTED", None)
|
|
|
|
|
|
class TestSQLCipherMissing:
|
|
"""Test behavior when SQLCipher is not available."""
|
|
|
|
def test_error_message_mentions_sqlcipher(self):
|
|
"""Error message should mention SQLCipher so users know what's missing."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
old_canonical = os.environ.pop("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", None)
|
|
old_deprecated = os.environ.pop("LDR_ALLOW_UNENCRYPTED", None)
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
DatabaseManager()
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "SQLCipher" in error_msg, (
|
|
f"Error should mention SQLCipher. Got: {error_msg}"
|
|
)
|
|
finally:
|
|
if old_canonical is not None:
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical
|
|
if old_deprecated is not None:
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated
|
|
|
|
def test_error_message_mentions_workaround(self):
|
|
"""Error message should mention LDR_BOOTSTRAP_ALLOW_UNENCRYPTED workaround."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
old_canonical = os.environ.pop("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED", None)
|
|
old_deprecated = os.environ.pop("LDR_ALLOW_UNENCRYPTED", None)
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
DatabaseManager()
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert "LDR_BOOTSTRAP_ALLOW_UNENCRYPTED" in error_msg, (
|
|
f"Error should mention workaround. Got: {error_msg}"
|
|
)
|
|
finally:
|
|
if old_canonical is not None:
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical
|
|
if old_deprecated is not None:
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated
|
|
|
|
def test_canonical_workaround_allows_startup_without_encryption(self):
|
|
"""LDR_BOOTSTRAP_ALLOW_UNENCRYPTED=true should allow startup without SQLCipher."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
old_canonical = os.environ.get("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED")
|
|
old_deprecated = os.environ.get("LDR_ALLOW_UNENCRYPTED")
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "true"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False, (
|
|
"With workaround and no SQLCipher, has_encryption should be False"
|
|
)
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
if old_canonical is not None:
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical
|
|
if old_deprecated is not None:
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated
|
|
|
|
def test_deprecated_workaround_still_works(self):
|
|
"""Deprecated LDR_ALLOW_UNENCRYPTED=true should still allow startup (backward compat)."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
old_canonical = os.environ.get("LDR_BOOTSTRAP_ALLOW_UNENCRYPTED")
|
|
old_deprecated = os.environ.get("LDR_ALLOW_UNENCRYPTED")
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = "true"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise (backward compatibility)
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False, (
|
|
"With deprecated workaround and no SQLCipher, has_encryption should be False"
|
|
)
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
if old_canonical is not None:
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = old_canonical
|
|
if old_deprecated is not None:
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = old_deprecated
|
|
|
|
def test_db_manager_has_encryption_is_boolean(self):
|
|
"""db_manager.has_encryption should be a boolean."""
|
|
from local_deep_research.database.encrypted_db import db_manager
|
|
|
|
assert isinstance(db_manager.has_encryption, bool), (
|
|
f"has_encryption should be bool, got {type(db_manager.has_encryption)}"
|
|
)
|
|
|
|
|
|
class TestAllowUnencryptedEdgeCases:
|
|
"""Edge cases for LDR_BOOTSTRAP_ALLOW_UNENCRYPTED fallback.
|
|
|
|
The allow_unencrypted setting uses the BooleanSetting from the env
|
|
settings registry, which accepts "true", "1", "yes", "on", "enabled"
|
|
(case-insensitive) as truthy values and handles deprecated alias
|
|
fallback automatically.
|
|
"""
|
|
|
|
def test_canonical_empty_string_does_not_fall_through(self):
|
|
"""Empty string canonical should NOT fall through to deprecated.
|
|
|
|
When LDR_BOOTSTRAP_ALLOW_UNENCRYPTED="" is set, it takes precedence
|
|
and the check (empty_string or "").lower() == "true" is False.
|
|
"""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = ""
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = "true"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should raise because empty string is not "true"
|
|
with pytest.raises(RuntimeError):
|
|
DatabaseManager()
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_uppercase_true_not_accepted(self):
|
|
"""'TRUE' (uppercase) is not accepted as true.
|
|
|
|
The code uses .lower() == "true", so "TRUE".lower() == "true" is True.
|
|
"""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "TRUE"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise - "TRUE".lower() == "true" is True
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_mixed_case_true_accepted(self):
|
|
"""'True' (mixed case) is accepted as true due to .lower()."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "True"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise - "True".lower() == "true" is True
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_whitespace_padded_true_not_accepted(self):
|
|
"""' true ' (whitespace padded) is not accepted as true.
|
|
|
|
The code doesn't strip whitespace, so " true " != "true".
|
|
"""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = " true "
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should raise - " true " != "true"
|
|
with pytest.raises(RuntimeError):
|
|
DatabaseManager()
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_yes_accepted_as_true(self):
|
|
"""'yes' is accepted as true by BooleanSetting."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "yes"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise - BooleanSetting accepts "yes"
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_one_accepted_as_true(self):
|
|
"""'1' is accepted as true by BooleanSetting."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "1"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise - BooleanSetting accepts "1"
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_canonical_false_does_not_check_deprecated(self):
|
|
"""canonical='false' should NOT fall through to deprecated='true'.
|
|
|
|
Once canonical is set (to any value), deprecated is not checked.
|
|
"""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = "false"
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = "true"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should raise - canonical "false" takes precedence
|
|
with pytest.raises(RuntimeError):
|
|
DatabaseManager()
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_empty_canonical_with_deprecated_true_raises(self):
|
|
"""Empty canonical with deprecated='true' - empty takes precedence.
|
|
|
|
When canonical is "", the deprecated is not checked because
|
|
canonical is "set" (to empty string).
|
|
"""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_BOOTSTRAP_ALLOW_UNENCRYPTED"] = ""
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = "true"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should raise - empty string canonical takes precedence
|
|
with pytest.raises(RuntimeError):
|
|
DatabaseManager()
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_only_deprecated_set_to_true_works(self):
|
|
"""When only deprecated is set to 'true', it should work."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = "true"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise - deprecated "true" works
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_deprecated_uppercase_true_accepted(self):
|
|
"""Deprecated 'TRUE' (uppercase) works due to .lower()."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
os.environ["LDR_ALLOW_UNENCRYPTED"] = "TRUE"
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
# Should NOT raise - "TRUE".lower() == "true"
|
|
manager = DatabaseManager()
|
|
assert manager.has_encryption is False
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|
|
|
|
def test_neither_set_raises_runtime_error(self):
|
|
"""When neither canonical nor deprecated is set, RuntimeError is raised."""
|
|
from local_deep_research.database.encrypted_db import (
|
|
DatabaseManager,
|
|
)
|
|
|
|
_clear_allow_unencrypted_env()
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.encrypted_db.get_sqlcipher_module"
|
|
) as mock_get:
|
|
mock_get.side_effect = ImportError(
|
|
"No module named 'sqlcipher3'"
|
|
)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
DatabaseManager()
|
|
finally:
|
|
_clear_allow_unencrypted_env()
|