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
404 lines
14 KiB
Python
404 lines
14 KiB
Python
"""Extended tests for database/session_context.py - targeting untested paths.
|
|
|
|
Covers:
|
|
- ensure_db_session() decorator (lines 160-212, entirely untested)
|
|
- get_user_db_session() reusing g.db_session
|
|
- get_user_db_session() password from g.user_password
|
|
- get_user_db_session() password from session_password_store
|
|
- get_user_db_session() password from thread context
|
|
- get_user_db_session() get_metrics_session returns None
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from flask import Flask, g, session as flask_session
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create test Flask application."""
|
|
app = Flask(__name__)
|
|
app.config["TESTING"] = True
|
|
app.secret_key = "test-secret-key"
|
|
return app
|
|
|
|
|
|
# ── ensure_db_session decorator ──────────────────────────────────
|
|
|
|
|
|
class TestEnsureDbSession:
|
|
"""Tests for ensure_db_session decorator (lines 160-212, entirely untested)."""
|
|
|
|
def test_no_username_passes_through(self, app):
|
|
"""When no username in session, view runs without db_session setup."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
view_called = []
|
|
|
|
@ensure_db_session
|
|
def my_view():
|
|
view_called.append(True)
|
|
return "ok"
|
|
|
|
with app.test_request_context():
|
|
result = my_view()
|
|
|
|
assert result == "ok"
|
|
assert view_called == [True]
|
|
|
|
def test_connected_user_gets_session(self, app):
|
|
"""Connected user gets g.db_session set from db_manager."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
|
|
@ensure_db_session
|
|
def my_view():
|
|
return g.db_session
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.is_user_connected.return_value = True
|
|
mock_db.get_session.return_value = mock_session
|
|
|
|
result = my_view()
|
|
|
|
assert result is mock_session
|
|
mock_db.get_session.assert_called_once_with("testuser")
|
|
|
|
def test_encrypted_db_not_connected_redirects_to_login(self, app):
|
|
"""Encrypted DB + disconnected user → clear session + redirect."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
# Register auth blueprint with login route
|
|
from flask import Blueprint
|
|
|
|
auth_bp = Blueprint("auth", __name__)
|
|
|
|
@auth_bp.route("/login")
|
|
def login():
|
|
return "login page"
|
|
|
|
app.register_blueprint(auth_bp)
|
|
|
|
@ensure_db_session
|
|
def my_view():
|
|
return "should not reach"
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
flask_session["some_data"] = "preserve_check"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.is_user_connected.return_value = False
|
|
mock_db.has_encryption = True
|
|
|
|
result = my_view()
|
|
|
|
# Should be a redirect response
|
|
assert result.status_code == 302
|
|
assert "/login" in result.location
|
|
|
|
def test_unencrypted_db_not_connected_reopens(self, app):
|
|
"""Unencrypted DB + disconnected user → reopen database."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
UNENCRYPTED_DB_PLACEHOLDER,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
|
|
@ensure_db_session
|
|
def my_view():
|
|
return g.db_session
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.is_user_connected.return_value = False
|
|
mock_db.has_encryption = False
|
|
mock_engine = Mock()
|
|
mock_db.open_user_database.return_value = mock_engine
|
|
mock_db.get_session.return_value = mock_session
|
|
|
|
result = my_view()
|
|
|
|
mock_db.open_user_database.assert_called_once_with(
|
|
"testuser", UNENCRYPTED_DB_PLACEHOLDER
|
|
)
|
|
assert result is mock_session
|
|
|
|
def test_unencrypted_reopen_fails_gracefully(self, app):
|
|
"""Failed unencrypted reopen doesn't crash — view still runs."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
@ensure_db_session
|
|
def my_view():
|
|
return "view ran"
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.is_user_connected.return_value = False
|
|
mock_db.has_encryption = False
|
|
mock_db.open_user_database.return_value = None # Engine failed
|
|
|
|
result = my_view()
|
|
|
|
assert result == "view ran"
|
|
|
|
def test_exception_during_session_setup_returns_500(self, app):
|
|
"""Exception in session setup returns 500 error response."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
@ensure_db_session
|
|
def my_view():
|
|
return "recovered"
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.is_user_connected.side_effect = RuntimeError("DB crash")
|
|
|
|
result = my_view()
|
|
|
|
assert result[1] == 500
|
|
|
|
def test_preserves_function_name(self):
|
|
"""Decorator preserves wrapped function name via functools.wraps."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
@ensure_db_session
|
|
def my_special_view():
|
|
pass
|
|
|
|
assert my_special_view.__name__ == "my_special_view"
|
|
|
|
def test_passes_args_to_view(self, app):
|
|
"""Decorator passes positional and keyword args to wrapped view."""
|
|
from local_deep_research.database.session_context import (
|
|
ensure_db_session,
|
|
)
|
|
|
|
@ensure_db_session
|
|
def my_view(item_id, mode="default"):
|
|
return f"{item_id}:{mode}"
|
|
|
|
with app.test_request_context():
|
|
# No username → passes through without DB setup
|
|
result = my_view(42, mode="edit")
|
|
|
|
assert result == "42:edit"
|
|
|
|
|
|
# ── get_user_db_session: g.db_session reuse ──────────────────────
|
|
|
|
|
|
class TestGetUserDbSessionReuse:
|
|
"""Tests for g.db_session reuse path (lines 62-65)."""
|
|
|
|
def test_reuses_existing_g_db_session(self, app):
|
|
"""When g.db_session exists, it is reused without creating new session."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
)
|
|
|
|
existing_session = Mock()
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
g.db_session = existing_session
|
|
|
|
with get_user_db_session() as session:
|
|
assert session is existing_session
|
|
|
|
|
|
# ── get_user_db_session: password resolution paths ───────────────
|
|
|
|
|
|
class TestGetUserDbSessionPasswordPaths:
|
|
"""Tests for password resolution fallback chain."""
|
|
|
|
def test_password_from_g_user_password(self, app):
|
|
"""Password is retrieved from g.user_password."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
g.user_password = "secret123"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.has_encryption = True
|
|
|
|
with patch(
|
|
"local_deep_research.database.thread_local_session.get_metrics_session"
|
|
) as mock_get:
|
|
mock_get.return_value = mock_session
|
|
|
|
with get_user_db_session() as session:
|
|
assert session is mock_session
|
|
|
|
mock_get.assert_called_once_with("testuser", "secret123")
|
|
|
|
def test_password_from_session_password_store(self, app):
|
|
"""Password is retrieved from session_password_store."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
flask_session["session_id"] = "sess-abc"
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db,
|
|
patch(
|
|
"local_deep_research.database.session_context.get_search_context"
|
|
) as mock_ctx,
|
|
):
|
|
mock_db.has_encryption = True
|
|
mock_ctx.return_value = None # No thread context
|
|
|
|
# session_password_store is imported locally inside get_user_db_session
|
|
with patch(
|
|
"local_deep_research.database.session_passwords.session_password_store"
|
|
) as mock_store:
|
|
mock_store.get_session_password.return_value = "store-pass"
|
|
|
|
with patch(
|
|
"local_deep_research.database.thread_local_session.get_metrics_session"
|
|
) as mock_get:
|
|
mock_get.return_value = mock_session
|
|
|
|
with get_user_db_session() as session:
|
|
assert session is mock_session
|
|
|
|
mock_get.assert_called_once_with(
|
|
"testuser", "store-pass"
|
|
)
|
|
mock_store.get_session_password.assert_called_once_with(
|
|
"testuser", "sess-abc"
|
|
)
|
|
|
|
def test_password_from_thread_context(self, app):
|
|
"""Password is retrieved from thread context (background threads)."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db,
|
|
patch(
|
|
"local_deep_research.database.session_context.get_search_context"
|
|
) as mock_ctx,
|
|
):
|
|
mock_db.has_encryption = True
|
|
mock_ctx.return_value = {"user_password": "thread-pass"}
|
|
|
|
with patch(
|
|
"local_deep_research.database.thread_local_session.get_metrics_session"
|
|
) as mock_get:
|
|
mock_get.return_value = mock_session
|
|
|
|
with get_user_db_session() as session:
|
|
assert session is mock_session
|
|
|
|
mock_get.assert_called_once_with("testuser", "thread-pass")
|
|
|
|
def test_get_metrics_session_returns_none_raises(self, app):
|
|
"""When get_metrics_session returns None, DatabaseSessionError is raised."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
DatabaseSessionError,
|
|
)
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.has_encryption = False
|
|
|
|
with patch(
|
|
"local_deep_research.database.thread_local_session.get_metrics_session"
|
|
) as mock_get:
|
|
mock_get.return_value = None
|
|
|
|
with pytest.raises(
|
|
DatabaseSessionError,
|
|
match="Could not establish session",
|
|
):
|
|
with get_user_db_session():
|
|
pass
|
|
|
|
def test_password_stored_in_g_after_success(self, app):
|
|
"""After successful session creation, password is stored in g."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
|
|
with app.test_request_context():
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.has_encryption = False
|
|
|
|
with patch(
|
|
"local_deep_research.database.thread_local_session.get_metrics_session"
|
|
) as mock_get:
|
|
mock_get.return_value = mock_session
|
|
|
|
with get_user_db_session() as _session:
|
|
# After yield, g.user_password should be set
|
|
assert hasattr(g, "user_password")
|