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

190 lines
7.1 KiB
Python

"""Unit tests for ChatService user isolation.
These tests verify that users cannot access each other's data.
In LDR's architecture, each user has a separate encrypted database,
providing strong isolation at the storage layer.
These are isolation tests — they only care that the correct username
reaches ``get_user_db_session``. Whether the service then succeeds,
returns empty, or raises is irrelevant to the isolation property, so
each test catches the expected service-layer exception
(``ChatSessionNotFound`` for missing rows, ``SQLAlchemyError`` from
the now-strict read methods) and proceeds to assert on the captured
username.
"""
import contextlib
from contextlib import contextmanager
from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy.exc import SQLAlchemyError
from src.local_deep_research.chat.service import ChatSessionNotFound
# Exceptions every isolation test must tolerate: the MagicMock query
# chains don't match the real SQLAlchemy shape used by some methods, so
# the service may raise after the username has already been captured —
# isolation only cares about the capture.
_ISOLATION_EXPECTED = (ChatSessionNotFound, SQLAlchemyError, TypeError)
@pytest.fixture
def username_capturing_db():
"""Patch ``get_user_db_session`` to capture every username that
reaches the data layer.
Returns a tuple ``(captured_usernames, configure)`` where
``configure`` lets each test customise the inner ``MagicMock``
session (e.g. wire a specific ``query`` chain) before the patch
goes live. Replaces the 8-line ``captured_username`` +
``@contextmanager`` boilerplate that was duplicated across 7 test
methods.
"""
captured: list[str] = []
mock_session = MagicMock()
@contextmanager
def _fake_get_user_db_session(username, password=None):
captured.append(username)
yield mock_session
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
_fake_get_user_db_session,
):
yield captured, mock_session
class TestUserIsolation:
"""Tests verifying user data isolation in ChatService."""
def test_get_session_uses_correct_username_for_db_access(
self, username_capturing_db
):
"""get_session queries the correct user's database."""
from src.local_deep_research.chat.service import ChatService
captured, mock_session = username_capturing_db
mock_session.query.return_value.filter_by.return_value.first.return_value = None
service = ChatService(username="alice")
with contextlib.suppress(ChatSessionNotFound):
service.get_session("some-session-id")
assert captured == ["alice"]
def test_get_messages_uses_correct_username_for_db_access(
self, username_capturing_db
):
"""get_session_messages queries the correct user's database."""
from src.local_deep_research.chat.service import ChatService
captured, _ = username_capturing_db
service = ChatService(username="bob")
with contextlib.suppress(*_ISOLATION_EXPECTED):
service.get_session_messages("some-session-id")
assert captured == ["bob"]
def test_list_sessions_uses_correct_username_for_db_access(
self, username_capturing_db
):
"""list_sessions queries the correct user's database."""
from src.local_deep_research.chat.service import ChatService
captured, _ = username_capturing_db
service = ChatService(username="charlie")
with contextlib.suppress(*_ISOLATION_EXPECTED):
service.list_sessions()
assert captured == ["charlie"]
def test_update_context_uses_correct_username_for_db_access(
self, username_capturing_db
):
"""update_accumulated_context queries the correct user's database."""
from src.local_deep_research.chat.service import ChatService
captured, mock_session = username_capturing_db
mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = None
mock_session.query.return_value.filter_by.return_value.first.return_value = None
service = ChatService(username="diana")
service.update_accumulated_context("some-session-id")
assert captured == ["diana"]
def test_delete_session_uses_correct_username_for_db_access(
self, username_capturing_db
):
"""delete_session queries the correct user's database."""
from src.local_deep_research.chat.service import ChatService
captured, mock_session = username_capturing_db
mock_session.query.return_value.filter_by.return_value.first.return_value = None
service = ChatService(username="eve")
service.delete_session("some-session-id")
assert captured == ["eve"]
class TestCrossUserAccess:
"""Tests verifying cross-user access is prevented."""
def test_different_services_use_different_usernames(
self, username_capturing_db
):
"""Different ChatService instances use their own usernames."""
from src.local_deep_research.chat.service import ChatService
captured, _ = username_capturing_db
service_alice = ChatService(username="alice")
service_bob = ChatService(username="bob")
# Both create sessions
service_alice.create_session()
service_bob.create_session()
# Verify each service used its own username — order-preserving.
assert captured[0] == "alice"
assert captured[1] == "bob"
def test_each_operation_passes_username_to_database(
self, username_capturing_db
):
"""All CRUD operations consistently use the service username."""
from src.local_deep_research.chat.service import ChatService
captured, mock_session = username_capturing_db
mock_session.query.return_value.filter_by.return_value.first.return_value = None
mock_session.query.return_value.filter_by.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value = []
mock_session.query.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value = []
service = ChatService(username="consistent_user")
# Perform various operations. Each get_user_db_session entry
# must record the username; the operations themselves may
# legitimately raise (ChatSessionNotFound for missing rows,
# SQLAlchemyError from the now-strict list/messages reads,
# or TypeError from MagicMock chains that don't match the
# real query shape). Isolate each call so a later op still
# gets to record its username.
service.create_session()
for op in (
lambda: service.get_session("session-1"),
lambda: service.get_session_messages("session-1"),
lambda: service.list_sessions(),
lambda: service.delete_session("session-1"),
):
with contextlib.suppress(*_ISOLATION_EXPECTED):
op()
# Every operation must have used the same username
assert all(u == "consistent_user" for u in captured), captured
assert len(captured) >= 5 # create + 4 ops