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

431 lines
14 KiB
Python

"""Fixtures for chat feature tests."""
import pytest
import uuid
from unittest.mock import MagicMock, patch
from contextlib import contextmanager
from datetime import datetime
@pytest.fixture(autouse=True)
def _stub_request_path_llm():
"""Keep the send-message request path from making real LLM calls.
build_research_context() condenses prior turns into a query-focused
summary via get_llm() before research is dispatched. Most chat tests
don't exercise that summary (they mock the research worker), so stub the
LLM to keep them fast and deterministic regardless of provider
availability. Tests that assert on the summary patch get_llm themselves,
which overrides this stub within their own context.
"""
fake_llm = MagicMock()
fake_llm.invoke.return_value = MagicMock(
content="stub conversation summary"
)
with patch(
"local_deep_research.config.llm_config.get_llm",
return_value=fake_llm,
):
yield
def generate_unique_test_username():
"""Generate a unique test username to avoid conflicts in parallel tests."""
return f"test_user_{uuid.uuid4().hex[:8]}"
def setup_query_mock_with_session(db_session_mock, session_obj):
"""
Set up query mock to return session for the call patterns used by ChatService:
- filter_by().first() — get_session, archive, delete, etc.
- filter_by().with_for_update().first() — update_accumulated_context
- execute(update(...).returning(...)).scalar_one_or_none() — add_message
(atomic increment-and-return; mutates session_obj.message_count to
mirror the SQL UPDATE side effect so callers can assert on it).
Args:
db_session_mock: The mock database session
session_obj: The mock session object to return (or None for not found)
"""
query_mock = MagicMock()
filter_result = MagicMock()
# Set up both patterns to return the same session
filter_result.first.return_value = session_obj
filter_result.with_for_update.return_value.first.return_value = session_obj
query_mock.filter_by.return_value = filter_result
db_session_mock.query.return_value = query_mock
def _execute_side_effect(_stmt, *args, **kwargs):
result = MagicMock()
if session_obj is None:
result.scalar_one_or_none.return_value = None
else:
current = session_obj.message_count or 0
session_obj.message_count = current + 1
result.scalar_one_or_none.return_value = session_obj.message_count
return result
db_session_mock.execute.side_effect = _execute_side_effect
return query_mock
@pytest.fixture
def mock_user_db_session():
"""Mock the database session context manager."""
session_mock = MagicMock()
@contextmanager
def _mock_session(username, password=None):
yield session_mock
with patch(
"src.local_deep_research.chat.service.get_user_db_session",
side_effect=_mock_session,
):
yield session_mock
@pytest.fixture
def chat_service(setup_database_for_all_tests):
"""Provide ChatService with test database."""
from src.local_deep_research.chat.service import ChatService
return ChatService(username="test_user")
@pytest.fixture
def csrf_authenticated_client(app_with_csrf, temp_data_dir):
"""Authenticated test client against the CSRF-enabled app.
Returns ``(client, csrf_token)``. The token is the value the client
must send as ``X-CSRFToken`` (or as a ``csrf_token`` form field)
for mutating requests to be accepted; tests that omit the header
verify that Flask-WTF rejects the request.
Mirrors the registration/login flow of the no-CSRF
``authenticated_client`` fixture in tests/conftest.py — auth pages
use ``WTForms``-managed CSRF tokens so the standard
register-then-login pattern still works against ``app_with_csrf``
when we read the token from the form's ``<input name="csrf_token">``
first.
"""
import re
import shutil
test_username = generate_unique_test_username()
test_password = "TestPass123"
encrypted_db_dir = temp_data_dir / "encrypted_databases"
if encrypted_db_dir.exists():
try:
shutil.rmtree(encrypted_db_dir)
except Exception:
pass
client = app_with_csrf.test_client()
csrf_input_re = re.compile(
rb'name="csrf_token"\s+(?:type="hidden"\s+)?value="([^"]+)"'
)
csrf_meta_re = re.compile(rb'name="csrf-token"\s+content="([^"]+)"')
def _extract_csrf(resp_data):
m = csrf_input_re.search(resp_data) or csrf_meta_re.search(resp_data)
return m.group(1).decode() if m else None
# 1. GET the register form to obtain its CSRF token, then POST
# register. Successful registration auto-logs the user in (route
# redirects to "/"), so a separate login step is not needed.
reg_form = client.get("/auth/register")
reg_token = _extract_csrf(reg_form.data)
if not reg_token:
raise RuntimeError("CSRF-fixture: no token in /auth/register form")
register_resp = client.post(
"/auth/register",
data={
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
"csrf_token": reg_token,
},
follow_redirects=False,
)
if register_resp.status_code not in [200, 302]:
raise RuntimeError(
f"CSRF-fixture registration failed: {register_resp.status_code} "
f"{register_resp.data.decode()[:300]}"
)
# 2. GET an authenticated page to harvest the post-login CSRF token
# that mutating chat-API requests must echo back. The chat page
# embeds it in <meta name="csrf-token">.
page = client.get("/chat/")
api_token = _extract_csrf(page.data)
if not api_token:
raise RuntimeError(
f"CSRF-fixture: no csrf-token in /chat/ "
f"(status={page.status_code}, "
f"location={page.headers.get('Location')})"
)
return client, api_token
@pytest.fixture
def sample_messages():
"""Sample message list for context manager tests."""
return [
{
"id": "msg-001",
"role": "user",
"content": "What is quantum computing?",
"message_type": "query",
"created_at": datetime(2024, 1, 15, 10, 0, 0),
},
{
"id": "msg-002",
"role": "assistant",
"content": "Quantum computing uses quantum mechanics principles like superposition and entanglement to process information. Unlike classical computers that use bits (0 or 1), quantum computers use qubits that can exist in multiple states simultaneously.",
"message_type": "response",
"research_id": "research-abc123",
"created_at": datetime(2024, 1, 15, 10, 1, 0),
},
{
"id": "msg-003",
"role": "user",
"content": "What are the practical applications?",
"message_type": "query",
"created_at": datetime(2024, 1, 15, 10, 2, 0),
},
{
"id": "msg-004",
"role": "assistant",
"content": "Practical applications include cryptography, drug discovery, financial modeling, and optimization problems.",
"message_type": "response",
"research_id": "research-def456",
"created_at": datetime(2024, 1, 15, 10, 3, 0),
},
]
@pytest.fixture
def sample_accumulated_context():
"""Sample accumulated context."""
return {
"key_entities": [
"quantum computing",
"qubits",
"superposition",
"entanglement",
],
"topics": ["physics", "computing", "cryptography"],
"summary": "Discussion about quantum computing fundamentals and applications.",
}
@pytest.fixture
def mock_chat_session():
"""Create a mock ChatSession object."""
session = MagicMock()
session.id = "session-test-123"
session.title = "Test Chat Session"
session.status = "active"
session.message_count = 0
session.accumulated_context = {}
session.created_at = datetime(2024, 1, 15, 10, 0, 0)
session.updated_at = datetime(2024, 1, 15, 10, 0, 0).isoformat()
return session
@pytest.fixture
def mock_settings_manager():
"""Mock the SettingsManager."""
manager_mock = MagicMock()
manager_mock.get_all_settings.return_value = {
"llm.provider": {"value": "ollama"},
"llm.model": {"value": "gemma:latest"},
"search.tool": {"value": "searxng"},
"search.iterations": {"value": 2},
"search.questions_per_iteration": {"value": 3},
}
return manager_mock
@pytest.fixture
def long_query_text():
"""A query text longer than 100 characters for title truncation tests."""
return (
"Can you explain the fundamental principles of quantum computing and how "
"it differs from classical computing, including practical applications "
"in cryptography, drug discovery, and financial modeling?"
)
@pytest.fixture
def many_messages():
"""Generate more than 10 messages for limit testing."""
messages = []
for i in range(15):
messages.append(
{
"id": f"msg-{i:03d}",
"role": "user" if i % 2 == 0 else "assistant",
"content": f"Message content {i}",
"message_type": "query" if i % 2 == 0 else "response",
"research_id": f"research-{i:03d}" if i % 2 == 1 else None,
"created_at": datetime(2024, 1, 15, 10, i, 0),
}
)
return messages
# =============================================================================
# Security Test Fixtures
# =============================================================================
@pytest.fixture
def second_user_client(app, temp_data_dir):
"""
Create a second authenticated test client for cross-user tests.
This creates a separate user to test user isolation and
cross-user access prevention.
"""
# Create unique test username
test_username = generate_unique_test_username()
test_password = "testpassword456"
# Create a test client
client = app.test_client()
# Register and login the second user
with client:
# Register new user
register_response = client.post(
"/auth/register",
data={
"username": test_username,
"password": test_password,
"confirm_password": test_password,
"acknowledge": "true",
},
follow_redirects=False,
)
if register_response.status_code not in [200, 302]:
raise Exception(
f"Second user registration failed: {register_response.status_code}"
)
# Login user
login_response = client.post(
"/auth/login",
data={"username": test_username, "password": test_password},
follow_redirects=False,
)
if login_response.status_code not in [200, 302]:
raise Exception(
f"Second user login failed: {login_response.status_code}"
)
return client
# =============================================================================
# XSS Test Fixtures
# =============================================================================
@pytest.fixture
def xss_payloads():
"""Common XSS attack vectors for testing."""
return {
"script_tags": [
"<script>alert('XSS')</script>",
"<script>alert(document.cookie)</script>",
"<SCRIPT>alert('XSS')</SCRIPT>",
"<script src='http://evil.com/xss.js'></script>",
],
"event_handlers": [
"<img src=x onerror=alert('XSS')>",
"<svg onload=alert('XSS')>",
"<body onload=alert('XSS')>",
"<div onmouseover=alert('XSS')>hover</div>",
],
"javascript_urls": [
"<a href='javascript:alert(1)'>Click</a>",
"<iframe src='javascript:alert(1)'></iframe>",
],
"nested": [
"<<script>script>alert('XSS')<</script>/script>",
"<scr<script>ipt>alert('XSS')</scr</script>ipt>",
],
"encoded": [
"&#60;script&#62;alert('XSS')&#60;/script&#62;",
"%3Cscript%3Ealert('XSS')%3C/script%3E",
],
}
@pytest.fixture
def malicious_session_ids():
"""Session ID values that could be used in attacks."""
return [
# Path traversal
"../../../etc/passwd",
"..%2F..%2Fetc%2Fpasswd",
"session-id/../../admin",
# Null bytes
"valid-id%00.txt",
"test\x00admin",
# SQL injection
"' OR '1'='1",
"'; DROP TABLE chat_sessions; --",
"1 UNION SELECT * FROM users--",
# XSS in IDs
"<script>alert(1)</script>",
"session-<img onerror=alert(1)>",
]
# =============================================================================
# Fuzz Test Fixtures
# =============================================================================
@pytest.fixture
def random_unicode_strings():
"""Collection of Unicode strings for fuzz testing."""
return [
"", # Empty
" ", # Whitespace
"\t\n\r", # Control chars
"Hello, World!", # Normal ASCII
"Привет мир", # Cyrillic
"你好世界", # Chinese
"こんにちは", # Japanese
"مرحبا", # Arabic
"🎉🔥💯", # Emoji
"a" * 10000, # Very long
"\x00\x01\x02", # Null and control
"line1\nline2\rline3", # Newlines
"'\"<>&", # HTML special chars
]
@pytest.fixture
def boundary_values():
"""Boundary values for testing limits."""
return {
"message_lengths": [0, 1, 99, 100, 101, 9999, 10000, 10001],
"title_lengths": [0, 1, 499, 500, 501],
"pagination_limits": [-1, 0, 1, 20, 100, 101, 1000],
"pagination_offsets": [-1, 0, 1, 100, 10000],
}