Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

833 lines
32 KiB
Python

"""Tests that ``logger.exception`` sites in ``database/encrypted_db.py``,
``web/queue/processor_v2.py``, and ``scheduler/background.py`` never leak
the SQLCipher master password into log output.
Companion fix: issue #4182 (follow-up to #4131). API keys are rotatable,
but SQLCipher master passwords are not (TRUST.md §5) — a leaked password
in a CI log or shared error report forces the user to abandon their
encrypted DB.
The fix pattern, established in PRs #4168/#4175/#4181, is:
except Exception as e:
safe_msg = redact_secrets(str(e), password)
logger.warning(f"... {safe_msg}")
These tests use ``loguru_caplog_full`` to capture the rendered exception
block (the existing ``loguru_caplog`` would false-pass on a leak that
lives only in the traceback frames). Each test is mutation-verified —
revert the production wrap and the test fails with the sentinel visible.
"""
import base64
from unittest.mock import MagicMock, patch
from urllib.parse import quote, quote_plus
import pytest
_LEAKED_PASSWORD = "ldr-master-pw-SHOULD-NEVER-LEAK-987654321"
_LEAKED_OLD_PASSWORD = "ldr-old-master-pw-ALSO-NEVER-LEAK-123"
def _all_encodings_of(secret: str) -> list:
"""Return every encoding the sentinel might appear under in a log.
Mirrors the helper in ``test_api_key_leakage.py``. Extend when a
fixed call site introduces a new transformation (e.g., bcrypt hash,
JWT payload).
"""
return [
secret,
quote(secret, safe=""),
quote_plus(secret),
repr(secret)[1:-1],
base64.b64encode(secret.encode()).decode(),
secret[:8],
]
def _assert_no_leak(text: str, secret: str, where: str) -> None:
"""Helper: assert no encoding of *secret* appears in *text*."""
for encoding in _all_encodings_of(secret):
assert encoding not in text, (
f"password leaked at {where} as encoding {encoding!r}. The "
f"except handler must call redact_secrets(str(e), password) "
f"and use logger.warning (not logger.exception)."
)
# ---------------------------------------------------------------------------
# database/encrypted_db.py
# ---------------------------------------------------------------------------
class TestEncryptedDBPasswordLeakage:
"""``DatabaseManager`` lifecycle methods receive the user's SQLCipher
master password and pass it through to ``set_sqlcipher_key`` /
``create_sqlcipher_connection`` / ``BackupService``. An exception in
any of those paths can carry frame locals containing the plaintext
password under loguru ``diagnose=True``. The except handlers must
drop the traceback chain and redact str(e).
"""
def test_change_password_does_not_leak_either_password(
self, loguru_caplog_full, tmp_path
):
"""``change_password`` has BOTH ``old_password`` and
``new_password`` in lexical scope. A failure in
``open_user_database`` or ``set_sqlcipher_rekey`` whose exception
message embeds either password must not surface to logs.
"""
from local_deep_research.database.encrypted_db import DatabaseManager
manager = DatabaseManager.__new__(DatabaseManager)
manager.has_encryption = True
manager.connections = {}
manager._connections_lock = MagicMock()
manager._connections_lock.__enter__ = lambda *a: None
manager._connections_lock.__exit__ = lambda *a: None
# Drive _get_user_db_path -> a real, existing path so the
# ``if not db_path.exists()`` short-circuit doesn't fire.
fake_db = tmp_path / "fake.db"
fake_db.touch()
manager._get_user_db_path = lambda u: fake_db
manager.close_user_database = lambda u: None
# Construct an exception whose str() embeds BOTH passwords —
# simulates a SQLAlchemy OperationalError that carries frame
# locals from set_sqlcipher_rekey's traceback.
exc = RuntimeError(
f"sqlcipher rekey failed: old={_LEAKED_OLD_PASSWORD} "
f"new={_LEAKED_PASSWORD} target={fake_db}"
)
manager.open_user_database = MagicMock(side_effect=exc)
with loguru_caplog_full.at_level("DEBUG"):
result = manager.change_password(
"alice", _LEAKED_OLD_PASSWORD, _LEAKED_PASSWORD
)
assert result is False
_assert_no_leak(
loguru_caplog_full.text, _LEAKED_PASSWORD, "change_password new_pw"
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_OLD_PASSWORD,
"change_password old_pw",
)
assert "Failed to change password" in loguru_caplog_full.text, (
"test did not exercise the except branch — check the fixtures"
)
def test_open_user_database_outer_catch_does_not_leak(
self, loguru_caplog_full, tmp_path
):
"""The outer ``except Exception`` in ``open_user_database`` (was
line 688 in the issue's listing) fires when the engine fails to
open. ``password`` is in scope and was passed into the engine
creator closure via ``set_sqlcipher_key`` — an OperationalError
traceback could leak it.
"""
from local_deep_research.database.encrypted_db import DatabaseManager
manager = DatabaseManager.__new__(DatabaseManager)
# has_encryption=True drives the SQLCipher-engine branch, which
# uses a custom creator with ``sqlite://`` (no event.listen).
manager.has_encryption = True
manager.connections = {}
# __init__ is bypassed here; mirror its per-user init-lock dict so
# open_user_database -> _get_init_lock has its backing store.
manager._init_locks = {}
manager._connections_lock = MagicMock()
manager._connections_lock.__enter__ = lambda *a: None
manager._connections_lock.__exit__ = lambda *a: None
manager._pool_class = type("StubPool", (), {})
manager._get_pool_kwargs = lambda: {}
db_path = tmp_path / "alice.db"
db_path.touch()
manager._get_user_db_path = lambda u: db_path
# Drive an exception from the SELECT 1 test connect that
# follows engine creation.
exc = RuntimeError(
f"connect failed: dsn=sqlite:///x?key={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.encrypted_db.create_engine"
) as mock_create:
with patch(
"local_deep_research.database.encrypted_db.has_per_database_salt",
return_value=True,
):
mock_engine = MagicMock()
mock_engine.connect.side_effect = exc
mock_create.return_value = mock_engine
result = manager.open_user_database(
"alice", _LEAKED_PASSWORD
)
assert result is None
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"open_user_database outer catch",
)
assert "Failed to open database" in loguru_caplog_full.text
def test_open_user_database_migration_failure_sanitizes_typed_error(
self, loguru_caplog_full, tmp_path
):
"""A migration failure re-raises as ``DatabaseInitializationError``.
The typed error must carry a redacted message and no exception
chain (``from None``, ADR-0003): callers (e.g.
``thread_local_session``) log it, and the broken chain plus
redacted message ensure the original exception — and its frame
locals holding the password — can't be rendered, defeating the
redaction applied at the raise site.
"""
from local_deep_research.database.encrypted_db import (
DatabaseInitializationError,
DatabaseManager,
)
manager = DatabaseManager.__new__(DatabaseManager)
manager.has_encryption = True
manager.connections = {}
# __init__ is bypassed here; mirror its per-user init-lock dict so
# open_user_database -> _get_init_lock has its backing store.
manager._init_locks = {}
manager._connections_lock = MagicMock()
manager._connections_lock.__enter__ = lambda *a: None
manager._connections_lock.__exit__ = lambda *a: None
manager._pool_class = type("StubPool", (), {})
manager._get_pool_kwargs = lambda: {}
db_path = tmp_path / "alice.db"
db_path.touch()
manager._get_user_db_path = lambda u: db_path
# The SELECT 1 connection check passes; the migration step then
# raises with the password embedded (simulating an
# OperationalError that carries frame locals).
exc = RuntimeError(
f"migration failed: dsn=sqlite:///x?key={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.encrypted_db.create_engine",
return_value=MagicMock(),
):
with patch(
"local_deep_research.database.encrypted_db.has_per_database_salt",
return_value=True,
):
with patch(
"local_deep_research.database.alembic_runner.needs_migration",
return_value=False,
):
with patch(
"local_deep_research.database.initialize.initialize_database",
side_effect=exc,
):
with pytest.raises(
DatabaseInitializationError
) as excinfo:
manager.open_user_database(
"alice", _LEAKED_PASSWORD
)
# The typed error's own message must be redacted, and the chain
# broken so a downstream logger.exception can't render init_err.
_assert_no_leak(
str(excinfo.value),
_LEAKED_PASSWORD,
"DatabaseInitializationError message",
)
assert excinfo.value.__cause__ is None
assert excinfo.value.__suppress_context__ is True
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"open_user_database migration catch",
)
assert "Database migration failed" in loguru_caplog_full.text
# ---------------------------------------------------------------------------
# web/queue/processor_v2.py
# ---------------------------------------------------------------------------
class TestProcessorV2PasswordLeakage:
"""``QueueProcessorV2._start_research_directly`` receives the user's
password and forwards it to ``start_research_process``. A failure in
the research startup path (or in the active-record DB write) whose
exception embeds the password must not reach logs.
"""
def test_start_research_directly_does_not_leak_on_research_failure(
self, loguru_caplog_full
):
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
exc = RuntimeError(
f"could not start research thread, "
f"connection url=sqlite:///x.db?pwd={_LEAKED_PASSWORD}"
)
# The initial active-record create + status update succeeds, and
# then start_research_process raises with the password in the
# exception message.
fake_session = MagicMock()
fake_session.__enter__ = lambda s: s
fake_session.__exit__ = lambda s, *a: None
fake_session.query.return_value.filter_by.return_value.first.return_value = None
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.web.queue.processor_v2.get_user_db_session",
return_value=fake_session,
):
with patch(
"local_deep_research.web.queue.processor_v2.UserQueueService"
):
with patch(
"local_deep_research.web.queue.processor_v2.start_research_process",
side_effect=exc,
):
processor._start_research_directly(
username="alice",
research_id="r1",
password=_LEAKED_PASSWORD,
query="test",
mode="quick",
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_start_research_directly",
)
# Sanity: the failure branch ran.
assert (
"Failed to start research" in loguru_caplog_full.text
or "Failed to create active research record"
in loguru_caplog_full.text
)
def test_notify_research_queued_does_not_leak(self, loguru_caplog_full):
"""``notify_research_queued`` retrieves the session password and
opens the user DB in its direct-execution branch, then falls back
to a queue-status update. Both except handlers have ``password``
in scope; an exception from either path must not leak it.
"""
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
exc = RuntimeError(f"open failed: key={_LEAKED_PASSWORD} db=alice.db")
with loguru_caplog_full.at_level("DEBUG"):
with (
patch(
"local_deep_research.web.queue.processor_v2.session_password_store"
) as mock_store,
patch(
"local_deep_research.web.queue.processor_v2.db_manager"
) as mock_db,
patch(
"local_deep_research.web.queue.processor_v2.get_user_db_session",
side_effect=exc,
),
):
mock_store.get_session_password.return_value = _LEAKED_PASSWORD
mock_db.open_user_database.side_effect = exc
processor.notify_research_queued(
"alice", "r1", session_id="s1", query="q", mode="quick"
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"notify_research_queued",
)
# Sanity: both except branches ran (direct-execution error, then
# the queue-status fallback error).
assert "Error in direct execution" in loguru_caplog_full.text
assert "Failed to update queue status" in loguru_caplog_full.text
def test_process_user_queue_does_not_leak(self, loguru_caplog_full):
"""``_process_user_queue`` retrieves the session password before
its try block and passes it into ``open_user_database`` /
``get_user_db_session``. The catch-all must redact it.
"""
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
exc = RuntimeError(f"sqlcipher key rejected: {_LEAKED_PASSWORD}")
with loguru_caplog_full.at_level("DEBUG"):
with (
patch(
"local_deep_research.web.queue.processor_v2.session_password_store"
) as mock_store,
patch(
"local_deep_research.web.queue.processor_v2.db_manager"
) as mock_db,
):
mock_store.get_session_password.return_value = _LEAKED_PASSWORD
mock_db.open_user_database.side_effect = exc
result = processor._process_user_queue("alice", "s1")
assert result is False
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_process_user_queue",
)
assert "Error processing queue for user" in loguru_caplog_full.text
def test_notify_research_completed_does_not_leak(self, loguru_caplog_full):
"""``notify_research_completed`` has ``user_password`` as a
parameter and passes it into ``get_user_db_session``.
"""
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
exc = RuntimeError(
f"session open failed: dsn=sqlite:///x?key={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with (
patch(
"local_deep_research.web.queue.processor_v2.get_user_db_session",
side_effect=exc,
),
patch(
"local_deep_research.research_library.search.services.research_history_indexer.auto_convert_research"
),
):
processor.notify_research_completed(
"alice", "r1", user_password=_LEAKED_PASSWORD
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"notify_research_completed",
)
assert "Failed to update completion status" in loguru_caplog_full.text
def test_notify_research_failed_does_not_leak(self, loguru_caplog_full):
"""``notify_research_failed`` — same contract as the completed
notification.
"""
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
exc = RuntimeError(
f"session open failed: dsn=sqlite:///x?key={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.web.queue.processor_v2.get_user_db_session",
side_effect=exc,
):
processor.notify_research_failed(
"alice",
"r1",
error_message="boom",
user_password=_LEAKED_PASSWORD,
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"notify_research_failed",
)
assert "Failed to update failure status" in loguru_caplog_full.text
def test_start_queued_researches_spawn_failure_does_not_leak(
self, loguru_caplog_full
):
"""The spawn-failure handler in ``_start_queued_researches`` has
``password`` as a parameter. A spawn exception whose message
embeds it must be redacted — and the rollback-failure debug log
in the same handler must not re-render the traceback.
"""
import threading
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
processor._spawn_retry_counts = {}
processor._spawn_retry_counts_lock = threading.Lock()
processor._reclaim_stranded_queue_rows = lambda *a: None
exc = RuntimeError(
f"spawn failed: thread env carried pwd={_LEAKED_PASSWORD}"
)
processor._start_research = MagicMock(side_effect=exc)
queued_item = MagicMock()
queued_item.research_id = "r1"
db_session = MagicMock()
query_chain = db_session.query.return_value.filter_by.return_value
query_chain.order_by.return_value.limit.return_value.all.return_value = [
queued_item
]
query_chain.update.return_value = 1 # claim succeeds
# Rollback after the spawn failure raises too, with the password
# embedded — exercises the redacted debug path.
db_session.rollback.side_effect = RuntimeError(
f"rollback failed: {_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
processor._start_queued_researches(
db_session,
MagicMock(), # queue_service
"alice",
_LEAKED_PASSWORD,
available_slots=1,
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_start_queued_researches spawn failure",
)
assert "Error starting queued research" in loguru_caplog_full.text
def test_process_user_request_does_not_leak(self, loguru_caplog_full):
"""``process_user_request`` retrieves the session password inside
its try block (hence the ``password = None`` pre-declaration) and
opens the user DB. The catch-all must redact it.
"""
import threading
from local_deep_research.web.queue.processor_v2 import (
QueueProcessorV2,
)
processor = QueueProcessorV2.__new__(QueueProcessorV2)
processor._users_to_check = set()
processor._users_lock = threading.Lock()
exc = RuntimeError(f"open failed, key in dsn: {_LEAKED_PASSWORD}")
with loguru_caplog_full.at_level("DEBUG"):
with (
patch(
"local_deep_research.web.queue.processor_v2.session_password_store"
) as mock_store,
patch(
"local_deep_research.web.queue.processor_v2.db_manager"
) as mock_db,
):
mock_store.get_session_password.return_value = _LEAKED_PASSWORD
mock_db.open_user_database.side_effect = exc
result = processor.process_user_request("alice", "s1")
assert result == 0
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"process_user_request",
)
assert "Error in process_user_request" in loguru_caplog_full.text
# ---------------------------------------------------------------------------
# scheduler/background.py
# ---------------------------------------------------------------------------
class TestSchedulerBackgroundPasswordLeakage:
"""The scheduler retrieves user passwords from its credential store
and passes them into encrypted-DB session contexts. Every
``except Exception`` site in those flows must scrub the password
from str(e) before logging.
"""
@pytest.fixture
def scheduler(self):
"""Build a ``BackgroundJobScheduler`` with a populated credential
store but no actual APScheduler running — enough for the except
handlers to be exercised in isolation.
"""
from local_deep_research.scheduler.background import (
BackgroundJobScheduler,
SchedulerCredentialStore,
)
sched = BackgroundJobScheduler.__new__(BackgroundJobScheduler)
sched._credential_store = SchedulerCredentialStore(ttl_hours=1)
sched._credential_store.store("alice", _LEAKED_PASSWORD)
sched.user_sessions = {
"alice": {"last_activity": None, "scheduled_jobs": set()}
}
sched.lock = MagicMock()
sched.lock.__enter__ = lambda *a: None
sched.lock.__exit__ = lambda *a: None
sched.scheduler = MagicMock()
sched.is_running = True
sched.config = {}
return sched
def test_trigger_subscription_research_sync_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""``_trigger_subscription_research_sync`` retrieves the
password from the credential store and passes it through to
``get_user_db_session`` and ``quick_summary``. A SQLAlchemy
/ requests exception from any of those paths must not surface
the password.
"""
exc = RuntimeError(f"db session open failed: pwd={_LEAKED_PASSWORD}")
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=exc,
):
scheduler._trigger_subscription_research_sync(
"alice",
{"id": 42, "name": "test sub", "query": "test"},
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_trigger_subscription_research_sync",
)
assert "Error triggering research" in loguru_caplog_full.text
def test_schedule_user_subscriptions_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""``_schedule_user_subscriptions`` retrieves the password and
opens an encrypted DB session. Failure inside that block must
not leak the password.
"""
exc = RuntimeError(
f"NewsSubscription query failed: dsn=sqlite:///x?pwd={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=exc,
):
# _schedule_user_subscriptions also calls
# _schedule_document_processing at the end — stub it so
# we isolate the failure to the subscription branch.
scheduler._schedule_document_processing = lambda u: None
scheduler._schedule_user_subscriptions("alice")
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_schedule_user_subscriptions",
)
assert "Error scheduling subscriptions" in loguru_caplog_full.text
def test_check_user_overdue_subscriptions_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""``_check_user_overdue_subscriptions`` retrieves the password
and opens an encrypted DB session. Same contract.
"""
exc = RuntimeError(
f"overdue query failed, pwd={_LEAKED_PASSWORD} in dsn"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=exc,
):
scheduler._check_user_overdue_subscriptions("alice")
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_check_user_overdue_subscriptions",
)
assert "Error checking overdue subscriptions" in loguru_caplog_full.text
def test_check_subscription_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""``_check_subscription`` retrieves the password from the
credential store and opens an encrypted DB session. A failure
from that path must not surface the password.
"""
exc = RuntimeError(
f"subscription refresh failed: dsn=sqlite:///x?pwd={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=exc,
):
scheduler._check_subscription("alice", 42)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_check_subscription",
)
assert "Error checking subscription 42" in loguru_caplog_full.text
def test_get_document_scheduler_settings_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""``_get_document_scheduler_settings`` retrieves the password
before its try block and passes it into ``get_user_db_session``.
On failure it must log redacted and fall back to defaults.
"""
import threading
scheduler._settings_cache = {}
scheduler._settings_cache_lock = threading.Lock()
exc = RuntimeError(
f"settings fetch failed: key={_LEAKED_PASSWORD} db=alice"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=exc,
):
settings = scheduler._get_document_scheduler_settings("alice")
# Falls back to defaults rather than raising.
assert settings is not None
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_get_document_scheduler_settings",
)
assert "Error fetching settings" in loguru_caplog_full.text
def test_store_research_result_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""``_store_research_result`` receives the password as a
function parameter and passes it into ``get_user_db_session``.
A failure anywhere inside the storage block must not surface
the password.
"""
exc = RuntimeError(
f"history insert failed: dsn=sqlite:///x?pwd={_LEAKED_PASSWORD}"
)
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=exc,
):
scheduler._store_research_result(
username="alice",
password=_LEAKED_PASSWORD,
research_id="r1",
subscription_id=42,
result={},
subscription={},
)
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_store_research_result",
)
assert "Error storing research result" in loguru_caplog_full.text
def test_process_user_documents_snapshot_failure_does_not_leak(
self, loguru_caplog_full, scheduler
):
"""The settings-snapshot handler inside ``_process_user_documents``
runs nested inside ``get_user_db_session(username, password)`` —
the password is live in the frame. A failure building the
snapshot must not surface it.
"""
exc = RuntimeError(
f"snapshot build failed: dsn=sqlite:///x?pwd={_LEAKED_PASSWORD}"
)
# Document-scheduler settings: one processing flag enabled so
# the function reaches the snapshot block instead of returning
# early; no last_run so the date filter is skipped.
doc_settings = MagicMock(
download_pdfs=True,
extract_text=False,
generate_rag=False,
last_run=None,
)
scheduler._get_document_scheduler_settings = lambda u: doc_settings
scheduler._arm_egress_backstop = lambda sm, u: None
# One fake completed research session so the snapshot block is
# reached (empty result returns before it).
fake_research = MagicMock(id="r1", title="t", completed_at=None)
fake_db = MagicMock()
fake_db.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
fake_research
]
fake_session = MagicMock()
fake_session.__enter__ = lambda s: fake_db
fake_session.__exit__ = lambda s, *a: None
with loguru_caplog_full.at_level("DEBUG"):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
return_value=fake_session,
):
with patch(
"local_deep_research.settings.manager.SettingsManager"
) as mock_sm:
mock_sm.return_value.get_settings_snapshot.side_effect = exc
with patch(
"local_deep_research.research_library.services.download_service.DownloadService"
):
scheduler._process_user_documents("alice")
_assert_no_leak(
loguru_caplog_full.text,
_LEAKED_PASSWORD,
"_process_user_documents snapshot handler",
)
assert "Could not build settings snapshot" in loguru_caplog_full.text