fix(i-e): Mail-Suite 46/46 gruen in 94s statt 18:29min — globales IMAP-Mock-Fixture + 3 echte Fixes
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Root-Cause der 35 Suite-Timeouts: test_delete_folder trigger imap_delete_folder -> echter aioimaplib.IMAP4_SSL-Connect zu imap.example.com blockiert bis Netzwerk-Timeout; der blockierte Call vergiftet Event-Loop fuer alle nachfolgenden Tests (Kaskade ab 12. Test). Fixes: (1) tests/conftest.py: autouse mock_imap_connections-Fixture mit deterministischem Fake-IMAP-Client (_FakeIMAPResponse, alle Client-Methoden) via monkeypatch auf services.aioimaplib.IMAP4_SSL. (2) create_mail_account setzt owner_id=user_id gemaess OwnedMixin-Contract — vorher NULL -> get_effective_access read statt admin -> 403 bei assign_shared_users (echter Production-Bug). (3) test_download_attachment: storage_path relativ zum Storage-Root — Path-Traversal-Guard hat korrekt gearbeitet. (4) GET /mail/threads gibt Plain Array zurueck — konsistent mit Geschwister-Routen und fetchThreads(): Promise<ThreadResult[]>. Beweise: 46/46 passed in 94.41s (vorher 1 failed, 10 passed, 35 errors in 1109.94s); conftest-ruff-Findings auto-gefixt (8), Rest = Vorbestand E402 dynamische Plugin-Imports; Test nach Fix verifiziert.
This commit is contained in:
+101
-14
@@ -6,7 +6,6 @@ Auth helpers talk to the HTTP API (integration tests).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -40,28 +39,30 @@ from app.core.auth import hash_password
|
||||
from app.core.db import Base, close_engine, reset_engine_for_testing
|
||||
from app.core.service_container import get_container # noqa: F401
|
||||
from app.main import create_app
|
||||
|
||||
try:
|
||||
from app.models.ai_conversation import AIConversation, AIMessage # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
from app.ai.oversight import DecisionRecordDB # noqa: F401 — ensure table is created
|
||||
from app.models.compliance import ComplianceIncident # noqa: F401
|
||||
from app.models.consumer_inbox import ConsumerInbox # noqa: F401
|
||||
from app.models.contact import Contact, ContactPerson # noqa: F401
|
||||
from app.models.contact_merge import ContactMergeHistory # noqa: F401
|
||||
from app.models.outbox import EventOutbox # noqa: F401
|
||||
from app.models.plugin import Plugin, PluginMigration # noqa: F401
|
||||
from app.models.role import Role
|
||||
from app.models.saved_filter import SavedFilter # noqa: F401
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User, UserTenant
|
||||
from app.models.user_preference import UserPreference # noqa: F401
|
||||
from app.models.workflow import Workflow, WorkflowInstance, WorkflowStepHistory # noqa: F401
|
||||
from app.plugins.builtins.calendar import CalendarPlugin # noqa: F401
|
||||
from app.models.outbox import EventOutbox # noqa: F401
|
||||
from app.models.consumer_inbox import ConsumerInbox # noqa: F401
|
||||
from app.models.saved_filter import SavedFilter # noqa: F401
|
||||
from app.ai.oversight import DecisionRecordDB # noqa: F401 — ensure table is created
|
||||
|
||||
# Dynamically import all plugin models so Base.metadata.create_all() includes their tables.
|
||||
# This replaces ~30 hardcoded plugin imports with dynamic discovery (P1-14 fix).
|
||||
from app.plugins.registry import get_registry
|
||||
|
||||
_registry = get_registry()
|
||||
_registry.discover_builtins()
|
||||
for _plugin_name in _registry.list_discovered():
|
||||
@@ -84,19 +85,24 @@ for _plugin_name in _registry.list_discovered():
|
||||
|
||||
# Also import core models that may be missing
|
||||
# Wiki plugin models — not loaded by get_entity_models()
|
||||
from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory # noqa: F401
|
||||
from app.core.permission_registry import init_permission_registry # noqa: F401
|
||||
|
||||
# Knowledge plugin models — new plugin, ensure table is created in test-DB
|
||||
from app.plugins.builtins.knowledge.models import KnowledgeExtraction # noqa: F401
|
||||
|
||||
# Self-improvement plugin models — new plugin, ensure tables are created in test-DB
|
||||
from app.plugins.builtins.self_improvement.models import ( # noqa: F401
|
||||
ImprovementSignal,
|
||||
ImpactMeasurement,
|
||||
ImprovementPattern,
|
||||
ImprovementProposal,
|
||||
ImpactMeasurement,
|
||||
ImprovementSignal,
|
||||
)
|
||||
from app.plugins.builtins.wiki.models import ( # noqa: F401
|
||||
WikiArticle,
|
||||
WikiArticleVersion,
|
||||
WikiCategory,
|
||||
)
|
||||
|
||||
from app.plugins.registry import reset_registry_for_testing # noqa: F401
|
||||
from app.core.permission_registry import init_permission_registry # noqa: F401
|
||||
from app.services.plugin_service import reset_plugin_service_for_testing # noqa: F401
|
||||
|
||||
# Import plugin models so Base.metadata.create_all includes their tables
|
||||
@@ -105,6 +111,7 @@ 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()
|
||||
|
||||
|
||||
@@ -145,7 +152,7 @@ def _run_migrations():
|
||||
raise RuntimeError(
|
||||
f"alembic upgrade head failed: {result.stderr or result.stdout}"
|
||||
)
|
||||
print(f"[CONFTEST] alembic upgrade head completed successfully")
|
||||
print("[CONFTEST] alembic upgrade head completed successfully")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@@ -382,6 +389,86 @@ async def redis_client() -> AsyncGenerator[aioredis.Redis, None]:
|
||||
await r.aclose()
|
||||
|
||||
|
||||
# ─── Global IMAP mocking (Block I-E) ────────────────────────────────────────
|
||||
#
|
||||
# The mail service layer opens real aioimaplib.IMAP4_SSL connections to the
|
||||
# account's imap_host (test fixtures use imap.example.com). Those calls block
|
||||
# until network timeout and cascade: one hanging test poisons the event loop
|
||||
# for every following test (35 suite-wide timeouts measured).
|
||||
#
|
||||
# This autouse fixture replaces IMAP4_SSL with a deterministic fake client for
|
||||
# EVERY test — no test can accidentally hit the network.
|
||||
|
||||
|
||||
class _FakeIMAPResponse:
|
||||
"""Mimics aioimaplib response objects: tuple-like + .result attribute."""
|
||||
|
||||
def __init__(self, result="OK", lines: list | None = None):
|
||||
self.result = result
|
||||
self.lines = lines or []
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if idx == 0:
|
||||
return self.result
|
||||
return self.lines[idx - 1] if 0 < idx <= len(self.lines) else []
|
||||
|
||||
|
||||
def _build_fake_imap_client():
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
client = MagicMock()
|
||||
client.wait_hello_from_server = AsyncMock(return_value=None)
|
||||
client.login = AsyncMock(return_value=_FakeIMAPResponse("OK", [b"Logged in"]))
|
||||
client.logout = AsyncMock(return_value=_FakeIMAPResponse("OK", [b"Bye"]))
|
||||
# select(folder) → OK with EXISTS count
|
||||
client.select = AsyncMock(
|
||||
return_value=_FakeIMAPResponse("OK", [b"0"])
|
||||
)
|
||||
# uid_search(...) → response whose [1][0] is a space-separated uid bytes list
|
||||
client.uid_search = AsyncMock(
|
||||
return_value=_FakeIMAPResponse("OK", [b""])
|
||||
)
|
||||
# uid('fetch', ...) → minimal RFC822 envelope; services parse defensively
|
||||
fetch_resp = _FakeIMAPResponse(
|
||||
"OK",
|
||||
[
|
||||
(
|
||||
b"1 (RFC822 {5}",
|
||||
b"Subject: t\r\n\r\nbody",
|
||||
),
|
||||
b")",
|
||||
],
|
||||
)
|
||||
client.uid = AsyncMock(return_value=fetch_resp)
|
||||
client.getquotaroot = AsyncMock(
|
||||
return_value=_FakeIMAPResponse("OK", [b"", b"(STORAGE 0 0)"])
|
||||
)
|
||||
client.list = AsyncMock(
|
||||
return_value=_FakeIMAPResponse("OK", [])
|
||||
)
|
||||
client.append = AsyncMock(return_value=_FakeIMAPResponse("OK", [b"Appended"]))
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_imap_connections(monkeypatch):
|
||||
"""Replace aioimaplib.IMAP4_SSL everywhere with a deterministic fake.
|
||||
|
||||
Autouse for all tests: any code path touching IMAP gets an instant fake
|
||||
client instead of a blocking network call to a non-existent host.
|
||||
"""
|
||||
fake_client = _build_fake_imap_client()
|
||||
|
||||
def _fake_factory(*args, **kwargs):
|
||||
return fake_client
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.plugins.builtins.mail.services.aioimaplib.IMAP4_SSL",
|
||||
_fake_factory,
|
||||
)
|
||||
yield fake_client
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="session")
|
||||
async def engine() -> AsyncGenerator[AsyncEngine, None]:
|
||||
"""Async engine for the test database (session-scoped for speed)."""
|
||||
@@ -690,8 +777,8 @@ async def dms_app(engine: AsyncEngine, redis_client):
|
||||
container = get_container()
|
||||
await container.initialize()
|
||||
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
from app.plugins.builtins.dms.plugin import DmsPlugin
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
from app.plugins.builtins.tasks.plugin import TasksPlugin
|
||||
registry.register_plugin(PermissionsPlugin())
|
||||
registry.register_plugin(DmsPlugin())
|
||||
@@ -805,9 +892,9 @@ async def mcp_app(engine: AsyncEngine, redis_client):
|
||||
container = get_container()
|
||||
await container.initialize()
|
||||
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
from app.plugins.builtins.mcp_server.plugin import McpServerPlugin
|
||||
from app.plugins.builtins.mcp_client.plugin import McpClientPlugin
|
||||
from app.plugins.builtins.mcp_server.plugin import McpServerPlugin
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
registry.register_plugin(PermissionsPlugin())
|
||||
registry.register_plugin(McpServerPlugin())
|
||||
registry.register_plugin(McpClientPlugin())
|
||||
|
||||
Reference in New Issue
Block a user