Files
core/tests/conftest.py

606 lines
20 KiB
Python
Raw Normal View History

2025-02-09 22:26:50 +03:00
import pytest
2025-08-12 14:45:59 +03:00
import os
from settings import FRONTEND_URL
2025-06-02 21:50:58 +03:00
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
2025-07-31 18:55:59 +03:00
import time
import uuid
from starlette.testclient import TestClient
2025-08-12 18:23:53 +03:00
import requests
2025-08-17 11:09:29 +03:00
import subprocess
import signal
import asyncio
from typing import Optional, Generator, AsyncGenerator
from contextlib import asynccontextmanager
2025-05-29 12:37:39 +03:00
2025-02-09 22:26:50 +03:00
from services.redis import redis
2025-07-31 18:55:59 +03:00
from orm.base import BaseModel as Base
def get_test_client():
"""
Создает и возвращает тестовый клиент для интеграционных тестов.
Returns:
TestClient: Клиент для выполнения тестовых запросов
"""
from starlette.testclient import TestClient
# Отложенный импорт для предотвращения циклических зависимостей
def _import_app():
from main import app
return app
return TestClient(_import_app())
2025-08-17 11:09:29 +03:00
2025-08-12 18:23:53 +03:00
@pytest.fixture(autouse=True, scope="session")
def _set_requests_default_timeout():
"""Глобально задаем таймаут по умолчанию для requests в тестах, чтобы исключить зависания.
🪓 Упрощение: мокаем методы requests, добавляя timeout=10, если он не указан.
"""
original_request = requests.sessions.Session.request
def request_with_default_timeout(self, method, url, **kwargs): # type: ignore[override]
if "timeout" not in kwargs:
kwargs["timeout"] = 10
return original_request(self, method, url, **kwargs)
requests.sessions.Session.request = request_with_default_timeout # type: ignore[assignment]
yield
requests.sessions.Session.request = original_request # type: ignore[assignment]
2025-02-09 22:26:50 +03:00
2025-02-11 12:00:35 +03:00
2025-02-09 22:26:50 +03:00
@pytest.fixture(scope="session")
2025-06-02 21:50:58 +03:00
def test_engine():
"""
Создает тестовый engine для всей сессии тестирования.
Использует in-memory SQLite для быстрых тестов.
"""
2025-07-31 18:55:59 +03:00
# Импортируем все модели, чтобы они были зарегистрированы
from orm.base import BaseModel as Base
from orm.community import Community, CommunityAuthor
from auth.orm import Author
from orm.draft import Draft, DraftAuthor, DraftTopic
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutReactionsFollower
from orm.topic import Topic
from orm.reaction import Reaction
from orm.invite import Invite
from orm.notification import Notification
2025-06-02 21:50:58 +03:00
engine = create_engine(
"sqlite:///:memory:", echo=False, poolclass=StaticPool, connect_args={"check_same_thread": False}
)
2025-07-31 18:55:59 +03:00
# Принудительно удаляем все таблицы и создаем заново
Base.metadata.drop_all(engine)
2025-06-02 21:50:58 +03:00
Base.metadata.create_all(engine)
yield engine
# Cleanup после всех тестов
Base.metadata.drop_all(engine)
@pytest.fixture(scope="session")
def test_session_factory(test_engine):
"""
Создает фабрику сессий для тестирования.
"""
return sessionmaker(bind=test_engine, expire_on_commit=False)
@pytest.fixture
2025-07-31 18:55:59 +03:00
def db_session(test_session_factory, test_engine):
2025-06-02 21:50:58 +03:00
"""
Создает новую сессию БД для каждого теста.
Простая реализация без вложенных транзакций.
"""
2025-07-31 18:55:59 +03:00
# Принудительно пересоздаем таблицы для каждого теста
from orm.base import BaseModel as Base
from sqlalchemy import inspect
# Удаляем все таблицы
Base.metadata.drop_all(test_engine)
# Создаем таблицы заново
Base.metadata.create_all(test_engine)
# Проверяем что таблица draft создана с правильной схемой
inspector = inspect(test_engine)
draft_columns = [col['name'] for col in inspector.get_columns('draft')]
print(f"Draft table columns: {draft_columns}")
# Убеждаемся что колонка shout существует
if 'shout' not in draft_columns:
print("WARNING: Column 'shout' not found in draft table!")
2025-06-02 21:50:58 +03:00
session = test_session_factory()
2025-07-02 22:30:21 +03:00
# Создаем дефолтное сообщество для тестов
from orm.community import Community
from auth.orm import Author
import time
# Создаем системного автора если его нет
2025-07-31 18:55:59 +03:00
system_author = session.query(Author).where(Author.slug == "system").first()
2025-07-02 22:30:21 +03:00
if not system_author:
system_author = Author(
name="System",
slug="system",
email="system@test.local",
created_at=int(time.time()),
updated_at=int(time.time()),
last_seen=int(time.time())
)
session.add(system_author)
session.flush()
# Создаем дефолтное сообщество если его нет
2025-07-31 18:55:59 +03:00
default_community = session.query(Community).where(Community.id == 1).first()
2025-07-02 22:30:21 +03:00
if not default_community:
default_community = Community(
id=1,
name="Главное сообщество",
slug="main",
desc="Основное сообщество для тестов",
pic="",
created_at=int(time.time()),
created_by=system_author.id,
settings={"default_roles": ["reader", "author"], "available_roles": ["reader", "author", "artist", "expert", "editor", "admin"]},
private=False
)
session.add(default_community)
session.commit()
2025-06-02 21:50:58 +03:00
yield session
# Очищаем все данные после теста
try:
for table in reversed(Base.metadata.sorted_tables):
session.execute(table.delete())
session.commit()
except Exception:
session.rollback()
finally:
session.close()
2025-02-09 22:26:50 +03:00
2025-02-11 12:00:35 +03:00
2025-02-09 22:26:50 +03:00
@pytest.fixture
2025-06-02 21:50:58 +03:00
def db_session_commit(test_session_factory):
"""
Создает сессию БД с реальными commit'ами для интеграционных тестов.
Используется когда нужно тестировать реальные транзакции.
"""
session = test_session_factory()
2025-02-11 12:00:35 +03:00
2025-07-31 18:55:59 +03:00
# Создаем дефолтное сообщество для тестов
2025-07-02 22:30:21 +03:00
from orm.community import Community
from auth.orm import Author
# Создаем системного автора если его нет
2025-07-31 18:55:59 +03:00
system_author = session.query(Author).where(Author.slug == "system").first()
2025-07-02 22:30:21 +03:00
if not system_author:
system_author = Author(
name="System",
slug="system",
email="system@test.local",
created_at=int(time.time()),
updated_at=int(time.time()),
last_seen=int(time.time())
)
session.add(system_author)
2025-07-31 18:55:59 +03:00
session.commit()
2025-07-02 22:30:21 +03:00
# Создаем дефолтное сообщество если его нет
2025-07-31 18:55:59 +03:00
default_community = session.query(Community).where(Community.id == 1).first()
2025-07-02 22:30:21 +03:00
if not default_community:
default_community = Community(
id=1,
name="Главное сообщество",
slug="main",
desc="Основное сообщество для тестов",
pic="",
created_at=int(time.time()),
created_by=system_author.id,
settings={"default_roles": ["reader", "author"], "available_roles": ["reader", "author", "artist", "expert", "editor", "admin"]},
private=False
)
session.add(default_community)
session.commit()
2025-02-09 22:26:50 +03:00
yield session
2025-02-11 12:00:35 +03:00
2025-06-02 21:50:58 +03:00
# Очищаем все данные после теста
try:
for table in reversed(Base.metadata.sorted_tables):
session.execute(table.delete())
session.commit()
except Exception:
session.rollback()
finally:
session.close()
2025-05-16 09:11:39 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def frontend_url():
"""
Возвращает URL фронтенда для тестов.
"""
return FRONTEND_URL or "http://localhost:3000"
2025-02-09 22:26:50 +03:00
2025-02-11 12:00:35 +03:00
2025-02-09 22:26:50 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def backend_url():
"""
Возвращает URL бэкенда для тестов.
"""
return "http://localhost:8000"
2025-07-31 18:55:59 +03:00
2025-08-17 11:09:29 +03:00
@pytest.fixture(scope="session")
def backend_server():
"""
🚀 Фикстура для автоматического запуска/остановки бэкенд сервера.
Запускает сервер только если он не запущен.
"""
backend_process: Optional[subprocess.Popen] = None
backend_running = False
# Проверяем, не запущен ли уже сервер
try:
response = requests.get("http://localhost:8000/", timeout=2)
if response.status_code == 200:
print("✅ Бэкенд сервер уже запущен")
backend_running = True
else:
backend_running = False
except:
backend_running = False
if not backend_running:
print("🔄 Запускаем бэкенд сервер для тестов...")
try:
# Запускаем бэкенд сервер
backend_process = subprocess.Popen(
["uv", "run", "python", "dev.py"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
2025-07-31 18:55:59 +03:00
2025-08-17 11:09:29 +03:00
# Ждем запуска бэкенда
print("⏳ Ждем запуска бэкенда...")
for i in range(30): # Ждем максимум 30 секунд
try:
response = requests.get("http://localhost:8000/", timeout=2)
if response.status_code == 200:
print("✅ Бэкенд сервер запущен")
backend_running = True
break
except:
pass
time.sleep(1)
else:
print("❌ Бэкенд сервер не запустился за 30 секунд")
if backend_process:
backend_process.terminate()
backend_process.wait()
raise Exception("Бэкенд сервер не запустился за 30 секунд")
except Exception as e:
print(f"❌ Ошибка запуска сервера: {e}")
if backend_process:
backend_process.terminate()
backend_process.wait()
raise Exception(f"Не удалось запустить бэкенд сервер: {e}")
yield backend_running
# Cleanup: останавливаем сервер только если мы его запускали
if backend_process and not backend_running:
print("🛑 Останавливаем бэкенд сервер...")
try:
backend_process.terminate()
backend_process.wait(timeout=10)
except subprocess.TimeoutExpired:
backend_process.kill()
backend_process.wait()
2025-06-02 21:50:58 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def test_client(backend_server):
2025-06-02 21:50:58 +03:00
"""
2025-08-17 11:09:29 +03:00
🧪 Создает тестовый клиент для API тестов.
Требует запущенный бэкенд сервер.
2025-06-02 21:50:58 +03:00
"""
2025-08-17 11:09:29 +03:00
return get_test_client()
2025-06-02 21:50:58 +03:00
2025-07-31 18:55:59 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
async def browser_context():
"""
🌐 Создает контекст браузера для e2e тестов.
Автоматически управляет жизненным циклом браузера.
"""
try:
from playwright.async_api import async_playwright
except ImportError:
pytest.skip("Playwright не установлен")
async with async_playwright() as p:
# Определяем headless режим
headless = os.getenv("PLAYWRIGHT_HEADLESS", "true").lower() == "true"
browser = await p.chromium.launch(
headless=headless,
args=[
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--disable-web-security",
"--disable-features=VizDisplayCompositor"
]
)
context = await browser.new_context(
viewport={"width": 1280, "height": 720},
ignore_https_errors=True,
java_script_enabled=True
)
yield context
await context.close()
await browser.close()
2025-07-02 22:30:21 +03:00
2025-07-31 18:55:59 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
async def page(browser_context):
"""
📄 Создает новую страницу для каждого теста.
"""
page = await browser_context.new_page()
# Устанавливаем таймауты
page.set_default_timeout(30000)
page.set_default_navigation_timeout(30000)
yield page
await page.close()
2025-07-02 22:30:21 +03:00
2025-07-31 18:55:59 +03:00
2025-08-17 11:09:29 +03:00
@pytest.fixture
def api_base_url(backend_server):
"""
🔗 Возвращает базовый URL для API тестов.
"""
return "http://localhost:8000/graphql"
2025-07-31 18:55:59 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def test_user_credentials():
"""
👤 Возвращает тестовые учетные данные для авторизации.
"""
return {
"email": "test_admin@discours.io",
"password": "password123"
}
2025-07-31 18:55:59 +03:00
2025-07-02 22:30:21 +03:00
2025-08-17 11:09:29 +03:00
@pytest.fixture
def auth_headers(api_base_url, test_user_credentials):
"""
🔐 Создает заголовки авторизации для API тестов.
"""
def _get_auth_headers(token: Optional[str] = None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
return headers
return _get_auth_headers
2025-07-31 18:55:59 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def wait_for_server():
"""
Утилита для ожидания готовности сервера.
"""
def _wait_for_server(url: str, max_attempts: int = 30, delay: float = 1.0):
"""Ждет готовности сервера по указанному URL."""
for attempt in range(max_attempts):
try:
response = requests.get(url, timeout=2)
if response.status_code == 200:
return True
except:
pass
time.sleep(delay)
return False
return _wait_for_server
2025-07-31 18:55:59 +03:00
2025-08-17 11:09:29 +03:00
@pytest.fixture
def test_users(db_session):
"""Создает тестовых пользователей для тестов"""
2025-08-17 16:33:54 +03:00
from auth.orm import Author
2025-08-17 11:09:29 +03:00
# Создаем первого пользователя (администратор)
admin_user = Author(
slug="test-admin",
email="test_admin@discours.io",
password="hashed_password_123",
name="Test Admin",
bio="Test admin user for testing",
pic="https://example.com/avatar1.jpg",
oauth={}
)
db_session.add(admin_user)
# Создаем второго пользователя (обычный пользователь)
regular_user = Author(
slug="test-user",
email="test_user@discours.io",
password="hashed_password_456",
name="Test User",
bio="Test regular user for testing",
pic="https://example.com/avatar2.jpg",
oauth={}
2025-07-31 18:55:59 +03:00
)
2025-08-17 11:09:29 +03:00
db_session.add(regular_user)
# Создаем третьего пользователя (только читатель)
reader_user = Author(
slug="test-reader",
email="test_reader@discours.io",
password="hashed_password_789",
name="Test Reader",
bio="Test reader user for testing",
pic="https://example.com/avatar3.jpg",
oauth={}
)
db_session.add(reader_user)
2025-07-31 18:55:59 +03:00
db_session.commit()
2025-08-17 11:09:29 +03:00
return [admin_user, regular_user, reader_user]
2025-07-31 18:55:59 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def test_community(db_session, test_users):
"""Создает тестовое сообщество для тестов"""
from orm.community import Community
2025-07-31 18:55:59 +03:00
community = Community(
2025-08-17 11:09:29 +03:00
name="Test Community",
slug="test-community",
desc="A test community for testing purposes",
created_by=test_users[0].id, # Администратор создает сообщество
2025-07-31 18:55:59 +03:00
settings={
"default_roles": ["reader", "author"],
2025-08-17 11:09:29 +03:00
"custom_setting": "custom_value"
2025-07-31 18:55:59 +03:00
}
)
db_session.add(community)
db_session.commit()
2025-08-17 11:09:29 +03:00
return community
2025-07-31 18:55:59 +03:00
2025-08-17 11:09:29 +03:00
@pytest.fixture
def community_with_creator(db_session, test_users):
"""Создает сообщество с создателем"""
from orm.community import Community
community = Community(
name="Community With Creator",
slug="community-with-creator",
desc="A test community with a creator",
created_by=test_users[0].id,
settings={"default_roles": ["reader", "author"]}
)
db_session.add(community)
db_session.commit()
return community
2025-07-31 18:55:59 +03:00
@pytest.fixture
def community_without_creator(db_session):
2025-08-17 11:09:29 +03:00
"""Создает сообщество без создателя"""
2025-07-31 18:55:59 +03:00
from orm.community import Community
2025-08-17 11:09:29 +03:00
2025-07-31 18:55:59 +03:00
community = Community(
name="Community Without Creator",
2025-08-17 11:09:29 +03:00
slug="community-without-creator",
desc="A test community without a creator",
created_by=None, # Без создателя
settings={"default_roles": ["reader"]}
2025-07-31 18:55:59 +03:00
)
db_session.add(community)
db_session.commit()
2025-08-17 11:09:29 +03:00
2025-07-31 18:55:59 +03:00
return community
@pytest.fixture
def admin_user_with_roles(db_session, test_users, test_community):
2025-08-17 11:09:29 +03:00
"""Создает администратора с ролями в сообществе"""
2025-07-31 18:55:59 +03:00
from orm.community import CommunityAuthor
2025-08-17 11:09:29 +03:00
2025-07-31 18:55:59 +03:00
ca = CommunityAuthor(
community_id=test_community.id,
2025-08-17 11:09:29 +03:00
author_id=test_users[0].id,
roles="admin,author,reader"
2025-07-31 18:55:59 +03:00
)
db_session.add(ca)
db_session.commit()
2025-08-17 11:09:29 +03:00
return test_users[0]
2025-07-31 18:55:59 +03:00
@pytest.fixture
def regular_user_with_roles(db_session, test_users, test_community):
2025-08-17 11:09:29 +03:00
"""Создает обычного пользователя с ролями в сообществе"""
2025-07-31 18:55:59 +03:00
from orm.community import CommunityAuthor
2025-08-17 11:09:29 +03:00
2025-07-31 18:55:59 +03:00
ca = CommunityAuthor(
community_id=test_community.id,
2025-08-17 11:09:29 +03:00
author_id=test_users[1].id,
roles="author,reader"
2025-07-31 18:55:59 +03:00
)
db_session.add(ca)
db_session.commit()
2025-08-17 11:09:29 +03:00
return test_users[1]
2025-07-31 18:55:59 +03:00
2025-08-17 11:09:29 +03:00
@pytest.fixture
def mock_verify(monkeypatch):
"""Мокает функцию верификации для тестов"""
from unittest.mock import AsyncMock
mock = AsyncMock()
# Здесь можно настроить возвращаемые значения по умолчанию
return mock
2025-08-12 14:45:59 +03:00
@pytest.fixture
2025-08-17 11:09:29 +03:00
def redis_client():
"""Создает Redis клиент для тестов токенов"""
from services.redis import RedisService
redis_service = RedisService()
return redis_service._client
2025-08-17 11:37:55 +03:00
# Mock для Redis если он недоступен
@pytest.fixture(autouse=True)
def mock_redis_if_unavailable():
"""Автоматически мокает Redis если он недоступен"""
try:
import redis
# Пробуем подключиться к Redis
r = redis.Redis(host='localhost', port=6379, socket_connect_timeout=1)
r.ping()
# Redis доступен, не мокаем
yield
except Exception:
# Redis недоступен, мокаем
with patch('services.redis.RedisService') as mock_redis:
# Создаем базовый mock для Redis методов
mock_redis.return_value.get.return_value = None
mock_redis.return_value.set.return_value = True
mock_redis.return_value.delete.return_value = True
mock_redis.return_value.exists.return_value = False
mock_redis.return_value.ping.return_value = True
mock_redis.return_value.is_connected = False
yield