Newer
Older
gnexus-tasks / backend / tests / conftest.py
"""Фикстуры тестов: in-memory БД и авторизация-заглушка."""

import os
import tempfile
from collections.abc import Iterator
from typing import Any

# env-переменные — ДО импорта app.*: get_settings() кэшируется при первом вызове
_tmpdir = tempfile.mkdtemp(prefix="gntodo-att-")
os.environ.setdefault("ATTACHMENTS_PATH", _tmpdir)
os.environ.setdefault("MCP_TOKEN", "test-mcp-token")
MCP_TOKEN = os.environ["MCP_TOKEN"]

import pytest  # noqa: E402
from fastapi.testclient import TestClient  # noqa: E402
from sqlalchemy import create_engine  # noqa: E402
from sqlalchemy.orm import Session, sessionmaker  # noqa: E402
from sqlalchemy.pool import StaticPool  # noqa: E402

from app import services  # noqa: E402
from app.db import Base  # noqa: E402
from app.dependencies import get_db, require_user  # noqa: E402
from app.main import app  # noqa: E402

detailing = services.detailing

TEST_DB_URL = "sqlite://"

_engine = create_engine(
    TEST_DB_URL,
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)
Base.metadata.create_all(_engine)
_test_session_factory = sessionmaker(bind=_engine, expire_on_commit=False)


def _override_get_db() -> Iterator[Session]:
    session = _test_session_factory()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()


app.dependency_overrides[get_db] = _override_get_db

AUTH_USER: dict[str, str] = {"user_id": "1", "email": "test@example.com", "locale": "ru"}
app.dependency_overrides[require_user] = lambda: AUTH_USER


@pytest.fixture(autouse=True)
def _mock_detailing(monkeypatch: Any) -> Iterator[None]:
    """Фоновая детализация в тестах: тестовая БД и без реального вызова Ollama."""
    monkeypatch.setattr(detailing, "get_session_factory", lambda: _test_session_factory)
    import app.mcp_server as mcp_server

    monkeypatch.setattr(mcp_server, "get_session_factory", lambda: _test_session_factory)
    # Фоновый поток детализации из create_task гоняет транзакции по общему
    # соединению SQLite (StaticPool) — в тестах не порождаем потоков вовсе.
    monkeypatch.setattr(mcp_server, "detail_task", lambda task_id: None)
    # Запрещаем сетевой вызов LLM: propose вернёт None (как при недоступной Ollama).
    # Тесты при необходимости подменяют propose/generate поверх этого.
    monkeypatch.setattr(
        detailing.DetailingService,
        "generate",
        lambda self, prompt: (_ for _ in ()).throw(RuntimeError("LLM отключён в тестах")),
    )


@pytest.fixture(autouse=True)
def _isolate_db() -> Iterator[None]:
    """Чистые таблицы перед каждым тестом (in-memory БД общая на процесс)."""
    conn = _engine.connect()
    trans = conn.begin()
    for table in reversed(Base.metadata.sorted_tables):
        conn.execute(table.delete())
    trans.commit()
    conn.close()
    yield


@pytest.fixture
def client() -> Iterator[TestClient]:
    # Без `with`: lifespan (старт MCP session manager) в обычных тестах не нужен,
    # а менеджер сессий допускает только один запуск на инстанс.
    yield TestClient(app)