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
302 lines
10 KiB
Python
302 lines
10 KiB
Python
"""Tests for database/session_context.py."""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
from flask import Flask
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create test Flask application."""
|
|
app = Flask(__name__)
|
|
app.config["TESTING"] = True
|
|
app.secret_key = "test-secret-key"
|
|
return app
|
|
|
|
|
|
class TestDatabaseSessionError:
|
|
"""Tests for DatabaseSessionError exception."""
|
|
|
|
def test_exception_can_be_raised(self):
|
|
"""Test that DatabaseSessionError can be raised."""
|
|
from local_deep_research.database.session_context import (
|
|
DatabaseSessionError,
|
|
)
|
|
|
|
with pytest.raises(DatabaseSessionError):
|
|
raise DatabaseSessionError("Test error")
|
|
|
|
def test_exception_message(self):
|
|
"""Test that exception preserves message."""
|
|
from local_deep_research.database.session_context import (
|
|
DatabaseSessionError,
|
|
)
|
|
|
|
try:
|
|
raise DatabaseSessionError("Custom error message")
|
|
except DatabaseSessionError as e:
|
|
assert str(e) == "Custom error message"
|
|
|
|
|
|
class TestGetUserDbSession:
|
|
"""Tests for get_user_db_session context manager."""
|
|
|
|
def test_raises_when_no_username_provided(self, app):
|
|
"""Test that error is raised when no username available."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
DatabaseSessionError,
|
|
)
|
|
|
|
with app.test_request_context():
|
|
# No username in session
|
|
with pytest.raises(
|
|
DatabaseSessionError, match="No authenticated user"
|
|
):
|
|
with get_user_db_session():
|
|
pass
|
|
|
|
def test_uses_provided_username(self, app):
|
|
"""Test that explicitly provided username is used."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
)
|
|
|
|
with app.test_request_context():
|
|
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_session:
|
|
mock_session = Mock()
|
|
mock_get_session.return_value = mock_session
|
|
|
|
with get_user_db_session(
|
|
username="testuser", password="testpass"
|
|
) as session:
|
|
assert session is mock_session
|
|
|
|
def test_uses_flask_session_username_when_not_provided(self, app):
|
|
"""Test that Flask session username is used when not explicitly provided."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
UNENCRYPTED_DB_PLACEHOLDER,
|
|
)
|
|
|
|
with app.test_request_context():
|
|
from flask import session as flask_session
|
|
|
|
flask_session["username"] = "flask_user"
|
|
|
|
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_session:
|
|
mock_session = Mock()
|
|
mock_get_session.return_value = mock_session
|
|
|
|
with get_user_db_session() as _session:
|
|
mock_get_session.assert_called_once_with(
|
|
"flask_user", UNENCRYPTED_DB_PLACEHOLDER
|
|
)
|
|
|
|
def test_raises_when_encrypted_db_requires_password(self, app):
|
|
"""Test error when encrypted DB accessed without password."""
|
|
from local_deep_research.database.session_context import (
|
|
get_user_db_session,
|
|
DatabaseSessionError,
|
|
)
|
|
|
|
with app.test_request_context():
|
|
from flask import session as flask_session
|
|
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.db_manager"
|
|
) as mock_db:
|
|
mock_db.has_encryption = True
|
|
mock_db.connections = {}
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_search_context"
|
|
) as mock_ctx:
|
|
mock_ctx.return_value = None
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_passwords.session_password_store"
|
|
) as mock_store:
|
|
mock_store.get_session_password.return_value = None
|
|
|
|
with pytest.raises(
|
|
DatabaseSessionError, match="requires password"
|
|
):
|
|
with get_user_db_session():
|
|
pass
|
|
|
|
|
|
class TestWithUserDatabase:
|
|
"""Tests for with_user_database decorator."""
|
|
|
|
def test_decorator_injects_db_session(self, app):
|
|
"""Test that decorator injects db_session as first argument."""
|
|
from local_deep_research.database.session_context import (
|
|
with_user_database,
|
|
)
|
|
|
|
@with_user_database
|
|
def test_func(db_session, arg1, arg2):
|
|
return (db_session, arg1, arg2)
|
|
|
|
with app.test_request_context():
|
|
from flask import session as flask_session
|
|
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_ctx:
|
|
mock_session = Mock()
|
|
mock_ctx.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_ctx.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = test_func("value1", "value2")
|
|
assert result == (mock_session, "value1", "value2")
|
|
|
|
def test_decorator_passes_kwargs(self, app):
|
|
"""Test that decorator passes keyword arguments."""
|
|
from local_deep_research.database.session_context import (
|
|
with_user_database,
|
|
)
|
|
|
|
@with_user_database
|
|
def test_func(db_session, key1=None, key2=None):
|
|
return {"session": db_session, "key1": key1, "key2": key2}
|
|
|
|
with app.test_request_context():
|
|
from flask import session as flask_session
|
|
|
|
flask_session["username"] = "testuser"
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_ctx:
|
|
mock_session = Mock()
|
|
mock_ctx.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_ctx.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = test_func(key1="a", key2="b")
|
|
assert result["key1"] == "a"
|
|
assert result["key2"] == "b"
|
|
|
|
def test_decorator_extracts_special_kwargs(self, app):
|
|
"""Test that _username and _password are extracted from kwargs."""
|
|
from local_deep_research.database.session_context import (
|
|
with_user_database,
|
|
)
|
|
|
|
@with_user_database
|
|
def test_func(db_session):
|
|
return db_session
|
|
|
|
with app.app_context():
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_ctx:
|
|
mock_session = Mock()
|
|
mock_ctx.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_ctx.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
test_func(_username="custom_user", _password="custom_pass")
|
|
|
|
# Verify get_user_db_session was called with the custom credentials
|
|
mock_ctx.assert_called_once_with("custom_user", "custom_pass")
|
|
|
|
|
|
class TestDatabaseAccessMixin:
|
|
"""Tests for DatabaseAccessMixin class."""
|
|
|
|
def test_get_db_session_raises_deprecation_warning(self, app):
|
|
"""Test that get_db_session raises DeprecationWarning.
|
|
|
|
The method was deprecated because it returned a closed session
|
|
(context manager exits before returning).
|
|
"""
|
|
from local_deep_research.database.session_context import (
|
|
DatabaseAccessMixin,
|
|
)
|
|
import pytest
|
|
|
|
class TestService(DatabaseAccessMixin):
|
|
pass
|
|
|
|
service = TestService()
|
|
|
|
with pytest.raises(DeprecationWarning) as exc_info:
|
|
service.get_db_session()
|
|
|
|
assert "deprecated" in str(exc_info.value).lower()
|
|
assert "get_user_db_session" in str(exc_info.value)
|
|
|
|
|
|
class TestUnencryptedDbPlaceholder:
|
|
"""Tests for UNENCRYPTED_DB_PLACEHOLDER constant."""
|
|
|
|
def test_placeholder_value(self):
|
|
"""Test the placeholder constant value."""
|
|
from local_deep_research.database.session_context import (
|
|
UNENCRYPTED_DB_PLACEHOLDER,
|
|
)
|
|
|
|
assert UNENCRYPTED_DB_PLACEHOLDER == "unencrypted-mode"
|
|
assert isinstance(UNENCRYPTED_DB_PLACEHOLDER, str)
|
|
|
|
|
|
class TestSafeRollback:
|
|
"""Tests for the safe_rollback helper introduced for issue #3827."""
|
|
|
|
def test_calls_session_rollback(self):
|
|
from local_deep_research.database.session_context import (
|
|
safe_rollback,
|
|
)
|
|
|
|
session = Mock()
|
|
safe_rollback(session, "ctx")
|
|
session.rollback.assert_called_once()
|
|
|
|
def test_swallows_rollback_exception(self):
|
|
"""Rollback errors must not propagate — call sites are exception
|
|
handlers themselves and a raise here would mask the original error.
|
|
"""
|
|
from local_deep_research.database.session_context import (
|
|
safe_rollback,
|
|
)
|
|
|
|
session = Mock()
|
|
session.rollback.side_effect = RuntimeError("simulated rollback fail")
|
|
# Must NOT raise.
|
|
safe_rollback(session, "ctx")
|
|
session.rollback.assert_called_once()
|
|
|
|
def test_works_without_context_argument(self):
|
|
from local_deep_research.database.session_context import (
|
|
safe_rollback,
|
|
)
|
|
|
|
session = Mock()
|
|
safe_rollback(session)
|
|
session.rollback.assert_called_once()
|