fix: close remaining security gaps, test fixes, frontend integration, event bus
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- RCE: move _check_dangerous_imports() BEFORE exec_module() in plugins.py - verify_ws_origin: reject empty Origin header when CORS configured - Test: ai_app fixture with permission_registry init for ai_assistant - Test: login_client sets CSRF token + Origin as client default headers - Test: SESSION_COOKIE_SECURE=false override + get_settings.cache_clear() - Test: asyncio_default_test_loop_scope=session fixes event loop closed - Test: fix 15 assertions (paths, variables, auth expectations) - Frontend: integrate SavedFilterBar in ContactsList, Mail, Calendar - Frontend: integrate TagSelector in ContactsList, Mail, Calendar - Event Bus: add 4 subscribers in system_notif (conversation/participant/reaction) - Docs: update all analysis reports and FIX-PLAN-V2 to current state
This commit is contained in:
+43
-1
@@ -9,6 +9,12 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# Override .env settings for tests — must be set BEFORE any app imports
|
||||
# so that pydantic-settings picks them up on first get_settings() call
|
||||
os.environ["SESSION_COOKIE_SECURE"] = "false"
|
||||
os.environ["SESSION_COOKIE_SAMESITE"] = "lax"
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
@@ -91,6 +97,10 @@ from app.services.plugin_service import reset_plugin_service_for_testing # noqa
|
||||
|
||||
TEST_DB_URL = "postgresql+asyncpg://leocrm:leocrm@localhost:5432/leocrm_test"
|
||||
|
||||
# Clear settings cache so the env overrides (set at top of file) take effect
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def _get_sync_engine():
|
||||
"""Create a sync engine for DDL operations (drop/create schema).
|
||||
@@ -228,6 +238,32 @@ async def app(engine: AsyncEngine, redis_client: aioredis.Redis):
|
||||
await close_engine()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def ai_app(engine: AsyncEngine, redis_client: aioredis.Redis):
|
||||
"""FastAPI app with ai_assistant plugin activated for AI copilot tests."""
|
||||
from app.core.permission_registry import init_permission_registry, register_plugin_permissions
|
||||
reset_engine_for_testing(engine)
|
||||
app = create_app()
|
||||
# Re-initialize AFTER create_app() which reads active plugins from DB
|
||||
# (DB is empty in tests, so create_app leaves active_plugin_names empty)
|
||||
init_permission_registry(active_plugin_names={"ai_assistant"})
|
||||
# Register ai_assistant permissions so require_permission checks work
|
||||
from app.plugins.builtins.ai_assistant.plugin import AIAssistantPlugin
|
||||
plugin = AIAssistantPlugin()
|
||||
if hasattr(plugin.manifest, 'permissions') and plugin.manifest.permissions:
|
||||
register_plugin_permissions("ai_assistant", plugin.manifest.permissions)
|
||||
yield app
|
||||
await close_engine()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def ai_client(ai_app) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""HTTP async test client with ai_assistant plugin active."""
|
||||
transport = ASGITransport(app=ai_app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(app) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""HTTP async test client."""
|
||||
@@ -344,13 +380,19 @@ async def seed_tenant_and_users(db: AsyncSession) -> dict[str, Any]:
|
||||
async def login_client(
|
||||
client: AsyncClient, email: str, password: str = "TestPass123!"
|
||||
) -> dict[str, str]:
|
||||
"""Login via HTTP API and return cookies dict."""
|
||||
"""Login via HTTP API, set CSRF token on client, return cookies dict."""
|
||||
resp = await client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
headers=ORIGIN_HEADER,
|
||||
)
|
||||
assert resp.status_code == 200, f"Login failed: {resp.status_code} {resp.text}"
|
||||
data = resp.json()
|
||||
csrf_token = data.get("csrf_token", "")
|
||||
# Set csrf_token as default header on client (merged with per-request headers)
|
||||
client.headers["X-CSRF-Token"] = csrf_token
|
||||
# Also add Origin to client defaults so per-request headers aren't needed
|
||||
client.headers["Origin"] = ORIGIN_HEADER["Origin"]
|
||||
return dict(resp.cookies)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user